As the title would suggest, this is an all in one event management system for a university. Right now its meant to serve the GUC but with very minimal changes it can serve any university or entity. Event managers (hereafter referred to as the Events Office) can post and manage events, while users can view details of and register for events on the system. Users can even pay for events they registered for using Stripe or the platform's wallet for convenience.
While this project was ultimately created as the main requirement of the ACL course at the GUC and served as very powerful practical learning experience of the MongoDB, Express, React and Node stack (hereafter referred to as MERN); it actually is a viable project that we can see being very useful in the real world.
On the more technical side, now that we are finalizing sprint 2, all features mandated by the course are now up and running (yes this implies our own features are in the pipeline - stay tuned!). Thankfully we have no system breaking bugs (none that we found during many rounds of testing anyway), however there are some quirks that should ideally be adressed in some future version. Namely some user actions require page refresh to take effect visually even though they take logical effect immediately. Also due to time limitations, unfortunately role based access control is enforced only on the frontend; ie someone who knows the hosting address of the backend may identify the endpoints and act maliciously. This is on our radar and the groundwork for its resolution is already in place.
On a lighter note, at the start of sprint 1 we coordinated our code style to be consistent so it would be easier for us to debug and maintain each other’s implementations. Unfortunately however old habits die hard and we often default to our own styles after a while. That being said, in general any camel cased identifier is a function name while variables generally follow a lowercase style with underscores separating individual words.
Below are some sample screenshots from our project UI:
As mentioned before, the MERN stack, a JavaScript technology suite powers this project.
-
MongoDB A NoSQL, document-oriented database used to store application data in a JSON-like format.
-
Express.js The backend framework running on Node.js, responsible for handling server-side logic, routing, and APIs.
-
React A frontend JavaScript library used to build fast, interactive user interfaces with a great focus in reusability due to being component definition based.
-
Node.js The JavaScript runtime environment that powers the backend and allows JavaScript to run on the server.
These work hand in hand to provide a seamless full-stack development experience using javascript as the only programming language across the entire application.
Below is a comprehensive list of currently implemented system features:
-
Students/Staff/TAs/Professors can sign up with my email, password, first name, last name and student/staff ID (Everyone should use their GUC email while signing up. Admins and Events Office do not sign up.)
-
Vendors can sign up with an email, password, and company name
-
Vendors can upload tax card and logo to prove company validity
-
Students can receive a verification email after registration (The email contains a verification link that redirects to login. If not clicked, login should fail.)
-
Admins can insert the correct role for Staff/TAs/Professors and send a verification email after approving their registration
-
Staff/TAs/Professors can receive a verification email after admin verification (The link redirects to login. If not clicked, login should fail.)
-
Admins can create other Admin/Events Office accounts with name, email, and password
-
Admins can delete other Admin/Events Office accounts
-
Students/Staff/Events Office/TAs/Professors/Admins can log in with my email and password (Everyone uses GUC email except companies and vendors.)
-
Students/Staff/Events Office/TAs/Professors/Admins can log out successfully
-
Students/Staff/Events Office/TAs/Professors/Admins can view all available events and their details including participating vendors (Events include workshops, trips, bazaars, booths, and conferences.)
-
Students/Staff/Events Office/TAs/Professors/Admins can search events by name or type (Professor name / Event name)
-
Students/Staff/Events Office/TAs/Professors/Admins can filter events by name, location, type, or date (Professor name in case of workshop or conference)
-
Students/Staff/Events Office/TAs/Professors/Admins can sort events by date
-
Students/Staff/TAs/Professors can rate an event that I attended
-
Students/Staff/TAs/Professors can comment on an event that I attended
-
Students/Staff/Events Office/TAs/Professors/Admins can view all ratings and comments on any event
-
Admins can delete any inappropriate comments
-
Admins can block any user
-
Admins can view a list of all users along with their details and status (active/blocked)
-
Students/Staff/Events Office/TAs/Professors can receive a warning email if my comment was deleted for being inappropriate
-
Students/Staff/TAs/Professors can add an event to my favorites list
-
Students/Staff/TAs/Professors can view my favorites list
-
Students/Staff/TAs/Professors can register for a workshop/trip using name, email, and student/staff ID
-
Students/Staff/TAs/Professors can pay for a workshop/trip using credit/debit card (Stripe) or wallet
-
Students/Staff/TAs/Professors can receive a payment receipt via email
-
Students/Staff/TAs/Professors can view a list of my registered events (upcoming and past)
-
Students/Staff/TAs/Professors can cancel registration and receive refund to wallet (Only if at least 2 weeks before the event)
-
Students/Staff/TAs/Professors can view refunded amount in my wallet
-
Students/Staff/TAs/Professors can receive a certificate of attendance by email after finishing a workshop
-
Events Office can create bazaars by adding name, dates, time, location, description, and registration deadline
-
Events Office can edit bazaar details (Only if the bazaar hasn’t started yet)
-
Events Office can create trips by adding name, location, price, dates, description, capacity, and deadline
-
Events Office can edit trip details (Only if the trip start date hasn’t passed)
-
Professors can create workshops with all required academic and logistical details
-
Professors can edit workshop details
-
Professors can view a list of all workshops that I created
-
Professors can view participants and remaining spots for my workshops
-
Events Office can receive system notifications when professors submit workshop requests
-
Events Office can accept and publish workshops
-
Events Office can reject workshops
-
Events Office can request edits for workshop details
-
Professors can view workshop status and requested edits
-
Professors can receive system notifications if my workshop was accepted/rejected
-
Events Office can create conferences with full details and website link (Website holds most information)
-
Events Office can edit conference details
-
Events Office can archive past events
-
Events Office/Admins can delete events with no registrations
-
Events Office can export registrant names to .xlsx (Except conferences)
-
Events Office can restrict events to specific user types
-
Events Office can generate QR codes for external visitors
-
Events Office/Admins can view total attendee reports
-
Events Office/Admins can filter attendee reports by name, type, or date
-
Events Office/Admins can view sales and revenue reports
-
Events Office/Admins can filter sales reports by type and/or date
-
Events Office/Admins can sort sales reports by revenue
-
Students/Staff/Events Office/TAs/Professors can receive notifications for new events
-
Students/Staff/Events Office/TAs/Professors can receive reminders 1 day and 1 hour before registered events
-
Vendors can view a list of upcoming bazaars
-
Vendors can apply to join a bazaar with attendee names/emails and booth size (2x2 or 4x4; max 5 people)
-
Vendors can apply for a platform booth with attendee info, duration, location, and size (Duration 1–4 weeks; size 2x2 or 4x4)
-
Vendors can upload IDs of attending individuals
-
Vendors can receive acceptance/rejection email for applications
-
Vendors can pay participation fees (Deadline is 3 days after acceptance email)
-
Vendors can receive a payment receipt via email
-
Vendors can receive QR codes for all registered visitors by email
-
Vendors can cancel participation request (Only if not paid yet)
-
Vendors can view accepted upcoming bazaars/booths
-
Vendors can view pending or rejected requests
-
Vendors can apply to the GUC loyalty program (Includes discount rate, promo code, and terms)
-
Vendors can cancel participation in loyalty program
-
Students/Staff/TAs/Professors/Events Office/Admins can view loyalty program vendors with discounts and terms
-
Students/Staff/TAs/Professors can receive notifications for new loyalty partners
-
Events Office/Admins can receive notifications for pending vendor requests
-
Events Office/Admins can view vendor participation request details
-
Events Office/Admins can view/download uploaded documents
-
Events Office/Admins can accept or reject vendor requests
-
Students can view courts and availability
-
Students can reserve courts (Reservation auto-includes name and GUC ID)
-
Students/Staff/Events Office/TAs/Professors can view monthly gym schedules
-
Students/Staff/TAs/Professors can register for gym sessions
-
Events Office can create vendor polls for platform booths
-
Students/Staff/TAs/Professors can vote in vendor polls
-
Events Office can create gym sessions (date, time, duration, type, max participants)
-
Events Office can cancel gym sessions
-
Events Office can edit gym session (only date, time, and duration)
-
Students/Staff/TAs/Professors can receive email if a gym session I registered for was cancelled or edited
This codebase is large so we thought we would provide some varried code examples of functions that we are particularly proud of:
Personal favorite of @DeveloSaurus
export async function refundEventPayment(req, res, next) {
const {
email,
event_type,
event_name,
event_id,
registration_price,
start_date,
} = req.body;
const refund_deadline = new Date(start_date);
const now = new Date();
const millis_to_days = 86400000;
if ((refund_deadline - now) / millis_to_days >= 14) {
const transaction = await Transaction.create({
owner: email,
amount: registration_price,
method: "Wallet",
transaction_type: "Event Registration Refund",
event_type: event_type,
event_name: event_name,
event_id: event_id,
pending: false,
});
await Wallet.findOneAndUpdate(
{ owner: email },
{
$inc: { balance: registration_price },
$push: { transaction_history: transaction },
}
);
if (event_type === "Trip") {
await Trip.findByIdAndUpdate(event_id, {
$pull: { confirmed_participants: { email: email } },
});
} else {
await workShop.findByIdAndUpdate(event_id, {
$pull: { confirmed_participants: { email: email } },
});
}
return res
.status(200)
.json({ message: "refund and unregistration successful" });
}
return res.status(409).json({ message: "refund deadline has passed" });
}Personal favorite of @EchoKnights
export async function UploadCompanyVerificationDocs(req, res, next) {
try {
const vendorId = req.params.vendorId;
if (!vendorId) {
return res.status(400).json({ message: "vendorId is required" });
}
const taxCardFile = req.files?.tax_card?.[0] || null;
const logoFile = req.files?.logo?.[0] || null;
let company = await Company.findOne({ vendor_id: vendorId });
if (!company) {
if (!req.body?.name) {
return res.status(400).json({ message: "Company name is required" });
}
company = await Company.create({
vendor_id: vendorId,
name: req.body.name,
});
}
const update = {};
if (req.body?.name) update.name = req.body.name;
const metaBase = {
ownerId: vendorId,
relatedEntityType: "Company",
relatedEntityId: company._id,
};
const [taxDoc, logoDoc] = await Promise.all([
persistCompanyDoc(taxCardFile, {
...metaBase,
category: "Company Tax Card",
}),
persistCompanyDoc(logoFile, { ...metaBase, category: "Company Logo" }),
]);
if (taxDoc) update.tax_card_file = taxDoc._id;
if (logoDoc) update.logo_file = logoDoc._id;
if (Object.keys(update).length > 0) {
company = await Company.findByIdAndUpdate(company._id, update, {
new: true,
runValidators: true,
});
}
const populated = await Company.findById(company._id)
.populate(["tax_card_file", "logo_file"])
.lean();
return res.status(200).json({
company: populated,
documents: {
tax_card: formatFileResponse(populated.tax_card_file),
logo: formatFileResponse(populated.logo_file),
},
});
} catch (error) {
next(error);
}
}Personal favorite of @AhmedShady911
export async function deleteComment(req, res, next) {
try {
const { event_id, type, comment_id, email } = req.body;
// Pick the correct model
let Model;
switch (type) {
case "Bazaar":
Model = Bazaar;
break;
case "Conference":
Model = conference;
break;
case "Trip":
Model = Trip;
break;
case "Workshop":
Model = workShop;
break;
case "Gym Session":
Model = GymSession;
break;
case "Platform Booth":
Model = PlatformApplication;
break;
default:
return res.status(400).json({ message: "Invalid event type" });
}
// Fetch the event
const event = await Model.findById(event_id);
if (!event) {
return res.status(404).json({ message: "Event not found" });
}
// Check if the comment exists
const commentExists = event.comments?.some(
(c) => c._id.toString() === comment_id
);
if (!commentExists) {
return res.status(404).json({ message: "Comment not found in event" });
}
// Remove the comment
await Model.findByIdAndUpdate(
event_id,
{ $pull: { comments: { _id: comment_id } } },
{ new: true }
);
await sendDeleteCommentEmail(email);
return res.status(200).json({ message: "Comment removed successfully" });
} catch (err) {
next(err);
}
}Personal favorite of @MostafaYakoutt
export async function bookCourt(req, res, next) {
try {
const courtId = req.params.id;
// prefer authenticated user info, fallback to body
const regId = req.user?.reg_id || req.body?.reg_id;
const firstName = req.user?.first_name || req.body?.first_name || "";
const lastName = req.user?.last_name || req.body?.last_name || "";
const name = `${firstName} ${lastName}`.trim();
if (!courtId)
return res.status(400).json({ error: "Court id is required" });
if (!regId)
return res
.status(400)
.json({ error: "User GUC id (reg_id) is required to book a court" });
// check existing court
const court = await Court.findById(courtId);
if (!court) return res.status(404).json({ error: "Court not found" });
// if already booked by someone else, prevent double-booking
// support legacy string value or new object format
// determine if court is currently booked
const bookedExists = (() => {
if (!court.booked) return false; // null/undefined/empty string are falsy
if (typeof court.booked === "object") return Boolean(court.booked.reg_id);
if (typeof court.booked === "string") return court.booked.trim() !== "";
return true;
})();
if (bookedExists) {
// if booked using new object format and has reg_id, compare
if (typeof court.booked === "object" && court.booked.reg_id) {
if (String(court.booked.reg_id) !== String(regId)) {
console.warn(
`Book attempt by reg_id=${regId} failed; already booked by reg_id=${court.booked.reg_id}`
);
return res
.status(409)
.json({ error: "Court already booked", booked: court.booked });
}
} else {
// legacy non-empty string value -> treat as booked
console.warn(
`Book attempt by reg_id=${regId} failed; already booked (legacy) by=${court.booked}`
);
return res
.status(409)
.json({ error: "Court already booked", booked: court.booked });
}
}
// set booked to the user's name and reg_id
const bookObj = { name, reg_id: regId, bookedAt: new Date() };
const updated = await Court.findByIdAndUpdate(
courtId,
{ booked: bookObj },
{ new: true }
);
return res
.status(200)
.json({ message: "Court booked successfully", court: updated });
} catch (err) {
next(err);
}
}Personal favorite of @omarhazem24
// Generate a QR code PNG for an event registration URL (for external visitors)
export async function generateEventQr(req, res, next) {
try {
const { type, id } = req.query || {};
if (!type) return res.status(400).json({ error: "type is required" });
//placeholder URL that the QR will encode
const url = "https://www.guc.edu.eg/";
const dataUrl = await QRCode.toDataURL(url, {
errorCorrectionLevel: "H",
type: "image/png",
margin: 2,
});
// dataUrl is like 'data:image/png;base64,...'
const base64 = dataUrl.split(",")[1];
const imgBuf = Buffer.from(base64, "base64");
res.setHeader("Content-Type", "image/png");
res.setHeader(
"Content-Disposition",
`inline; filename="qr_${type}_${id || "event"}.png"`
);
return res.send(imgBuf);
} catch (err) {
console.error("Generate QR error:", err);
return res
.status(500)
.json({ error: err.message || "Failed to generate QR code" });
}
}One of the best things about using node for this project is that installing all necessary dependencies and software is made trivial. Let's walk through it. Once you install node on your system, just clone this repository into the IDE of your choice and open up two terminals and navigate to the server directory in one terminal, and the client directory in the other. Now simply type and enter npm install into each terminal and this will install all necessary dependencies needed to run the project. Now you will also need to create 2 .env files to house all the important information that you do not want outsiders to access, this will be things such as your database connection URI and communication email app password. The first .env which should be placed in the server directory should be as follows:
MONGO_URI =
JWT_SECRET =
EMAIL =
EMAIL_PASSWORD =
VERIFYING_EMAIL =
VERIFYING_EMAIL_PASSWORD =
OFFICIAL_EMAIL =
OFFICIAL_EMAIL_PASSWORD =
STRIPE_SECRET_KEY =
STRIPE_WEBHOOK_SECRET =
the second .env which should be placed in the client directory is much simpler and should be as follows:
VITE_STRIPE_PUBLISHED_KEY =
Please note that the application can run without stripe functionality if the related variables are not set. To obtain the stripe api keys you need to sign up for a stripe account and copy them from the dashboard. The stripe webhook secret however is obtained in the terminal upon your first run.
Finally to run the project, in the server terminal type and enter npm run dev:hook to include a background command necessary for the stripe webhooks to function, or simply npm run dev if not using stripe. In the client terminal run npm run dev. Your application is now up and running! To shut it down simply type ctrl+c in both terminals.
Below are some example routes for our backend API endpoints:
-
app.post("/verify-account", sendEmail); -
app.get("/admin/getAllUsers", getAllUsers); -
app.get("/reports/sales", salesReport); -
app.get("/reports/qr", generateEventQr); -
app.post("/updateWorkShop/:id", updateWorkShop);
We also opted for dedicated routers to handle endpoints for specific controllers so here are some as well:
-
app.use("/user-payments", userPaymentRouter); -
app.use("/trips", tripRouter); -
app.use("/user", userRouter);
Below are some screenshots from postman taken during our testing:
We are open to suggestions and new feature ideas, we would love to expand this project to work for an even wider range of entities that would like to streamline event management. Also while we are very happy with how the application looks right now, we are by no means graphic designers or UI experts and would welcome any and all feedback with open arms in this department.
During the development of this project we have consulted more sources than we care to count, but here are some of the more memorable ones:
Please refer to the Apache 2.0 License here:
Sincerely, The Five-Guys Team










