diff --git a/backend/dist/controllers/emails.js b/backend/dist/controllers/emails.js index 25cb6c37..9e827e48 100644 --- a/backend/dist/controllers/emails.js +++ b/backend/dist/controllers/emails.js @@ -25,7 +25,6 @@ const createEmail = (req, res, next) => __awaiter(void 0, void 0, void 0, functi questionType = "Other message"; } else { - console.log("question: " + question); questionType = question.split(" ").slice(3).join(" "); questionType = questionType[0].toUpperCase() + questionType.slice(1); } diff --git a/backend/dist/controllers/eventDetails.js b/backend/dist/controllers/eventDetails.js index 387bfb61..abf3812a 100644 --- a/backend/dist/controllers/eventDetails.js +++ b/backend/dist/controllers/eventDetails.js @@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.updateEventDetails = exports.createEventDetails = exports.getEventDetails = exports.getAllEventDetails = void 0; +exports.deleteEventDetails = exports.updateEventDetails = exports.createEventDetails = exports.getEventDetails = exports.getAllEventDetails = void 0; const express_validator_1 = require("express-validator"); const http_errors_1 = __importDefault(require("http-errors")); const eventDetails_1 = __importDefault(require("../models/eventDetails")); @@ -46,7 +46,7 @@ const getEventDetails = (req, res, next) => __awaiter(void 0, void 0, void 0, fu exports.getEventDetails = getEventDetails; const createEventDetails = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { const errors = (0, express_validator_1.validationResult)(req); - const { name, description, guidelines, date, location, imageURI } = req.body; + const { name, description, guidelines, date, startTime, endTime, location, imageURI, description_short, } = req.body; try { (0, validationErrorParser_1.default)(errors); const eventDetails = yield eventDetails_1.default.create({ @@ -54,8 +54,11 @@ const createEventDetails = (req, res, next) => __awaiter(void 0, void 0, void 0, description, guidelines, date, + startTime, + endTime, location, imageURI, + description_short, }); res.status(201).json(eventDetails); } @@ -68,7 +71,6 @@ const updateEventDetails = (req, res, next) => __awaiter(void 0, void 0, void 0, const errors = (0, express_validator_1.validationResult)(req); const { id } = req.params; if (id !== req.body._id) { - // If the _id in the URL does not match the _id in the body, bad request res.status(400); } try { @@ -81,6 +83,7 @@ const updateEventDetails = (req, res, next) => __awaiter(void 0, void 0, void 0, const updatedEventDetails = yield eventDetails_1.default.findById(id); if (updatedEventDetails === null) { // No event found, something went wrong + console.log("updatedEventDetails is null"); res.status(404); } res.status(200).json(updatedEventDetails); @@ -90,3 +93,17 @@ const updateEventDetails = (req, res, next) => __awaiter(void 0, void 0, void 0, } }); exports.updateEventDetails = updateEventDetails; +const deleteEventDetails = (req, res, next) => __awaiter(void 0, void 0, void 0, function* () { + const { id } = req.params; + try { + const eventDetails = yield eventDetails_1.default.findByIdAndDelete(id); + if (!eventDetails) { + throw (0, http_errors_1.default)(404, "event not found"); + } + res.status(200).json(eventDetails); + } + catch (error) { + next(error); + } +}); +exports.deleteEventDetails = deleteEventDetails; diff --git a/backend/dist/controllers/newsletter.js b/backend/dist/controllers/newsletter.js index a4b33747..0ea0df2c 100644 --- a/backend/dist/controllers/newsletter.js +++ b/backend/dist/controllers/newsletter.js @@ -56,7 +56,6 @@ const createNewsletter = (req, res, next) => __awaiter(void 0, void 0, void 0, f date, content, }); - console.log("newsletter: ", newsletter); res.status(201).json(newsletter); } catch (error) { diff --git a/backend/dist/models/eventDetails.js b/backend/dist/models/eventDetails.js index 8a0986ce..bd33a4a1 100644 --- a/backend/dist/models/eventDetails.js +++ b/backend/dist/models/eventDetails.js @@ -6,7 +6,10 @@ const eventDetailsSchema = new mongoose_1.Schema({ description: { type: String, required: true }, guidelines: { type: String, required: true }, date: { type: String, required: true }, + startTime: { type: String, required: true }, + endTime: { type: String, required: true }, location: { type: String, required: true }, imageURI: { type: String, required: true }, + description_short: { type: String, required: true }, }); exports.default = (0, mongoose_1.model)("EventDetails", eventDetailsSchema); diff --git a/backend/dist/routes/eventDetails.js b/backend/dist/routes/eventDetails.js index a23b698f..feef8a4b 100644 --- a/backend/dist/routes/eventDetails.js +++ b/backend/dist/routes/eventDetails.js @@ -35,4 +35,5 @@ router.get("/:id", EventDetailsValidator.getEventDetails, EventDetailsController router.put("/:id", // getEventDetails validator works to just check ID EventDetailsValidator.getEventDetails, EventDetailsController.updateEventDetails); router.post("/", EventDetailsValidator.createEventDetails, EventDetailsController.createEventDetails); +router.delete("/:id", EventDetailsValidator.deleteEventDetails, EventDetailsController.deleteEventDetails); exports.default = router; diff --git a/backend/dist/validators/eventDetails.js b/backend/dist/validators/eventDetails.js index 4c5a5336..04a26eea 100644 --- a/backend/dist/validators/eventDetails.js +++ b/backend/dist/validators/eventDetails.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.getEventDetails = exports.createEventDetails = void 0; +exports.deleteEventDetails = exports.getEventDetails = exports.createEventDetails = void 0; const express_validator_1 = require("express-validator"); const makeIDValidator = () => (0, express_validator_1.body)("_id") .exists() @@ -47,6 +47,12 @@ const makeImageURIValidator = () => (0, express_validator_1.body)("imageURI") .bail() .isURL() .withMessage("imageURI must be a URL"); +const makeDescriptionShortValidator = () => (0, express_validator_1.body)("description_short") + .exists() + .withMessage("description_short is required") + .bail() + .isString() + .withMessage("description_short must be a string"); exports.createEventDetails = [ makeNameValidator(), makeDescriptionValidator(), @@ -54,5 +60,7 @@ exports.createEventDetails = [ makeDateValidator(), makeLocationValidator(), makeImageURIValidator(), + makeDescriptionShortValidator(), ]; exports.getEventDetails = [makeIDValidator()]; +exports.deleteEventDetails = [makeIDValidator()]; diff --git a/backend/src/controllers/emails.ts b/backend/src/controllers/emails.ts index c8beb4cc..e8ff310f 100644 --- a/backend/src/controllers/emails.ts +++ b/backend/src/controllers/emails.ts @@ -17,7 +17,6 @@ export const createEmail: RequestHandler = async (req, res, next) => { if (question === "Other") { questionType = "Other message"; } else { - console.log("question: " + question); questionType = question.split(" ").slice(3).join(" "); questionType = questionType[0].toUpperCase() + questionType.slice(1); } diff --git a/backend/src/controllers/eventDetails.ts b/backend/src/controllers/eventDetails.ts index b0af355d..ea0b5805 100644 --- a/backend/src/controllers/eventDetails.ts +++ b/backend/src/controllers/eventDetails.ts @@ -34,8 +34,19 @@ export const getEventDetails: RequestHandler = async (req, res, next) => { }; export const createEventDetails: RequestHandler = async (req, res, next) => { + console.log("backend createEventDetails. req.body: ", req.body); const errors = validationResult(req); - const { name, description, guidelines, date, location, imageURI } = req.body; + const { + name, + description, + guidelines, + date, + startTime, + endTime, + location, + imageURI, + description_short, + } = req.body; try { validationErrorParser(errors); @@ -45,10 +56,14 @@ export const createEventDetails: RequestHandler = async (req, res, next) => { description, guidelines, date, + startTime, + endTime, location, imageURI, + description_short, }); + // console.log("added eventDetails: ", eventDetails); res.status(201).json(eventDetails); } catch (error) { next(error); @@ -60,7 +75,6 @@ export const updateEventDetails: RequestHandler = async (req, res, next) => { const { id } = req.params; if (id !== req.body._id) { - // If the _id in the URL does not match the _id in the body, bad request res.status(400); } @@ -75,6 +89,7 @@ export const updateEventDetails: RequestHandler = async (req, res, next) => { const updatedEventDetails = await EventDetails.findById(id); if (updatedEventDetails === null) { // No event found, something went wrong + console.log("updatedEventDetails is null"); res.status(404); } res.status(200).json(updatedEventDetails); @@ -82,3 +97,18 @@ export const updateEventDetails: RequestHandler = async (req, res, next) => { next(error); } }; +export const deleteEventDetails: RequestHandler = async (req, res, next) => { + const { id } = req.params; + + try { + const eventDetails = await EventDetails.findByIdAndDelete(id); + + if (!eventDetails) { + throw createHttpError(404, "event not found"); + } + + res.status(200).json(eventDetails); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/controllers/newsletter.ts b/backend/src/controllers/newsletter.ts index e3b57c66..2b8a0fbe 100644 --- a/backend/src/controllers/newsletter.ts +++ b/backend/src/controllers/newsletter.ts @@ -49,7 +49,6 @@ export const createNewsletter: RequestHandler = async (req, res, next) => { content, }); - console.log("newsletter: ", newsletter); res.status(201).json(newsletter); } catch (error) { console.error("Error creating newsletter:", error); diff --git a/backend/src/models/eventDetails.ts b/backend/src/models/eventDetails.ts index e6d47ac7..1f048f4b 100644 --- a/backend/src/models/eventDetails.ts +++ b/backend/src/models/eventDetails.ts @@ -5,8 +5,11 @@ const eventDetailsSchema = new Schema({ description: { type: String, required: true }, guidelines: { type: String, required: true }, date: { type: String, required: true }, + startTime: { type: String, required: true }, + endTime: { type: String, required: true }, location: { type: String, required: true }, imageURI: { type: String, required: true }, + description_short: { type: String, required: true }, }); type EventDetails = InferSchemaType; diff --git a/backend/src/routes/eventDetails.ts b/backend/src/routes/eventDetails.ts index bd733e83..4a884f72 100644 --- a/backend/src/routes/eventDetails.ts +++ b/backend/src/routes/eventDetails.ts @@ -16,5 +16,10 @@ router.post( EventDetailsValidator.createEventDetails, EventDetailsController.createEventDetails, ); +router.delete( + "/:id", + EventDetailsValidator.deleteEventDetails, + EventDetailsController.deleteEventDetails, +); export default router; diff --git a/backend/src/services/paypal.ts b/backend/src/services/paypal.ts index e86cc026..b1e0f00e 100644 --- a/backend/src/services/paypal.ts +++ b/backend/src/services/paypal.ts @@ -1,6 +1,8 @@ const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID; const PAYPAL_CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET; +console.log("CLIENT ID: ", PAYPAL_CLIENT_ID, " SECRET: ", PAYPAL_CLIENT_SECRET); + const base = "https://api-m.sandbox.paypal.com"; /** @@ -24,6 +26,7 @@ const generateAccessToken = async () => { throw new Error("MISSING_PAYPAL_API_CREDENTIALS"); } const auth = Buffer.from(PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET).toString("base64"); + console.log("auth: ", auth); const response = await fetch(`${base}/v1/oauth2/token`, { method: "POST", body: "grant_type=client_credentials", @@ -33,6 +36,7 @@ const generateAccessToken = async () => { }); const data = await response.json(); + console.log("ACCESS TOKEN RESPONSE: ", data); return data.access_token; } catch (error) { console.error("Failed to generate PayPal Access Token: ", error); @@ -54,6 +58,7 @@ async function handleResponse(response: Response) { export async function createOrder(cart: Cart) { const accessToken = await generateAccessToken(); + console.log("creating order with ", accessToken); const url = `${base}/v2/checkout/orders`; console.log("shopping cart info", cart); diff --git a/backend/src/validators/eventDetails.ts b/backend/src/validators/eventDetails.ts index 91ec8450..0caf36b3 100644 --- a/backend/src/validators/eventDetails.ts +++ b/backend/src/validators/eventDetails.ts @@ -52,6 +52,13 @@ const makeImageURIValidator = () => .bail() .isURL() .withMessage("imageURI must be a URL"); +const makeDescriptionShortValidator = () => + body("description_short") + .exists() + .withMessage("description_short is required") + .bail() + .isString() + .withMessage("description_short must be a string"); export const createEventDetails = [ makeNameValidator(), @@ -60,6 +67,8 @@ export const createEventDetails = [ makeDateValidator(), makeLocationValidator(), makeImageURIValidator(), + makeDescriptionShortValidator(), ]; export const getEventDetails = [makeIDValidator()]; +export const deleteEventDetails = [makeIDValidator()]; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fac4fc07..7ee6b774 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@mui/material": "^5.15.12", "@mui/x-data-grid": "^7.1.1", "@paypal/react-paypal-js": "^8.2.0", + "date-fns": "^3.6.0", "envalid": "^8.0.0", "firebase": "^10.11.0", "html2canvas": "^1.4.1", @@ -23,6 +24,7 @@ "next": "14.0.4", "nodemailer": "^6.9.11", "react": "^18", + "react-datepicker": "^6.9.0", "react-dom": "^18", "react-firebase-hooks": "^5.1.1", "react-material-symbols": "^4.3.1" @@ -30,6 +32,7 @@ "devDependencies": { "@types/node": "^20", "@types/react": "^18", + "@types/react-datepicker": "^6.2.0", "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^6.18.0", "@typescript-eslint/parser": "^6.18.0", @@ -1010,12 +1013,26 @@ "@floating-ui/utils": "^0.2.0" } }, + "node_modules/@floating-ui/react": { + "version": "0.26.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.16.tgz", + "integrity": "sha512-HEf43zxZNAI/E781QIVpYSF3K2VH4TTYZpqecjdsFkjsaU1EbaWcM++kw0HXFffj7gDUcBFevX8s0rQGQpxkow==", + "dependencies": { + "@floating-ui/react-dom": "^2.1.0", + "@floating-ui/utils": "^0.2.0", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", - "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz", + "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==", "dependencies": { - "@floating-ui/dom": "^1.6.1" + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -1829,6 +1846,17 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-datepicker": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz", + "integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==", + "dev": true, + "dependencies": { + "@floating-ui/react": "^0.26.2", + "@types/react": "*", + "date-fns": "^3.3.1" + } + }, "node_modules/@types/react-dom": { "version": "18.2.18", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", @@ -2916,6 +2944,15 @@ "node": ">=0.10" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -6056,6 +6093,22 @@ "node": ">=0.10.0" } }, + "node_modules/react-datepicker": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-6.9.0.tgz", + "integrity": "sha512-QTxuzeem7BUfVFWv+g5WuvzT0c5BPo+XTCNbMTZKSZQLU+cMMwSUHwspaxuIcDlwNcOH0tiJ+bh1fJ2yxOGYWA==", + "dependencies": { + "@floating-ui/react": "^0.26.2", + "clsx": "^2.1.0", + "date-fns": "^3.3.1", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -6094,6 +6147,19 @@ "react-dom": "^18.2.0" } }, + "node_modules/react-onclickoutside": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.1.tgz", + "integrity": "sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -6855,6 +6921,11 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/tailwindcss": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8fcdeea2..f2c0920d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@mui/material": "^5.15.12", "@mui/x-data-grid": "^7.1.1", "@paypal/react-paypal-js": "^8.2.0", + "date-fns": "^3.6.0", "envalid": "^8.0.0", "firebase": "^10.11.0", "html2canvas": "^1.4.1", @@ -29,6 +30,7 @@ "next": "14.0.4", "nodemailer": "^6.9.11", "react": "^18", + "react-datepicker": "^6.9.0", "react-dom": "^18", "react-firebase-hooks": "^5.1.1", "react-material-symbols": "^4.3.1" @@ -36,6 +38,7 @@ "devDependencies": { "@types/node": "^20", "@types/react": "^18", + "@types/react-datepicker": "^6.2.0", "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^6.18.0", "@typescript-eslint/parser": "^6.18.0", diff --git a/frontend/src/api/eventDetails.ts b/frontend/src/api/eventDetails.ts index d354d35c..b2b9dbfd 100644 --- a/frontend/src/api/eventDetails.ts +++ b/frontend/src/api/eventDetails.ts @@ -1,4 +1,4 @@ -import { get, handleAPIError, post, put } from "./requests"; +import { del, get, handleAPIError, post, put } from "./requests"; import type { APIResult } from "./requests"; @@ -8,8 +8,11 @@ export type EventDetails = { description: string; guidelines: string; date: string; + startTime: string; + endTime: string; location: string; imageURI: string; + description_short: string; }; export async function getEventDetails(id: string): Promise> { @@ -32,13 +35,16 @@ export async function getAllEventDetails(): Promise> { } } -type CreateEventDetailsRequest = { +export type CreateEventDetailsRequest = { name: string; description: string; guidelines: string; date: string; + startTime: string; + endTime: string; location: string; imageURI: string; + description_short: string; }; export async function createEventDetails( @@ -49,18 +55,22 @@ export async function createEventDetails( const json = (await response.json()) as EventDetails; return { success: true, data: json }; } catch (error) { + console.log("error: ", error); return handleAPIError(error); } } -type UpdateEventDetailsRequest = { +export type UpdateEventDetailsRequest = { _id: string; name: string; description: string; guidelines: string; date: string; + startTime: string; + endTime: string; location: string; imageURI: string; + description_short: string; }; export async function updateEventDetails( @@ -73,6 +83,17 @@ export async function updateEventDetails( }); const json = (await response.json()) as EventDetails; return { success: true, data: json }; + } catch (error) { + console.log("updateEventDetails error: ", error); + return handleAPIError(error); + } +} + +export async function deleteEventDetails(id: string): Promise> { + try { + const response = await del(`/api/eventDetails/${id}`); + const json = (await response.json()) as EventDetails; + return { success: true, data: json }; } catch (error) { return handleAPIError(error); } diff --git a/frontend/src/api/requests.ts b/frontend/src/api/requests.ts index 247cb256..c4ae06bc 100644 --- a/frontend/src/api/requests.ts +++ b/frontend/src/api/requests.ts @@ -108,6 +108,7 @@ export async function post( headers: Record = {}, ): Promise { const response = await fetchRequest("POST", API_BASE_URL + url, body, headers); + console.log("fetch response: ", response); await assertOk(response); return response; } diff --git a/frontend/src/app/admin/event-creator/page.module.css b/frontend/src/app/admin/event-creator/page.module.css new file mode 100644 index 00000000..a76ae236 --- /dev/null +++ b/frontend/src/app/admin/event-creator/page.module.css @@ -0,0 +1,68 @@ +@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wdth,wght@0,75..100,300..800;1,75..100,300..800&family=Roboto+Slab:wght@100..900&display=swap"); + +.page { + display: flex; + justify-content: flex-start; + padding-left: 282px; + padding-top: 22px; + padding-bottom: 50px; +} + +.Headings { + text-align: left; + font: var(--font-body); + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 133.333% */ + color: white; +} + +.cellentry { + text-align: left; + font: var(--font-body); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 133.333% */ + color: black; +} + +.headingBackground { + background-color: #694c97; /* Replace with your desired color */ +} + +.cellBorderStyle { + border-right: 1px solid #c9c9c9; /* Adjust the border style as needed */ +} + +.selectedRow { + border-radius: 5px; + box-shadow: inset 0 0 0 2px #bda7e0; + box-shadow: inset 0 0.5px 0 2px #bda7e0; +} +.selectedCol { + background: rgba(105, 76, 151, 0.05); +} + +.evenRow { + background-color: #ffffff; /* White color for even rows */ +} + +.oddRow { + background-color: #f8f5fb; /* #F8F5FB color for odd rows */ +} + +.sidebar-container { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 400px; /* Adjust width as needed */ + transition: transform 0.3s ease-out; + transform: translateX(100%); +} + +.sidebar-container.open { + transform: translateX(0%); +} diff --git a/frontend/src/app/admin/event-creator/page.tsx b/frontend/src/app/admin/event-creator/page.tsx new file mode 100644 index 00000000..a6c00664 --- /dev/null +++ b/frontend/src/app/admin/event-creator/page.tsx @@ -0,0 +1,438 @@ +"use client"; +import Box from "@mui/material/Box"; +import { + DataGrid, + GridColDef, + GridEventListener, + GridRowClassNameParams, + GridRowId, +} from "@mui/x-data-grid"; +import Image from "next/image"; +import React, { useEffect, useState } from "react"; + +import styles from "./page.module.css"; + +import { + CreateEventDetailsRequest, + EventDetails, + createEventDetails, + getAllEventDetails, + getEventDetails, + updateEventDetails, +} from "@/api/eventDetails"; +import EventSidebar from "@/components/EventSidebar"; +import PageToggle from "@/components/PageToggle"; + +export default function EventCreator() { + const columns: GridColDef<(typeof rows)[number]>[] = [ + { + field: "name", + headerName: "Event Title", + width: 223, + editable: false, + resizable: false, + headerClassName: `${styles.headingBackground} ${styles.cellBorderStyle} ${styles.Headings}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Event Title
, + }, + { + field: "description_short", + headerName: "Description Short", + width: 223, + editable: false, + resizable: false, + headerClassName: `${styles.Headings} ${styles.headingBackground} ${styles.cellBorderStyle}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Description (short)
, + }, + + { + field: "description", + headerName: "Description Long", + width: 223, + editable: false, + resizable: false, + headerClassName: `${styles.Headings} ${styles.headingBackground} ${styles.cellBorderStyle}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Description (long)
, + }, + { + field: "date", + headerName: "DateTime", + width: 223, + editable: false, + resizable: false, + headerClassName: `${styles.Headings} ${styles.headingBackground} ${styles.cellBorderStyle}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Date/Time
, + renderCell: (params) => { + const { date, startTime, endTime } = params.row; + const formattedDate = new Date(date).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + return `${formattedDate}. ${startTime} - ${endTime}`; + }, + }, + + { + field: "location", + headerName: "Location", + width: 225.4, + editable: false, + resizable: false, + headerClassName: `${styles.Headings} ${styles.headingBackground} ${styles.cellBorderStyle}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Location
, + }, + ]; + + const [rows, setRow] = useState([]); + const [rowsCurrent, setRowsCurrent] = React.useState(rows); + const [currentEvents, setCurrentEvents] = useState([]); + const [pastEvents, setPastEvents] = useState([]); + const [pageToggle, setPageToggle] = useState(0); + const [selectedRow, setSelectedRow] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(Math.ceil(rows.length / 14)); + const [selectedEvent, setSelectedEvent] = useState(null); + const [sidebarOpen, setSidebarOpen] = useState(false); + const [rerenderKey, setRerenderKey] = useState(0); + + useEffect(() => { + getAllEventDetails() + .then((result) => { + if (result.success) { + console.log(result.data); + const now = new Date(); + const utcDateCurrent = now; + + const filteredCurrent = result.data.filter((item) => { + const dateObj = new Date(item.date); + + const utcDateitem = dateObj.getTime(); + + if (utcDateitem >= utcDateCurrent.getTime()) { + return true; + } + return false; + }); + + const formattedCurrentRows = filteredCurrent.map((item) => ({ + ...item, + id: item._id.toString(), + })); + + console.log("current: ", formattedCurrentRows); + setCurrentEvents(formattedCurrentRows); + + const filteredPast = result.data.filter((item) => { + const dateObj = new Date(item.date); + const utcDateitem = dateObj.getTime(); + + if (utcDateitem < utcDateCurrent.getTime()) { + return true; + } + return false; + }); + + const formattedPastRows = filteredPast.map((item) => ({ + ...item, + id: item._id.toString(), + })); + + console.log("past: ", formattedPastRows); + setPastEvents(formattedPastRows); + + setRow(formattedCurrentRows); + setRowsCurrent(formattedCurrentRows); + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + }, []); + + useEffect(() => { + if (selectedRow) { + getEventDetails(selectedRow?.toString()) + .then((result) => { + if (result.success) { + setSelectedEvent(result.data); + setRerenderKey((prevKey) => prevKey + 1); + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + } else { + setSelectedEvent(null); + } + }, [selectedRow]); + + useEffect(() => { + if (sidebarOpen) { + setRerenderKey((prevKey) => prevKey + 1); + } + }, [sidebarOpen]); + + const handleTogglePage = (index: number) => { + if (index === 0) { + setCurrentPage(1); + setRowsCurrent(currentEvents); + setTotalPages(Math.ceil(currentEvents.length / 14)); + } else if (index === 1) { + setCurrentPage(1); + setRowsCurrent(pastEvents); + setTotalPages(Math.ceil(pastEvents.length / 14)); + } + setPageToggle(index); + }; + + const openEvent = (createNew: boolean) => { + if (createNew) { + setSelectedRow(null); + } + setSidebarOpen(true); + }; + + useEffect(() => { + // Update total pages when rows change + setTotalPages(Math.ceil(rows.length / 14)); + console.log("rows.length: ", rows.length); + }, [rows]); + + const handleCellClick: GridEventListener<"rowClick"> = (params) => { + if (!sidebarOpen) { + setSelectedRow(params.id === selectedRow ? null : params.id); + openEvent(false); + } + }; + + const handleSetSidebarOpen = (open: boolean) => { + if (!open) { + setSelectedRow(null); + } + setSidebarOpen(open); + }; + const handleUpdateEvent = async (eventData: EventDetails) => { + const result = await updateEventDetails(eventData); + if (!result.success) { + console.log("result was not a success"); + alert(result.error); + console.error("ERROR:", result.error); + } + }; + + const handleCreateEvent = async (eventData: CreateEventDetailsRequest) => { + const result = await createEventDetails(eventData); + if (!result.success) { + console.error("ERROR:", result.error); + alert(result.error); + } + }; + + const handlePreviousPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + + const handleNextPage = () => { + if (currentPage < totalPages) { + setCurrentPage(currentPage + 1); + } + }; + + const getRowClassName = (params: GridRowClassNameParams) => { + let rowClasses = ""; + + // Add alternating row colors + rowClasses += params.indexRelativeToCurrentPage % 2 === 0 ? styles.evenRow : styles.oddRow; + + // Add border to the selected row + if (selectedRow === params.id) { + rowClasses += ` ${styles.selectedRow}`; + } + return rowClasses; + }; + + return ( +
+ + {sidebarOpen && ( +
+ +
+ )} + + + + + + + + { + + } + + + + + +
+ Previous page + Page +
+ {currentPage} +
+ of + {totalPages} + Next page +
+
+
+
+ ); +} diff --git a/frontend/src/app/admin/newsletter-creator/page.tsx b/frontend/src/app/admin/newsletter-creator/page.tsx index 74a938e4..9eaa295d 100644 --- a/frontend/src/app/admin/newsletter-creator/page.tsx +++ b/frontend/src/app/admin/newsletter-creator/page.tsx @@ -141,9 +141,13 @@ export default function NewsletterCreator() { const handleTogglePage = (index: number) => { if (index === 0) { + setCurrentPage(1); setRowsCurrent(currentNewsletters); + setTotalPages(Math.ceil(currentNewsletters.length / 14)); } else if (index === 1) { + setCurrentPage(1); setRowsCurrent(archiveNewsletters); + setTotalPages(Math.ceil(archiveNewsletters.length / 14)); } setPageToggle(index); }; @@ -170,33 +174,21 @@ export default function NewsletterCreator() { const handleSetSidebarOpen = (open: boolean) => { setSidebarOpen(open); }; - const handleUpdateNewsletter = (newsletterData: Newsletter) => { - updateNewsletter(newsletterData) - .then((result) => { - if (result.success) { - // TODO: add success message, update table - } else { - console.error("ERROR:", result.error); - } - }) - .catch((error) => { - alert(error); - }); + const handleUpdateNewsletter = async (newsletterData: Newsletter) => { + const result = await updateNewsletter(newsletterData); + if (!result.success) { + console.log("result was not a success"); + alert(result.error); + console.error("ERROR:", result.error); + } }; - const handleCreateNewsletter = (newsletterData: CreateNewsletterRequest) => { - console.log(newsletterData); - createNewsletter(newsletterData) - .then((result) => { - if (result.success) { - //TODO: add success message, update table - } else { - console.error("ERROR:", result.error); - } - }) - .catch((error) => { - alert(error); - }); + const handleCreateNewsletter = async (newsletterData: CreateNewsletterRequest) => { + const result = await createNewsletter(newsletterData); + if (!result.success) { + console.error("ERROR:", result.error); + alert(result.error); + } }; const handlePreviousPage = () => { diff --git a/frontend/src/components/CharacterCount.module.css b/frontend/src/components/CharacterCount.module.css new file mode 100644 index 00000000..41c85fe4 --- /dev/null +++ b/frontend/src/components/CharacterCount.module.css @@ -0,0 +1,9 @@ +.wrapper { + color: #484848; + font-family: --var(--font-body); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 150%; /* 15px */ + letter-spacing: 0.2px; +} diff --git a/frontend/src/components/CharacterCount.tsx b/frontend/src/components/CharacterCount.tsx new file mode 100644 index 00000000..3e953bf1 --- /dev/null +++ b/frontend/src/components/CharacterCount.tsx @@ -0,0 +1,23 @@ +import styles from "./CharacterCount.module.css"; + +export type CharacterCountProps = { + currCount: number; + maxCount: number; + error: boolean; +}; + +export const CharacterCount = ({ currCount, maxCount, error }: CharacterCountProps) => { + return ( +
+ {error ? ( +

+ {currCount}/{maxCount} characters +

+ ) : ( +

+ {currCount}/{maxCount} characters +

+ )} +
+ ); +}; diff --git a/frontend/src/components/EventSidebar.module.css b/frontend/src/components/EventSidebar.module.css new file mode 100644 index 00000000..8ecee7c3 --- /dev/null +++ b/frontend/src/components/EventSidebar.module.css @@ -0,0 +1,207 @@ +.sidebar { + position: fixed; + top: 0; + bottom: 0; + right: 0; + overflow-y: scroll; + overflow-x: hidden; + overscroll-behavior: contain; + width: 38vw; + z-index: 3; + border-left: 1px solid #c9c9c9; + background: #fff; +} + +.fixedPosition { + position: fixed; + top: 0; + bottom: 0; + right: 0; + width: 38vw; +} + +.grayOut { + opacity: 0.3; + background: #484848; + position: fixed; + top: 0; + bottom: 0; + right: 0; + width: 38vw; +} + +.closeWindow { + display: flex; + width: 524px; + height: 41px; + padding: 10px 20px; + align-items: center; + gap: 8px; + flex-shrink: 0; + color: var(--Neutral-Gray4, #909090); + border-bottom: 1px solid #c9c9c9; + font: var(--font-body); + font-size: 14px; +} + +.closeWindow:hover { + cursor: pointer; +} + +.sidebarContents { + padding: 60px; + padding-left: 35px; +} + +.sidebarContents h1 { + font: var(--font-small-subtitle); + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; /* 27px */ + letter-spacing: 0.36px; +} + +.sidebarContents h2 { + padding-top: 24px; + font: var(--font-body); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + color: #909090; +} + +.sidebarContents { + font: var(--font-body); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 150% */ +} + +.header { + display: flex; + direction: row; + align-items: center; + justify-content: space-between; + height: 46px; +} + +.sidebar button { + display: flex; + padding: 4px 16px; + justify-content: center; + align-items: center; + gap: 6px; + border-radius: 4px; +} + +.sidebar button img { + padding: 4px; +} + +.sidebar button p { + font: var(--text-body); + font-size: 20px; + font-weight: 700; + line-height: 150%; /* 30px */ + letter-spacing: 0.7px; +} + +.editButton { + background: #694c97; + color: #fff; +} + +.cancelButton { + font: var(--font-body); + background: #fff; + border: 1px solid #694c97; + color: #694c97; +} + +.saveButton { + font: var(--font-body); + background: #694c97; + color: #fff; +} + +.deleteButton { + background: #fff; + border: 1px solid #b93b3b; + color: #b93b3b; +} + +.bottomButtons { + display: flex; + direction: row; + gap: 24px; + justify-content: flex-end; + margin-right: 60px; + margin-bottom: 77px; +} + +.deleteButtonWrapper { + display: flex; + direction: row; + justify-content: center; + margin-top: 50px; +} + +.contentPar { + margin-bottom: 1rem; +} + +.sidebar .alert { + position: relative; + top: 35px; +} + +.sidebar .alert .div { + position: absolute; +} + +.sidebar .alert img { + padding: 0; +} + +.textField { + width: 454px; +} + +.textFieldSmall { + width: 240px; +} + +.textFieldSmallest { + width: 100px; +} + +.textArea { + width: 454px; + height: 80px; + padding: 11px; + border: 1px solid #d8d8d8; + border-radius: 4px; + font-size: 14px; +} + +.textAreaLong { + width: 454px; + height: 120px; + padding: 10px; + border: 1px solid #d8d8d8; + border-radius: 4px; + font-size: 14px; +} + +.textAreaContent { + width: 100%; + white-space: pre-wrap; + font: var(--font-body); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; +} diff --git a/frontend/src/components/EventSidebar.tsx b/frontend/src/components/EventSidebar.tsx new file mode 100644 index 00000000..08913760 --- /dev/null +++ b/frontend/src/components/EventSidebar.tsx @@ -0,0 +1,501 @@ +"use client"; +import Image from "next/image"; +import React, { useState } from "react"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; + +import { CreateEventDetailsRequest, EventDetails, deleteEventDetails } from "../api/eventDetails"; + +import styles from "./EventSidebar.module.css"; +import { TextAreaCharLimit } from "./TextAreaCharLimit"; +import { TextFieldCharLimit } from "./TextFieldCharLimit"; + +import AlertBanner from "@/components/AlertBanner"; +import { TextField } from "@/components/TextField"; +import { WarningModule } from "@/components/WarningModule"; + +type eventSidebarProps = { + eventDetails: null | EventDetails; + setSidebarOpen: (open: boolean) => void; + updateEvent: (eventData: EventDetails) => Promise; + createEvent: (eventData: CreateEventDetailsRequest) => Promise; +}; + +type formErrors = { + name?: boolean; + description?: boolean; + description_short?: boolean; + guidelines?: boolean; + date?: boolean; + startTime?: boolean; + endTime?: boolean; + location?: boolean; +}; + +const EventSidebar = ({ + eventDetails, + setSidebarOpen, + updateEvent, + createEvent, +}: eventSidebarProps) => { + const [name, setName] = useState(eventDetails ? eventDetails.name : ""); + const [description, setDescription] = useState(eventDetails ? eventDetails.description : ""); + const [description_short, setDescription_short] = useState( + eventDetails ? eventDetails.description_short : "", + ); + const [date, setDate] = useState(eventDetails ? new Date(eventDetails.date) : new Date()); + const [startTime, setStartTime] = useState(eventDetails ? eventDetails.startTime : ""); + const [endTime, setEndTime] = useState(eventDetails ? eventDetails.endTime : ""); + + const [location, setLocation] = useState(eventDetails ? eventDetails.location : ""); + const [guidelines, setGuidelines] = useState(eventDetails ? eventDetails.guidelines : ""); + const [isEditing, setIsEditing] = useState(!eventDetails); + const [isDeleting, setIsDeleting] = useState(false); + const [errors, setErrors] = useState({}); + const [warningOpen, setWarningOpen] = useState(false); + const [showAlert, setShowAlert] = useState(false); + + const confirmCancel = () => { + setName(eventDetails ? eventDetails.name : ""); + setDescription(eventDetails ? eventDetails.description : ""); + setDescription_short(eventDetails ? eventDetails.description_short : ""); + setDate(eventDetails ? new Date(eventDetails.date) : new Date()); + setStartTime(eventDetails ? eventDetails.startTime : ""); + setEndTime(eventDetails ? eventDetails.endTime : ""); + setLocation(eventDetails ? eventDetails.location : ""); + setGuidelines(eventDetails ? eventDetails.guidelines : ""); + setIsEditing(false); + setIsDeleting(false); + setErrors({}); + setWarningOpen(false); + setSidebarOpen(false); + }; + + const handleCancel = () => { + const defaultDate = new Date(); + if ( + name !== (eventDetails ? eventDetails.name : "") || + description !== (eventDetails ? eventDetails.description : "") || + description_short !== (eventDetails ? eventDetails.description_short : "") || + date !== (eventDetails ? new Date(eventDetails.date) : defaultDate) || + startTime !== (eventDetails ? eventDetails.startTime : "") || + endTime !== (eventDetails ? eventDetails.endTime : "") || + location !== (eventDetails ? eventDetails.location : "") || + guidelines !== (eventDetails ? eventDetails.guidelines : "") + ) { + setWarningOpen(true); + } else { + confirmCancel(); + } + }; + + const handleCloseSidebar = () => { + const defaultDate = new Date(); + if ( + name !== (eventDetails ? eventDetails.name : "") || + description !== (eventDetails ? eventDetails.description : "") || + description_short !== (eventDetails ? eventDetails.description_short : "") || + date !== (eventDetails ? new Date(eventDetails.date) : defaultDate) || + startTime !== (eventDetails ? eventDetails.startTime : "") || + endTime !== (eventDetails ? eventDetails.endTime : "") || + location !== (eventDetails ? eventDetails.location : "") || + guidelines !== (eventDetails ? eventDetails.guidelines : "") + ) { + setWarningOpen(true); + } else { + confirmCancel(); + setSidebarOpen(false); + } + }; + + const handleSave = async () => { + setWarningOpen(false); + console.log("handleSave"); + + if ( + name === "" || + description === "" || + description_short === "" || + !date || + startTime === "" || + endTime === "" || + location === "" || + guidelines === "" + ) { + setErrors({ + name: name === "", + description: description === "", + description_short: description_short === "", + date: !date, + startTime: startTime === "", + endTime: endTime === "", + location: location === "", + guidelines: guidelines === "", + }); + } else { + setIsEditing(false); + if (eventDetails) { + console.log("eventDetails exist"); + await updateEvent({ + _id: eventDetails._id, + name, + description, + guidelines, + date: date.toISOString(), + startTime, + endTime, + location, + imageURI: eventDetails.imageURI, + description_short, + }); + console.log("after updating event"); + } else { + await createEvent({ + name, + description, + guidelines, + date: date.toISOString(), + startTime, + endTime, + location, + imageURI: + "https://images.unsplash.com/photo-1559027615-cd4628902d4a?q=80&w=2674&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + description_short, + }); + console.log("after creating event"); + } + + setIsEditing(false); + setErrors({}); + setShowAlert(true); + window.location.reload(); + console.log("last line in save"); + } + }; + + const handleDelete = () => { + setIsDeleting(true); + }; + + const confirmDelete = () => { + if (eventDetails) { + deleteEventDetails(eventDetails._id) + .then((result) => { + if (result.success) { + window.location.reload(); + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + setSidebarOpen(false); + } + }; + + const alertContent = { + text: "Event Saved!", + }; + + const handleCloseAlert = () => { + setShowAlert(false); + }; + + if (isDeleting) { + return ( +
+
+ {showAlert && } +
+
{ + setSidebarOpen(false); + }} + > + test +

Close Window

+
+
+
+

Event Details

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

Event Title

+

{name}

+

Event Description (short)

+
{description_short}
+

Event Description (long)

+
{description}
+

Date & Time

+

{`${date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}, ${startTime} - ${endTime}`}

+

Location

+

{location}

+

Guidelines

+
{guidelines}
+

Image

+

Placeholder - to be replaced with image

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

Close Window

+
+
+
+

Event Details

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

Event Description (short)

+ { + setDescription_short(event.target.value); + }} + maxCount={200} + /> +

Event Description (long)

+ { + setDescription(event.target.value); + }} + maxCount={275} + /> +
+ { + setDate(dateObj); + }} + dateFormat="MMMM d, yyyy" + customInput={ + + } + /> + {errors.date &&

Date is required

} +
+
+
+ ) => { + setStartTime(event.target.value); + }} + /> +
+

+ to +

+
+ ) => { + setEndTime(event.target.value); + }} + /> +
+
+ ) => { + setLocation(event.target.value); + }} + error={errors.location} + /> +

Guidelines

+