diff --git a/backend/.env.me b/backend/.env.me new file mode 100644 index 00000000..b3145b0d --- /dev/null +++ b/backend/.env.me @@ -0,0 +1,6 @@ +#/!!!!!!!!!!!!!!!!!!!!.env.me!!!!!!!!!!!!!!!!!!!!!!!/ +#/ credential file. DO NOT commit to source control / +#/ [how it works](https://dotenv.org/env-me) / +#/--------------------------------------------------/ + +DOTENV_ME="me_2876f4651cca3df3e9bad1256d77c64b7fe75eb300047597538726fbdc0680e8" diff --git a/backend/.env.vault b/backend/.env.vault new file mode 100644 index 00000000..02ff42ac --- /dev/null +++ b/backend/.env.vault @@ -0,0 +1,25 @@ +#/-------------------.env.vault---------------------/ +#/ cloud-agnostic vaulting standard / +#/ [how it works](https://dotenv.org/env-vault) / +#/--------------------------------------------------/ + +# development +DOTENV_VAULT_DEVELOPMENT="CMIx45zMeUvOZgr36iRsrH4Du2UirHqfgfbvaZhn5KB9qr/CxlXwqnbJttXmFNye3XwFcZyoGKG6I6lfjBbzKNlNFGfzCu1jJ+y44iIbZqcZswwyhgYP5BvAir9+a1pTwtmIp2suZ9ofSs9nTwRmTpi4/75bwNG9wBm/5xGY++fJ5qacM+zOEAnvfeb6Wl/b3Adq7sBTjtiXTK4ByMmLFeOwm6nE7InNG665EBmdQE+UVd7GXpxCTsGZrWDxyLfUvbL15iF03vL3GkG2I+C+SkCOuGGkBN8Rv5RnNFXi9yUeG2eNCRYXGTEKyXYEC4LGT8Infq7RRAWvsJxe59aDPHdGyiHHa21EDMi63sEHTSdrXiqq9PwMu0SUg/NNx9xaANBWtvKMLAW0zmclq7lua3ewUHTQQoe8fDlWmviVCfPumG5K+QnwJQP9i8c77kdKZQHhkKG3IfAO06s8cMOFJ+25Md5JbgFXiz05GOdxRW4AFEHATG+yEIfe0rWWaNlGAXr4Ws4em2NOML6ULthGEbaOZt/w6JxNsrQkZYrqFu/LxZdp/+QlcXeYbcT4TVs0lK0B26H4O/Lly3GN3luvnM6/c8IXyhoz0Ytp5zqQISo/RK//JXA+VrM9y6fa536y4nKHboGOMr83NR7wz7WFWBsjuFD66Zob23S0AmRQLXr9/rnZYYRpeMo9yOtluWiVAP6bh91OGziUHHh+03MWOP/rgz7CVT55jDQbAL1wMkJxfZnlx2uu9GQXHo8GNeK/7uN0V693TGU9emu3JDpwszyjwvNd7ujP4VzP6xSn0dHix1Pox7GZlzE6C0Gxzcj/i8VGIg7LBo8vXZGTodRT6eRITrqmvG/bZ5ztlDbAwvv3BG/HFbEZijrlvPTOpFaeZC1SWYnYTPKcrV1O71zBdGP/xXrluCdYKv/GRPGDJnwLVLJ+iESS5xtm9Eh+++7F1BSgdIf0pmFQcUeZiN6lPwq17jTCQ/1d7ENZVyeEmYEDtu5cwm9ktESc1V0vef9nUVwg7XFoz1nLiz8+VWbtG+W+h2h6tuw472oUAXgpYkO1WnfMA4TKDyIgYzaje3R68PGwkiAIWFkubHZU4NqOzvNE2BnF6pLB4aF9hdLnDWze0PCXkGHXuJJrUeJCHpUUgs40w6n7w+uZ+zgfEvwtYuefwD/Nj5SKqmUT0lH9uCNrhhPeM1AAh7uDF3l2+Z6qeibQlNpOfPUCijVEQ1pX1LFaVoZ/TIPn9TTCcIZNSh4h2dtj1wFtEhxMGdQIeMxoIkoHJdXlG1zd3MY1qfyy44bmaYg0esM6c+tiCMB5+rSxJXZwpYXBcCPIeYJKH97CSvg14d0HcDzCa5+TUbuuQ9EJRAhBb4xvFDo8XuYMcTRzL+o1OjnfvydTJZ06dmmgQyOQ5VdXzUS7DmUyYCW7NV4l0Svqs9DUAF2JKKiU4Iz12YJX49IOBvPJ1MIIl4+INFHLiqVgced+0wrY+vVz/nTwJUCsO/9z" +DOTENV_VAULT_DEVELOPMENT_VERSION=2 + +# ci +DOTENV_VAULT_CI="LgVgNuEBUkhGG8ZdRxFBtT7441hP6L8QuODgG+h9DSIbylay" +DOTENV_VAULT_CI_VERSION=1 + +# staging +DOTENV_VAULT_STAGING="nNtpwQWUo3tAcPwZdhHb2kh2HmeOeIprqrawLNhRveIZYiEc" +DOTENV_VAULT_STAGING_VERSION=1 + +# production +DOTENV_VAULT_PRODUCTION="gvtiPWYriQIELI6JOjJcbETIpYToWW9hsPKzzx+ytrfl4dHk" +DOTENV_VAULT_PRODUCTION_VERSION=1 + +#/----------------settings/metadata-----------------/ +DOTENV_VAULT="vlt_b7c9def2e36a7ec5674985772e9f60c1ef0b75063e493d0136b80bcc17ed11ca" +DOTENV_API_URL="https://vault.dotenv.org" +DOTENV_CLI="npx dotenv-vault@latest" diff --git a/backend/app.js b/backend/app.js index ad126a76..2c32e6f1 100644 --- a/backend/app.js +++ b/backend/app.js @@ -7,6 +7,7 @@ const multer = require('multer'); require('dotenv').config(); const { createServer } = require('http'); const { Server } = require('socket.io'); +const enforce = require('express-sslify'); const app = express(); const port = process.env.PORT || 5001; @@ -25,6 +26,7 @@ const io = new Server(server, { }); if (process.env.NODE_ENV === 'production') { + app.use(enforce.HTTPS({ trustProtoHeader: true })); const corsOptions = { origin: ['https://www.study-compass.com', 'https://studycompass.com'], optionsSuccessStatus: 200 // for legacy browser support @@ -65,17 +67,32 @@ const classroomChangeRoutes = require('./routes/classroomChangeRoutes.js'); const ratingRoutes = require('./routes/ratingRoutes.js'); const searchRoutes = require('./routes/searchRoutes.js'); const eventRoutes = require('./routes/eventRoutes.js'); +const oieRoutes = require('./routes/oie-routes.js'); +const orgRoutes = require('./routes/orgRoutes.js'); app.use(authRoutes); app.use(dataRoutes); app.use(friendRoutes); app.use(userRoutes); -app.use(analyticsRoutes);app.use(eventRoutes); +app.use(analyticsRoutes); +app.use(eventRoutes); app.use(classroomChangeRoutes); app.use(ratingRoutes); app.use(searchRoutes); app.use(eventRoutes); +app.use(oieRoutes); +app.use(orgRoutes); + +// Serve static files from the React app in production +if (process.env.NODE_ENV === 'production') { + app.use(express.static(path.join(__dirname, '../frontend/build'))); + + // The "catchall" handler: for any request that doesn't match one above, send back React's index.html file. + app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '../frontend/build/index.html')); + }); +} //deprecated, should lowk invest in this // app.get('/update-database', (req, res) => { @@ -129,15 +146,6 @@ app.post('/upload-image/:classroomName', upload.single('image'), async (req, res } }); -// Serve static files from the React app in production -if (process.env.NODE_ENV === 'production') { - app.use(express.static(path.join(__dirname, '../frontend/build'))); - - // The "catchall" handler: for any request that doesn't match one above, send back React's index.html file. - app.get('*', (req, res) => { - res.sendFile(path.join(__dirname, '../frontend/build/index.html')); - }); -} // Socket.io functionality io.on('connection', (socket) => { diff --git a/backend/middlewares/verifyToken.js b/backend/middlewares/verifyToken.js index 01f2db49..eeb7f33c 100644 --- a/backend/middlewares/verifyToken.js +++ b/backend/middlewares/verifyToken.js @@ -6,13 +6,30 @@ const verifyToken = (req, res, next) => { if (token == null) return res.sendStatus(401); // if there's no token - jwt.verify(token, process.env.JWT_SECRET, (err, user) => { + jwt.verify(token, process.env.JWT_SECRET, (err, decodedToken) => { if (err) return res.sendStatus(403); // if the token is not valid - req.user = user; + // if (decodedToken && decodedToken.exp) { + // const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds + // const timeLeft = decodedToken.exp - currentTime; // Time left in seconds + // const hoursLeft = (timeLeft / 3600).toFixed(2); // Convert to hours and format to 2 decimal places + + // console.debug(`Token has ${hoursLeft} hours left until expiration.`); + // } + req.user = decodedToken; next(); }); }; +function authorizeRoles(...allowedRoles) { + return (req, res, next) => { + const { roles } = req.user; + if (!roles || !allowedRoles.some(role => roles.includes(role))) { + return res.status(403).json({ message: 'Forbidden' }); + } + next(); + }; +} + const verifyTokenOptional = (req, res, next) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN @@ -22,13 +39,13 @@ const verifyTokenOptional = (req, res, next) => { return next(); } - jwt.verify(token, process.env.JWT_SECRET, (err, user) => { + jwt.verify(token, process.env.JWT_SECRET, (err, decodedToken) => { if (!err) { - req.user = user; // Set the user if the token is valid + req.user = decodedToken; // Set the user if the token is valid } // Proceed regardless of token validity next(); }); }; -module.exports = { verifyToken, verifyTokenOptional }; \ No newline at end of file +module.exports = { verifyToken, verifyTokenOptional, authorizeRoles }; \ No newline at end of file diff --git a/backend/migrations/version1.10.py b/backend/migrations/version1.10.py new file mode 100644 index 00000000..7c202ecb --- /dev/null +++ b/backend/migrations/version1.10.py @@ -0,0 +1,31 @@ +from pymongo.mongo_client import MongoClient +from pymongo.server_api import ServerApi +import os +from dotenv import load_dotenv + +from helpers.datamigration import addNewField, updateVersion + + +# ============================== starter code ========================================== + +VERSION = 1.10 # set version here + +load_dotenv() +uri = os.environ.get('MONGO_URL_LOCAL') +database = input("Indicate which database you would like to update (d for development or p for production): ").strip() +if(database == "d"): + pass +elif (database == "p"): + sure = input("WARNING: This will effect a production database, type 'studycompass' to proceed: ").strip() + if(sure.lower() == 'studycompass'): + uri = os.environ.get('MONGO_URL') + else: + exit(1) +else: + print(f"Improper usage: invalid input {database}") + +# ===================================================================================== + +addNewField(uri, "users", {"roles" : ["user"]}) + +updateVersion(uri, VERSION) \ No newline at end of file diff --git a/backend/migrations/version1.12.py b/backend/migrations/version1.12.py new file mode 100644 index 00000000..d3fd46af --- /dev/null +++ b/backend/migrations/version1.12.py @@ -0,0 +1,31 @@ +from pymongo.mongo_client import MongoClient +from pymongo.server_api import ServerApi +import os +from dotenv import load_dotenv + +from helpers.datamigration import addNewField, updateVersion + + +# ============================== starter code ========================================== + +VERSION = 1.10 # set version here + +load_dotenv() +uri = os.environ.get('MONGO_URL_LOCAL') +database = input("Indicate which database you would like to update (d for development or p for production): ").strip() +if(database == "d"): + pass +elif (database == "p"): + sure = input("WARNING: This will effect a production database, type 'studycompass' to proceed: ").strip() + if(sure.lower() == 'studycompass'): + uri = os.environ.get('MONGO_URL') + else: + exit(1) +else: + print(f"Improper usage: invalid input {database}") + +# ===================================================================================== + +addNewField(uri, "users", {"clubAssociations" : []}) + +updateVersion(uri, VERSION) \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 4f3747c7..9bb27617 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -18,6 +18,7 @@ "date-fns": "^4.1.0", "dotenv": "^16.3.2", "express": "^4.18.2", + "express-sslify": "^1.2.0", "google-auth-library": "^9.4.2", "googleapis": "^131.0.0", "jsonwebtoken": "^9.0.2", @@ -892,6 +893,12 @@ "node": ">= 0.10.0" } }, + "node_modules/express-sslify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/express-sslify/-/express-sslify-1.2.0.tgz", + "integrity": "sha512-OOf2B3MxAVjEXPPWl4Z19wA2oMH+RCULJVhejPwuhiDDClr9QczZz5ycABLSnnN+oY8JcLs32ghs9cxOj0vi+w==", + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index d5e1770d..07f614d6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,6 +9,7 @@ "date-fns": "^4.1.0", "dotenv": "^16.3.2", "express": "^4.18.2", + "express-sslify": "^1.2.0", "google-auth-library": "^9.4.2", "googleapis": "^131.0.0", "jsonwebtoken": "^9.0.2", diff --git a/backend/routes/analytics.js b/backend/routes/analytics.js index a7065c1f..df50b974 100644 --- a/backend/routes/analytics.js +++ b/backend/routes/analytics.js @@ -2,8 +2,12 @@ const express = require('express'); const router = express.Router(); const { isValid, formatISO, parseISO, setHours, startOfWeek, endOfWeek, setMinutes, setSeconds, setMilliseconds, eachHourOfInterval, eachDayOfInterval } = require('date-fns'); const Visit = require('../schemas/visit'); +const Search = require('../schemas/search') const User = require('../schemas/user'); const QR = require('../schemas/qr'); +const RepeatedVisit = require('../schemas/repeatedVisit'); +const { verifyToken, verifyTokenOptional } = require('../middlewares/verifyToken'); + // Route to log a visit router.post('/log-visit', async (req, res) => { @@ -16,6 +20,28 @@ router.post('/log-visit', async (req, res) => { } }); +router.post('/log-repeated-visit', verifyTokenOptional, async (req, res) => { + const { hash } = req.body; + try { + console.log('Logging repeated visit'); + let repeatedVisit = await RepeatedVisit.findOne({hash: hash}); + if(!repeatedVisit){ + repeatedVisit = new RepeatedVisit({ visits: [new Date()], hash: hash, user_id: req.user ? req.user.userId : null }); + } + repeatedVisit.user_id = req.user ? req.user.userId : null; + repeatedVisit.visits.push(new Date()); + if(req.user && req.user.userId){ + repeatedVisit.user_id = req.user.userId; + } + await repeatedVisit.save(); + console.log('Repeated visit logged'); + res.status(200).json({ message: 'Visit logged' }); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Failed to log visit' }); + } +}); + router.get('/visits-by-day', async (req, res) => { try { const { startDate, endDate } = req.query; @@ -463,5 +489,215 @@ router.post('/qr', async (req, res) => { } }); +router.get('/searches-by-day', async (req, res) => { + try { + const { startDate, endDate } = req.query; + + // Validate the dates + if (!startDate || !isValid(parseISO(startDate))) { + return res.status(400).json({ error: 'Invalid startDate' }); + } + + const parsedStartDate = parseISO(startDate); + const parsedEndDate = endDate && isValid(parseISO(endDate)) ? parseISO(endDate) : new Date(); + + const startOfWeekDate = startOfWeek(parsedStartDate, { weekStartsOn: 0 }); // 1 = Monday + const endOfWeekDate = endOfWeek(parsedStartDate, { weekStartsOn: 0 }); //print for debugging + + + // Query visits within the week range and group by day + const visitsByDay = await Search.aggregate([ + { + $match: { + timestamp: { + $gte: startOfWeekDate, + $lt: endOfWeekDate + } + } + }, + { + $group: { + _id: { + year: { $year: "$timestamp" }, + month: { $month: "$timestamp" }, + day: { $dayOfMonth: "$timestamp" } + }, + count: { $sum: 1 } + } + }, + { + $sort: { "_id.year": 1, "_id.month": 1, "_id.day": 1 } + } + ]); + + // Generate an array with all days within the week range + const daysInRange = eachDayOfInterval({ + start: startOfWeekDate, + end: endOfWeekDate, + }); + + // Format and pad the result + const formattedResult = daysInRange.map((day) => { + const formattedDate = formatISO(day, { representation: 'date' }); + + // Find the matching day in the database result + const visitForDay = visitsByDay.find(item => + item._id.year === day.getFullYear() && + item._id.month === day.getMonth() + 1 && + item._id.day === day.getDate() + ); + + // Return either the actual count or 0 if there was no visit for this day + return { + date: formattedDate, + count: visitForDay ? visitForDay.count : 0 + }; + }); + + res.json(formattedResult); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'An error occurred while fetching visits by day' }); + } +}); + +// `/visits-by-hour` route: Fetches visits grouped by hour within the specified range and pads missing hours with 0 visits +router.get('/searches-by-hour', async (req, res) => { + try { + const { startDate } = req.query; + + // Validate the startDate + if (!startDate || !isValid(parseISO(startDate))) { + return res.status(400).json({ error: 'Invalid startDate' }); + } + + const parsedDate = parseISO(startDate); + + // Define the start and end of the day explicitly using setHours, setMinutes, etc. + const startOfDay = setMilliseconds(setSeconds(setMinutes(setHours(parsedDate, 0), 0), 0), 0); // 00:00 of the given day + const endOfDay = setMilliseconds(setSeconds(setMinutes(setHours(parsedDate, 23), 59), 59), 999); // 23:59:59.999 of the same day + // Query visits within the day range and group by hour + const visitsByHour = await Search.aggregate([ + { + $match: { + timestamp: { + $gte: startOfDay, + $lte: endOfDay + } + } + }, + { + $group: { + _id: { + year: { $year: "$timestamp" }, + month: { $month: "$timestamp" }, + day: { $dayOfMonth: "$timestamp" }, + hour: { $hour: "$timestamp" } + }, + count: { $sum: 1 } + } + }, + { + $sort: { + "_id.year": 1, "_id.month": 1, "_id.day": 1, "_id.hour": 1 + } + } + ]); + + // Generate an array with all hours from 00:00 to 23:00 + const hoursInRange = eachHourOfInterval({ + start: startOfDay, + end: endOfDay + }); + + // Format and pad the result + const formattedResult = hoursInRange.map((hour) => { + const formattedHour = `${hour.getFullYear()}-${String(hour.getMonth() + 1).padStart(2, '0')}-${String(hour.getDate()).padStart(2, '0')} ${String(hour.getHours()).padStart(2, '0')}:00`; + + // Find the matching hour in the database result + const visitForHour = visitsByHour.find(item => + item._id.year === hour.getFullYear() && + item._id.month === hour.getMonth() + 1 && + item._id.day === hour.getDate() && + item._id.hour === hour.getHours() + ); + + // Return either the actual count or 0 if there was no visit for this hour + return { + hour: formattedHour, + count: visitForHour ? visitForHour.count : 0 + }; + }); + + res.json(formattedResult); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'An error occurred while fetching visits by hour' }); + } +}); + + +router.get('/searches-by-all', async (req, res) => { + try { + // Query to get the earliest and latest visit dates for padding the result + const firstVisit = await Visit.findOne().sort({ timestamp: 1 }); + const lastVisit = await Visit.findOne().sort({ timestamp: -1 }); + + if (!firstVisit || !lastVisit) { + return res.json([]); // No visits in the database + } + + // Get the date range (from the first to the last visit) + const startDate = new Date(firstVisit.timestamp); + const endDate = new Date(lastVisit.timestamp); + + // Query to get all visits within the range, grouped by day + const visitsByAll = await Search.aggregate([ + { + $group: { + _id: { + year: { $year: "$timestamp" }, + month: { $month: "$timestamp" }, + day: { $dayOfMonth: "$timestamp" } + }, + count: { $sum: 1 } + } + }, + { + $sort: { "_id.year": 1, "_id.month": 1, "_id.day": 1 } + } + ]); + + // Generate an array with all days in the date range + const daysInRange = eachDayOfInterval({ + start: startDate, + end: endDate, + }); + + // Format and pad the result + const formattedResult = daysInRange.map((day) => { + const formattedDate = formatISO(day, { representation: 'date' }); + + // Find the matching day in the database result + const visitForDay = visitsByAll.find(item => + item._id.year === day.getFullYear() && + item._id.month === day.getMonth() + 1 && + item._id.day === day.getDate() + ); + + // Return either the actual count or 0 if there was no visit for this day + return { + date: formattedDate, + count: visitForDay ? visitForDay.count : 0 + }; + }); + + res.json(formattedResult); + } catch (err) { + console.error(err); + res.status(500).json({ error: 'An error occurred while fetching all-time visits' }); + } +}); + module.exports = router; diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js index 8ed6a525..8136a758 100644 --- a/backend/routes/authRoutes.js +++ b/backend/routes/authRoutes.js @@ -78,7 +78,7 @@ router.post('/register', async (req, res) => { await user.save(); // Generate a token for the new user - const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' }); + const token = jwt.sign({ userId: user._id, roles: user.roles }, process.env.JWT_SECRET, { expiresIn: '5d' }); console.log(`POST: /register new user ${username}`); sendDiscordMessage(`New user registered`, `user ${username} registered`, "newUser"); // Send the token to the client @@ -132,9 +132,10 @@ router.get('/validate-token', verifyToken, async (req, res) => { try { const user = await User.findById(req.user.userId) .select('-password') // Add fields you want to exclude - .lean(); // Assuming Mongoose for DB operations + .lean() + .populate('clubAssociations'); if (!user) { - console.log(`GET: /validate-token token is invalid`) + console.log(`GET: /validate-token token is invalid`); return res.status(404).json({ success: false, message: 'User not found' }); } console.log(`GET: /validate-token token is valid for user ${user.username}`) @@ -142,30 +143,11 @@ router.get('/validate-token', verifyToken, async (req, res) => { success: true, message: 'Token is valid', data: { - // user: { - // _id: user._id, - // username: user.username, - // email: user.email, - // name: user.name, - // picture : user.picture, - // admin : user.admin, - // saved: user.saved, - // visited: user.visited, - // partners: user.partners, - // sessions: user.sessions, - // hours: user.hours, - // contributions: user.contributions, - // onboarded: user.onboarded, - // classroomPreferences: user.classroomPreferences, - // recommendationPreferences: user.recommendationPreferences, - // google: user.googleId ? true : false, - // tags: user.tags - // } user : user } }); } catch (error) { - console.log(`GET: /validate-token token is invalid`) + console.log(`GET: /validate-token token is invalid`, error) res.status(500).json({ success: false, message: 'Error fetching user details', diff --git a/backend/routes/classroomChangeRoutes.js b/backend/routes/classroomChangeRoutes.js index 55be5a40..c4b85f21 100644 --- a/backend/routes/classroomChangeRoutes.js +++ b/backend/routes/classroomChangeRoutes.js @@ -4,7 +4,7 @@ const Schedule = require('../schemas/schedule.js'); const Rating = require('../schemas/rating.js'); const User = require('../schemas/user.js'); const Report = require('../schemas/report.js'); -const { verifyToken, verifyTokenOptional } = require('../middlewares/verifyToken'); +const { verifyToken, authorizeRoles } = require('../middlewares/verifyToken'); const { sortByAvailability } = require('../helpers.js'); const multer = require('multer'); const path = require('path'); @@ -122,7 +122,7 @@ router.post('/upload-image/:classroomName', upload.single('image'), async (req, } }); -router.post('/main-search-change', verifyToken, async (req, res) => { +router.post('/main-search-change', verifyToken, authorizeRoles('admin'), async (req, res) => { const userId = req.user.userId; const classroomId = req.body.classroomId; try{ @@ -131,7 +131,7 @@ router.post('/main-search-change', verifyToken, async (req, res) => { console.log(`POST: /main-search-change/${userId} failed`); return res.status(404).json({ success: false, message: "User not found" }); } - if(!user.admin){ + if(!user.roles.includes('admin')) { console.log(`POST: /main-search-change/${userId} failed`); return res.status(403).json({ success: false, message: "User not authorized" }); } diff --git a/backend/routes/dataRoutes.js b/backend/routes/dataRoutes.js index 43f8b48a..b22a0b44 100644 --- a/backend/routes/dataRoutes.js +++ b/backend/routes/dataRoutes.js @@ -3,6 +3,7 @@ const Classroom = require('../schemas/classroom.js'); const Schedule = require('../schemas/schedule.js'); const Rating = require('../schemas/rating.js'); const User = require('../schemas/user.js'); +const History = require('../schemas/studyHistory.js'); const Report = require('../schemas/report.js'); const { verifyToken, verifyTokenOptional } = require('../middlewares/verifyToken'); const { sortByAvailability } = require('../helpers.js'); @@ -183,7 +184,6 @@ router.post('/getbatch', async (req, res) => { }); - router.get('/get-recommendation', verifyTokenOptional, async (req, res) => { const userId = req.user ? req.user.userId : null; try { @@ -261,5 +261,48 @@ router.get('/get-recommendation', verifyTokenOptional, async (req, res) => { } }); +router.get("/get-history", verifyToken, async (req,res) => { + const userId = req.user.userId; + try{ + //takes in user id, returns all study history objects associated with user + const getHistory = await History.find({ user_id : userId }); + + if(getHistory){ + console.log(`GET: /get-history`); + return res.status(200).json({success: true, message: 'History grabbed', data: getHistory}); + } else { + return res.status(404).json({ success: false, message: 'Could not get history' }); + } + + } catch(error){ + console.log(`GET: /get-history failed`, error); + return res.status(500).json({ success: false, message: 'Error finding user', error: error.message }); + } +}); + + +router.delete("/delete-history",verifyToken, async (req,res)=>{ + // takes in study history id and deletes object + const histId = req.body.histId; + try{ + const deleteHist = await History.deleteOne({ _id: histId}); + //check if successful, if success, return success status, if not, return 404 + //if deleted acount =0 then return 404 + + if (deleteHist.deletedCount!==0){ + console.log(`DELETE: /delete-history`); + return res.status(200).json({success: true, message: 'History sucessfully deleted', data: deleteHist}); + } else { + return res.status(404).json({ success: false, message: 'Could not delete history' }); + } + + + }catch(error){ + console.log(`DELETE: /delete-history failed`, error); + return res.status(500).json({ success: false, message: 'Error finding user', error: error.message }); + } +}); + + module.exports = router; diff --git a/backend/routes/eventRoutes.js b/backend/routes/eventRoutes.js index 7f3a5a80..a55514e3 100644 --- a/backend/routes/eventRoutes.js +++ b/backend/routes/eventRoutes.js @@ -1,33 +1,51 @@ const express = require('express'); const router = express.Router(); -const { verifyToken, verifyTokenOptional } = require('../middlewares/verifyToken'); +const { verifyToken, verifyTokenOptional, authorizeRoles } = require('../middlewares/verifyToken'); const Event = require('../schemas/event'); const User = require('../schemas/user'); const Classroom = require('../schemas/classroom'); const OIEStatus = require('../schemas/OIE'); +const Org = require('../schemas/org') router.post('/create-event', verifyToken, async (req, res) => { const user_id = req.user.userId; + const orgId = req.body.orgId; + + try { + let event; - const event = new Event({ - ...req.body, - hostingId : user_id, - hostingType : 'User', - - }); + if(orgId){ + const user = await User.findById(user_id); + if(!user.clubAssociations.includes(orgId)){ + return res.status(403).json({ + "message": 'you are not authorized to create an event as this organization' + }) + } + event = new Event({ + ...req.body, + hostingId : user_id, + hostingType : 'User', + }); + } else { + event = new Event({ + ...req.body, + hostingId : orgId, + hostingType : 'Org', + }); + } + + let OIE; - let OIE = null; + if (event.expectedAttendance > 100 || event.OIEAcknowledgementItems && event.OIEAcknowledgementItems.length > 0) { + event.OIEStatus = "Pending"; + OIE = new OIEStatus({ + eventRef: event._id, + status: 'Pending', + checkListItems: [], + }); + } - if (event.expectedAttendance > 200 || event.OIEAcknowledgementItems && event.OIEAcknowledgementItems.length > 0) { - event.OIEStatus = "Pending"; - OIE = new OIEStatus({ - eventRef: event._id, - status: 'Pending', - checkListItems: event.OIEAcknowledgementItems - }); - } - try { await event.save(); if (OIE) { await OIE.save(); @@ -159,10 +177,10 @@ router.delete('/delete-event/:event_id', verifyToken, async (req, res) => { }); //get all oie-unapproved events -router.get('/oie/get-pending-events', verifyToken, async (req, res) => { +router.get('/oie/get-pending-events', verifyToken, authorizeRoles('oie'), async (req, res) => { try { const user = await User.findById(req.user.userId); - if (!user || !user.admin) { + if (!user) { return res.status(403).json({ success: false, message: 'You are not authorized to view this page.' @@ -183,6 +201,217 @@ router.get('/oie/get-pending-events', verifyToken, async (req, res) => { } }); +//get all oie-unapproved events +router.get('/oie/get-approved-events', verifyToken, authorizeRoles('oie'), async (req, res) => { + try { + const user = await User.findById(req.user.userId); + if (!user ) { + return res.status(403).json({ + success: false, + message: 'You are not authorized to view this page.' + }); + } + const events = await Event.find({ OIEStatus: 'Approved' }).populate('classroom_id').populate('hostingId'); + console.log('GET: /oie/get-approved-events successful'); + res.status(200).json({ + success: true, + events + }); + } catch (error) { + console.log('GET: /oie/get-approved-events failed', error); + res.status(500).json({ + success: false, + message: error.message + }); + } +}); + +//get all oie-unapproved events +router.get('/oie/get-rejected-events', verifyToken, authorizeRoles('oie'), async (req, res) => { + try { + const user = await User.findById(req.user.userId); + if (!user) { + return res.status(403).json({ + success: false, + message: 'You are not authorized to view this page.' + }); + } + const events = await Event.find({ OIEStatus: 'Rejected' }).populate('classroom_id').populate('hostingId'); + console.log('GET: /oie/get-rejected-events successful'); + res.status(200).json({ + success: true, + events + }); + } catch (error) { + console.log('GET: /oie/get-rejected-events failed', error); + res.status(500).json({ + success: false, + message: error.message + }); + } +}); + +router.get('/get-event/:event_id', verifyTokenOptional, async (req, res) => { + const { event_id } = req.params; + const user_id = req.user ? req.user.userId : null; + + try { + const user = user_id ? await User.findById(user_id) : null; + const event = await Event.findById(event_id).populate('classroom_id').populate('hostingId'); + if (!event) { + return res.status(404).json({ + success: false, + message: 'Event not found.' + }); + } + if (event.OIEStatus !== 'Not Applicable') { + if (!user){ + return res.status(403).json({ + success: false, + message: 'You are not authorized to view this page.' + }); + } else { + const OIE = await OIEStatus.findOne({ eventRef: event_id }); + if (OIE) { + //attach to event object + let newEvent = event.toObject(); + newEvent["OIE"] = OIE; + return res.status(202).json({ + success: true, + event: newEvent + }); + } + return res.status(200).json({ + success: true, + event + }); + } + } + console.log('GET: /get-event successful'); + res.status(200).json({ + success: true, + event + }); + } catch (error) { + console.log('GET: /get-event failed', error); + res.status(500).json({ + success: false, + message: error.message + }); + } +}); + +router.post('/approve-event', verifyToken, async (req, res) => { + const user_id = req.user.userId; + const { event_id } = req.body; + + try { + const user = await User.findById(user_id); + if(!user) { + return res.status(403).json({ + success: false, + message: 'You are not authorized to approve events.' + }); + } + const event = await Event.findById(event_id); + if (!event) { + return res.status(404).json({ + success: false, + message: 'Event not found.' + }); + } + if (event.OIEStatus !== 'Pending') { + return res.status(400).json({ + success: false, + message: 'Event is not pending approval.' + }); + } + event.OIEStatus = 'Approved'; + await event.save(); + console.log('POST: /approve-event successful'); + res.status(200).json({ + success: true, + message: 'Event approved successfully.' + }); + } catch (error) { + console.log('POST: /approve-event failed', error); + res.status(500).json({ + success: false, + message: error.message + }); + } +}); + +router.get('/get-events-by-month', verifyToken, authorizeRoles('oie'), async (req, res) => { + const { month, year } = req.query; + + if (!month || !year) { + return res.status(400).json({ + success: false, + message: 'Month and year are required parameters.', + }); + } + + try { + // Parse month and year into integers + const parsedMonth = parseInt(month, 10) - 1; // JavaScript months are 0-indexed + const parsedYear = parseInt(year, 10); + + // Construct the start and end of the month + const startOfMonth = new Date(parsedYear, parsedMonth, 1); // First day of the month + const endOfMonth = new Date(parsedYear, parsedMonth + 1, 0, 23, 59, 59, 999); // Last day of the month + + // Query events with dates within the range + const events = await Event.find({ + start_time: { $gte: startOfMonth, $lte: endOfMonth } + }).populate('classroom_id'); + + console.log('GET: /get-events-by-month successful'); + res.status(200).json({ + success: true, + events + }); + } catch (error) { + console.log('GET: /get-events-by-month failed', error); + res.status(500).json({ + success: false, + message: error.message + }); + } +}); + +router.get('/get-events-by-range', verifyToken, authorizeRoles('oie'), async (req, res) => { + const { start, end } = req.query; + + if (!start || !end) { + return res.status(400).json({ + success: false, + message: 'Start and end dates are required parameters.', + }); + } + + try { + const startOfRange = new Date(start); + const endOfRange = new Date(end); + + const events = await Event.find({ + start_time: { $gte: startOfRange, $lte: endOfRange } + }).populate('classroom_id'); + + console.log('GET: /get-events-by-week successful'); + res.status(200).json({ + success: true, + events + }); + } catch (error) { + console.log('GET: /get-events-by-week failed', error); + res.status(500).json({ + success: false, + message: error.message + }); + } +}); + module.exports = router; diff --git a/backend/routes/oie-routes.js b/backend/routes/oie-routes.js new file mode 100644 index 00000000..3b410e59 --- /dev/null +++ b/backend/routes/oie-routes.js @@ -0,0 +1,69 @@ +const express = require('express'); +const router = express.Router(); +const { verifyToken, authorizeRoles } = require('../middlewares/verifyToken'); +const OIEConfig = require('../schemas/OIEConfig'); +const OIEStatus = require('../schemas/OIE'); +const Event = require('../schemas/event'); + +router.get('/config', verifyToken, authorizeRoles('oie'), async (req, res) => { + try { + const config = await OIEConfig.findOne({}); + if (!config) { + return res.status(404).json({ message: 'Config not found' }); + } + console.log('GET: /config successful'); + res.json(config); + } catch (error) { + console.log('GET: /config failed', error); + res.status(500).json({ message: error.message }); + } +}); + +router.post('/config', verifyToken, authorizeRoles('oie'), async (req, res) => { + try { + const config = await OIEConfig.findOne({}); + if (config) { + // Update existing config + config.config = req.body.config; + await config.save(); + return res.json(config); + } + const newConfig = new OIEConfig(req.body); + await newConfig.save(); + console.log('POST: /config successful'); + res.json(newConfig); + } catch (error) { + console.log("POST: /config failed", error); + res.status(500).json({ message: error.message }); + } +}); + +router.post('/oie-status', verifyToken, authorizeRoles('oie'), async (req, res) => { + try { + const { eventRef, status, checkListItems } = req.body; + const oieStatus = await OIEStatus.findOne({ eventRef }); + if (oieStatus) { + oieStatus.status = status; + oieStatus.checkListItems = checkListItems; + await oieStatus.save(); + const event = await Event.findById(eventRef); + event.OIEStatus = status; + await event.save(); + console.log('POST: /oie-status successful'); + return res.json(oieStatus); + } + const newOIEStatus = new OIEStatus(req.body); + await newOIEStatus.save(); + const event = await Event.findById(eventRef); + event.OIEStatus = status; + await event.save(); + console.log(event); + console.log('POST: /oie-status successful'); + res.json(newOIEStatus); + } catch (error) { + console.log('POST: /oie-status failed', error); + res.status(500).json({ message: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/orgRoutes.js b/backend/routes/orgRoutes.js new file mode 100644 index 00000000..f59c6522 --- /dev/null +++ b/backend/routes/orgRoutes.js @@ -0,0 +1,474 @@ +const express = require('express'); +const router = express.Router(); +const { verifyToken, verifyTokenOptional } = require('../middlewares/verifyToken.js'); +const mongoose = require('mongoose'); +const User = require('../schemas/user.js'); +const Org = require('../schemas/org.js'); +const Follower = require('../schemas/orgFollower.js'); +const Member = require('../schemas/orgMember.js'); +const { clean, isProfane } = require('../services/profanityFilterService.js'); + + +//Route to get a specific org by name +router.get("/get-org/:id", verifyToken, async(req,res)=>{ +//May eventually change login permissions + +try{ + const orgId= req.params.id; + + //Find the Org + const org= await Org.find({_id: orgId}); + + if(org !== null){ + // If the org exists, return it + console.log(`GET: /get-org/${orgId}`); + res.json({ success: true, message: "Org found", org: org}); + + } else{ + // If not found, return a 404 with a message + return res.status(404).json({ success: false, message: 'Org not found' }); + } + + } catch(error){ + // Handle any errors that occur during the process + console.log(`GET: /get-org failed`, error); + return res.status(500).json({ success: false, message: 'Error retrieving org data', error: error.message }); + } +}); + +router.get("/get-org-by-name/:name", verifyToken, async(req,res)=>{ + try{ + const orgName= req.params.name; + + const org= await Org.findOne({org_name: orgName}); + + if(!org){ + return res.status(404).json({ success: false, message: 'Org not found' }); + } + + const orgMembers = await Member.find({org_id: org._id}).populate('user_id'); + + // If the org exists, return it + console.log(`GET: /get-org-by-name/${orgName}`); + res.json({ success: true, message: "Org found", org: { + overview: org, + members: orgMembers + }}); + + + + } catch(error){ + // Handle any errors that occur during the process + console.log(`GET: /get-org-by-name failed`, error); + return res.status(500).json({ success: false, message: 'Error retrieving org data', error: error.message }); + } + } +); + +router.post("/create-org", verifyToken, async(req,res)=>{ + + const{ org_name, org_profile_image, org_description, positions, weekly_meeting } = req.body; + + try { + + //Verify user and have their orgs saved under them + const userId = req.user.userId; + const user = await User.findById({_id: userId}); + console.log(user); + if(!user){ + return res.status(404).json({ success: false, message: 'User not found' }); + } + + const orgExist = await Org.findOne({org_name: org_name }); + //Check to verify if the org already exists + if (orgExist){ + return res.status(400).json({ success: false, message: 'Org name already exists'}); + } + if (isProfane(req.body.org_name)) { //Check if Bad Words + return res.status(400).json({ success: false, message: 'Org name contains inappropriate language' }); + } + + const cleanOrgName = clean(org_name); // Sanitize org name and description verify this right + if (isProfane(org_description)) { //Check if Bad Words + return res.status(400).json({ success: false, message: 'Description contains inappropriate language' }); + } + + const cleanOrgDescription = clean(org_description); + + const newOrg = new Org({ + org_name: cleanOrgName, + org_profile_image: org_profile_image, + org_description: cleanOrgDescription, + positions: positions || ['chair', 'officer', 'regular'], + weekly_meeting: weekly_meeting || null, + //Owner is the user + owner: userId + }); + + const newMember = new Member({ //add new member to the org + org_id: newOrg._id, + user_id: userId, + status: 0, + }); + + await newMember.save(); + + user.clubAssociations.push(newOrg._id); + await user.save(); + + const saveOrg = await newOrg.save(); //Save the org + console.log(`POST: /create-org`); + return res.status(200).json({ success: true, message: "Org created successfully", org: saveOrg }); + + } catch(error){ + // Handle any errors that occur during the process + console.log(`POST: /create-org failed`, error); + return res.status(500).json({ success: false, message: 'Error creating org', error: error.message }); + } +}); + + +router.post("/edit-org", verifyToken, async (req, res) => { + try { + const { orgId, org_profile_image, org_description, positions, weekly_meeting, org_name } = req.body; + const userId = req.user?.userId; + + // Validate that the essential fields are present + if (!orgId) { + return res.status(400).json({ success: false, message: "Org ID is required" }); + } + + const org = await Org.findById(orgId); + + // Check if the org exists + console.log + if (!org) { + return res.status(404).json({ success: false, message: "Org not found" }); + } + + // Check if the user is authorized to edit the org + if (org.owner.toString() !== userId) { + return res.status(400).json({ success: false, message: "You are not authorized to edit this org" }); + } + + // If org_name is provided, clean it and check for inappropriate language + if (org_name) { + + const cleanOrgName = clean(org_name); + + if (isProfane(org_name)) { + return res.status(400).json({ success: false, message: "Org name contains inappropriate language" }); + } + + // Check if a org with the cleaned name already exists, excluding the current org + const orgExist = await Org.findOne({ org_name: cleanOrgName }); + if (orgExist && orgExist._id.toString() !== orgId) { + return res.status(400).json({ success: false, message: "Org name already taken" }); + } + + // Update the org name with the clean version + org.org_name = cleanOrgName; + } + + // If org_description is provided, clean it + if (org_description) { + const cleanOrgDescription = clean(org_description); + + if (isProfane(org_description)) { + return res.status(400).json({ success: false, message: "Description contains inappropriate language" }); + } + org.org_description = cleanOrgDescription; + } + + // Update other fields only if they are provided + if (org_profile_image) { + org.org_profile_image = org_profile_image; + } + if (positions) { + org.positions = positions; + } + if (weekly_meeting) { + org.weekly_meeting = weekly_meeting; + } + + // Save the updated org + await org.save(); + + // Send success response + console.log(`POST: /edit-org`); + res.json({ success: true, message: "Org updated successfully", org }); + + } catch (error) { + // Handle any errors that occur during the process + console.log(`POST: /edit-org failed`, error); + return res.status(500).json({ success: false, message: "Error updating org", error: error.message }); + } +}); + + + +router.delete("/delete-org/:orgId", verifyToken, async(req,res)=>{ +try { + const { orgId } = req.params; + const userId = req.user.userId; + + const org = await Org.findById(orgId); + + //Check if the org exists + if (!org) { + return res.status(404).json({ success: false, message: "Org not found" }); + } + + // Verify that the user is the owner of the org + if (org.owner.toString() !== userId) { + return res.status(400).json({ success: false, message: "You are not authorized to delete this org" }); + } + + // Delete the org + await Org.findByIdAndDelete(orgId); + + console.log(`DELETE: /delete-org`); + res.json({ success: true, message: "Org deleted successfully" }); + +} catch (error) { + console.log(`DELETE: /delete-org failed`, error); + return res.status(500).json({ success: false, message: "Error deleting org", error: error.message }); +} +}); + +router.post("/follow-org/:orgId", verifyToken, async(req,res)=>{ + try { + const { orgId } = req.params; + const userId = req.user.userId; + + const org = await Org.findById(orgId); + + //Check if the org exists + if (!org) { + return res.status(404).json({ success: false, message: "Org not found" }); + } + + const alreadyFollowing = await Follower.findOne({ user_id: userId, org_id: orgId }); //Check if the user is already following the org + if (alreadyFollowing) { + return res.status(400).json({ success: false, message: "You are already following this org" }); + } + + const newFollower = new Follower({ + user_id: userId, + org_id: orgId + }); + await newFollower.save(); + + console.log(`POST: /follow-org`); + res.json({ success: true, message: "Org followed successfully" }); + + } catch (error) { + console.log(`POST: /follow-org failed`, error); + return res.status(500).json({ success: false, message: "Error following org", error: error.message }); + } +}); + +router.post("/unfollow-org/:orgId", verifyToken, async(req,res)=>{ + try { + const { orgId } = req.params; + const userId = req.user.userId; + + const org = await Org.findById(orgId); + + //Check if the org exists + if (!org) { + return res.status(404).json({ success: false, message: "Org not found" }); + } + + const follower = await Follower.findOne({ user_id: userId, org_id: orgId }); //Check if the user is already following the org + if (!follower) { + return res.status(400).json({ success: false, message: "You are not following this org" }); + } + + await Follower.findByIdAndDelete(follower._id); + + console.log(`POST: /unfollow-org`); + res.json({ success: true, message: "Org unfollowed successfully" }); + + } catch (error) { + console.log(`POST: /unfollow-org failed`, error); + return res.status(500).json({ success: false, message: "Error unfollowing org", error: error.message }); + } +}); + +router.get("/get-followed-orgs", verifyToken, async(req,res)=>{ + try { + const userId = req.user.userId; + + const followedOrgs = await Follower.find({ user_id: userId }).populate('org_id'); + console.log(`GET: /get-followed-orgs`); + res.json({ success: true, message: "Followed orgs retrieved successfully", orgs: followedOrgs }); + + } catch (error) { + console.log(`GET: /get-followed-orgs failed`, error); + return res.status(500).json({ success: false, message: "Error retrieving followed orgs", error: error.message }); + } +}); + +router.get("/get-org-members/:orgId", verifyToken, async(req,res)=>{ + try { + const { orgId } = req.params; + const userId = req.user.userId; + + const org = await Org.findById(orgId); + + //Check if the org exists + if (!org) { + return res.status(404).json({ success: false, message: "Org not found" }); + } + + // Check if the user is a member of the org + const member = await Member.findOne({ org_id: orgId, user_id: userId }); + if (!member) { + return res.status(400).json({ success: false, message: "You are not a member of this org" }); + } + + const members = await Member.find({ org_id: orgId }).populate('user_id'); + console.log(`GET: /get-org-members`); + res.json({ success: true, message: "Org members retrieved successfully", members }); + + } catch (error) { + console.log(`GET: /get-org-members failed`, error); + return res.status(500).json({ success: false, message: "Error retrieving org members", error: error.message }); + } +}); + +router.post("/add-org-member/:orgId", verifyToken, async(req,res)=>{ + try { + const { orgId } = req.params; + const userId = req.user.userId; + const { user_id, role } = req.body; + + const org = await Org.findById(orgId); + + //Check if the org exists + if (!org) { + return res.status(404).json({ success: false, message: "Org not found" }); + } + + // Check if the user is the owner of the org + if (org.owner.toString() !== userId) { + return res.status(400).json({ success: false, message: "You are not authorized to add members to this org" }); + } + + // Check if the user to be added is already a member + const member = await Member.findOne({ org_id: orgId, user_id: user_id }); + if (member) { + return res.status(400).json({ success: false, message: "User is already a member of this org" }); + } + + const newMember = new Member({ + org_id: orgId, + user_id, + role + }); + await newMember.save(); + + console.log(`POST: /add-org-member`); + res.json({ success: true, message: "Member added successfully" }); + + } catch (error) { + console.log(`POST: /add-org-member failed`, error); + return res.status(500).json({ success: false, message: "Error adding member", error: error.message }); + } +}); + +router.delete("/remove-org-member/:orgId", verifyToken, async(req,res)=>{ + try { + const { orgId } = req.params; + const userId = req.user.userId; + const { user_id } = req.body; + + const org = await Org.findById(orgId); + + //Check if the org exists + if (!org) { + return res.status(404).json({ success: false, message: "Org not found" }); + } + + // Check if the user is the owner of the org + if (org.owner.toString() !== userId) { + return res.status(400).json({ success: false, message: "You are not authorized to remove members from this org" }); + } + + // Check if the user to be removed is a member + const member = await Member.findOne({ org_id: orgId, user_id }); + if (!member) { + return res.status(400).json({ success: false, message: "User is not a member of this org" }); + } + + await Member.findByIdAndDelete(member._id); + + console.log(`DELETE: /remove-org-member`); + res.json({ success: true, message: "Member removed successfully" }); + + } catch (error) { + console.log(`DELETE: /remove-org-member failed`, error); + return res.status(500).json({ success: false, message: "Error removing member", error: error.message }); + } +}); + +router.post("/update-org-member/:orgId", verifyToken, async(req,res)=>{ + try { + const { orgId } = req.params; + const userId = req.user.userId; + const { user_id, role } = req.body; + + const org = await Org.findById(orgId); + + //Check if the org exists + if (!org) { + return res.status(404).json({ success: false, message: "Org not found" }); + } + + // Check if the user is the owner of the org + if (org.owner.toString() !== userId) { + return res.status(400).json({ success: false, message: "You are not authorized to update members of this org" }); + } + + // Check if the user to be updated is a member + const member = await Member.findOne({ org_id: orgId, user_id }); + if (!member) { + return res.status(400).json({ success: false, message: "User is not a member of this org" }); + } + + member.role = role; + await member.save(); + + console.log(`POST: /update-org-member`); + res.json({ success: true, message: "Member updated successfully" }); + + } catch (error) { + console.log(`POST: /update-org-member failed`, error); + return res.status(500).json({ success: false, message: "Error updating member", error: error.message }); + } +}); + +router.post('/check-org-name', verifyToken, async (req, res) => { + const { orgName } = req.body; + try{ + if(orgName.length === 0) { + return res.status(400).json({ success: false, message: 'Org name is required' }); + } + if (isProfane(orgName)) { + return res.status(400).json({ success: false, message: 'Org name contains inappropriate language' }); + } + const cleanOrgName = clean(orgName); + const orgExist = await Org.findOne({ org_name: cleanOrgName }); + if (orgExist) { + return res.status(402).json({ success: false, message: 'Org name already exists' }); + } + console.log(`POST: /org-name-check`); + return res.json({ success: true, message: 'Org name is available' }); + } catch (error){ + console.log('POST: /org-name-check failed', error); + return res.status(500).json({ success: false, message: 'Error checking org name', error: error.message }); + } +}); + +module.exports = router; diff --git a/backend/routes/searchRoutes.js b/backend/routes/searchRoutes.js index d6283e98..f378d0a1 100644 --- a/backend/routes/searchRoutes.js +++ b/backend/routes/searchRoutes.js @@ -11,6 +11,7 @@ const path = require('path'); const s3 = require('../aws-config'); const mongoose = require('mongoose'); const { clean } = require('../services/profanityFilterService'); +const Search = require('../schemas/search.js'); const router = express.Router(); @@ -243,6 +244,17 @@ router.get('/all-purpose-search', verifyTokenOptional, async (req, res) => { // Extract only the names from the result set const names = sortedClassrooms.map(classroom => classroom.name); + //analytics + const search = new Search({ + query: { + query: query, + attributes: attributes, + timePeriod: timePeriod + }, + user_id: userId ? userId : null, + }); + + search.save(); res.json({ success: true, message: "Rooms found", data: names }); diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index 06306494..c095081b 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -1,20 +1,21 @@ const express = require('express'); const User = require('../schemas/user.js'); const Developer = require('../schemas/developer.js'); -const { verifyToken, verifyTokenOptional } = require('../middlewares/verifyToken'); +const { verifyToken, verifyTokenOptional, authorizeRoles } = require('../middlewares/verifyToken'); const Classroom = require('../schemas/classroom.js'); const Schedule = require('../schemas/schedule.js'); const cron = require('node-cron'); const axios = require('axios'); -const { isProfane } = require('../services/profanityFilterService'); -const StudyHistory = require('../schemas/studyHistory.js'); +const { isProfane } = require('../services/profanityFilterService'); +const StudyHistory = require('../schemas/studyHistory.js'); const { findNext } = require('../helpers.js'); const { sendDiscordMessage } = require('../services/discordWebookService'); +const BadgeGrant = require('../schemas/badgeGrant'); const router = express.Router(); router.post("/update-user", verifyToken, async (req, res) =>{ - const { name, username, classroom, recommendation, onboarded } = req.body + const { name, username, classroom, recommendation, onboarded, darkModePreference } = req.body try{ const user = await User.findById(req.user.userId); if (!user) { @@ -23,65 +24,66 @@ router.post("/update-user", verifyToken, async (req, res) =>{ } user.name = name ? name : user.name; user.username = username ? username : user.username; - user.classroomPreferences = classroom ? classroom : user.classroomPreferences; + user.classroomPreferences = classroom ? classroom : user.classroomPreferences; user.recommendationPreferences = recommendation ? recommendation : user.recommendationPreferences; user.onboarded = onboarded ? onboarded : user.onboarded; + user.darkModePreference = darkModePreference !== null ? darkModePreference : user.darkModePreference; await user.save(); console.log(`POST: /update-user ${req.user.userId} successful`); return res.status(200).json({ success: true, message: 'User updated successfully' }); - } catch(error){ + } catch (error) { console.log(`POST: /update-user ${req.user.userId} failed`) return res.status(500).json({ success: false, message: error.message }); } }); // check if username is available -router.post("/check-username", verifyToken, async (req, res) =>{ +router.post("/check-username", verifyToken, async (req, res) => { const { username } = req.body; const userId = req.user.userId; - try{ + try { //check if username is taken, regardless of casing - if(isProfane(username)){ + if (isProfane(username)) { console.log(`POST: /check-username ${username} is profane`) return res.status(200).json({ success: false, message: 'Username does not abide by community standards' }); } const reqUser = await User.findById(userId); const user = await User.findOne({ username: { $regex: new RegExp(username, "i") } }); - if(user && user._id.toString() !== userId){ + if (user && user._id.toString() !== userId) { console.log(`POST: /check-username ${username} is taken`) return res.status(200).json({ success: false, message: 'Username is taken' }); } console.log(`POST: /check-username ${username} is available`) return res.status(200).json({ success: true, message: 'Username is available' }); - } catch(error){ + } catch (error) { console.log(`POST: /check-username ${username} failed`) return res.status(500).json({ success: false, message: 'Internal server error', error }); } }); -router.post("/check-in", verifyToken, async (req, res) =>{ +router.post("/check-in", verifyToken, async (req, res) => { const { classroomId } = req.body; - try{ + try { //check if user is checked in elsewhere in the checked_in array - const classrooms = await Classroom.find({checked_in: { $in: [req.user.userId] }}); - + const classrooms = await Classroom.find({ checked_in: { $in: [req.user.userId] } }); + // const classrooms = await Classroom.find({ checkIns: req.user.userId }); - if(classrooms.length > 0){ + if (classrooms.length > 0) { console.log(`POST: /check-in ${req.user.userId} is already checked in`) return res.status(400).json({ success: false, message: 'User is already checked in' }); } const classroom = await Classroom.findOne({ _id: classroomId }); classroom.checked_in.push(req.user.userId); await classroom.save(); - if(req.user.userId !== "65f474445dca7aca4fb5acaf"){ - sendDiscordMessage(`User check-in`,`user ${req.user.userId} checked in to ${classroom.name}`,"normal"); + if (req.user.userId !== "65f474445dca7aca4fb5acaf") { + sendDiscordMessage(`User check-in`, `user ${req.user.userId} checked in to ${classroom.name}`, "normal"); } //create history object, preempt end time using findnext const schedule = await Schedule.findOne({ classroom_id: classroomId }); - if(schedule){ + if (schedule) { let endTime = findNext(schedule.weekly_schedule); //time in minutes from midnight - endTime = new Date(new Date().setHours(Math.floor(endTime/60), endTime%60, 0, 0)); + endTime = new Date(new Date().setHours(Math.floor(endTime / 60), endTime % 60, 0, 0)); const history = new StudyHistory({ user_id: req.user.userId, classroom_id: classroomId, @@ -96,39 +98,39 @@ router.post("/check-in", verifyToken, async (req, res) =>{ console.log(`POST: /check-in ${req.user.userId} into ${classroom.name} successful`); return res.status(200).json({ success: true, message: 'Checked in successfully' }); - } catch(error){ + } catch (error) { console.log(`POST: /check-in ${req.user.userId} failed`); console.log(error); return res.status(500).json({ success: false, message: 'Internal server error', error }); } }); -router.get("/checked-in", verifyToken, async (req, res) =>{ - try{ +router.get("/checked-in", verifyToken, async (req, res) => { + try { const classrooms = await Classroom.find({ checked_in: { $in: [req.user.userId] } }); console.log(`GET: /checked-in ${req.user.userId} successful`) return res.status(200).json({ success: true, message: 'Checked in classrooms retrieved', classrooms }); - } catch(error){ + } catch (error) { console.log(`GET: /checked-in ${req.user.userId} failed`) return res.status(500).json({ success: false, message: 'Internal server error', error }); } }); -router.post("/check-out", verifyToken, async (req, res) =>{ +router.post("/check-out", verifyToken, async (req, res) => { const { classroomId } = req.body; - try{ + try { const classroom = await Classroom.findOne({ _id: classroomId }); classroom.checked_in = classroom.checked_in.filter(userId => userId !== req.user.userId); await classroom.save(); const schedule = await Schedule.findOne({ classroom_id: classroomId }); - if(schedule){ + if (schedule) { //find latest history object const history = await StudyHistory.findOne({ user_id: req.user.userId, classroom_id: classroomId }).sort({ start_time: -1 }); const endTime = new Date(); //if time spent is less than 5 minutes, delete history object - if(history){ + if (history) { const timeDiff = endTime - history.start_time; - if(timeDiff < 300000){ + if (timeDiff < 300000) { await history.deleteOne(); } else { //else update end time @@ -136,10 +138,10 @@ router.post("/check-out", verifyToken, async (req, res) =>{ await history.save(); //update user stats const user = await User.findOne({ _id: req.user.userId }); - user.hours += timeDiff/3600000; + user.hours += timeDiff / 3600000; //find if new classroom visited const pastHistory = await StudyHistory.findOne({ user_id: req.user.userId, classroom_id: classroomId }); - if(!pastHistory){ + if (!pastHistory) { user.visited.push(classroomId); } } @@ -148,43 +150,43 @@ router.post("/check-out", verifyToken, async (req, res) =>{ const io = req.app.get('io'); io.to(classroomId).emit('check-out', { classroomId, userId: req.user.userId }); console.log(`POST: /check-out ${req.user.userId} from ${classroom.name} successful`); - if(req.user.userId !== "65f474445dca7aca4fb5acaf"){ - sendDiscordMessage(`User check-out`,`user ${req.user.userId} checked out of ${classroom.name}`,"normal"); + if (req.user.userId !== "65f474445dca7aca4fb5acaf") { + sendDiscordMessage(`User check-out`, `user ${req.user.userId} checked out of ${classroom.name}`, "normal"); } return res.status(200).json({ success: true, message: 'Checked out successfully' }); - } catch(error){ + } catch (error) { console.log(`POST: /check-out ${req.user.userId} failed`); console.log(error); return res.status(500).json({ success: false, message: 'Internal server error', error }); } }); -router.get("/get-developer", verifyToken, async (req, res) =>{ - try{ +router.get("/get-developer", verifyToken, async (req, res) => { + try { const developer = await Developer.findOne({ user_id: req.user.userId }); console.log(`GET: /get-developer ${req.user.userId} successful`); - if(!developer){ + if (!developer) { return res.status(204).json({ success: false, message: 'Developer not found' }); } return res.status(200).json({ success: true, message: 'Developer retrieved', developer }); - } catch(error){ + } catch (error) { console.log(`GET: /get-developer ${req.user.userId} failed`) return res.status(500).json({ success: false, message: 'Internal server error', error }); } }); -router.post("/update-developer", verifyToken, async (req, res) =>{ +router.post("/update-developer", verifyToken, async (req, res) => { const { type, commitment, goals, skills } = req.body; - try{ + try { const developer = await Developer.findOne({ userId: req.user.userId }); const user = await User.findById(req.user.userId); - - if(!developer){ + + if (!developer) { //craete developer const newDeveloper = new Developer({ user_id: req.user.userId, - name : user.name, + name: user.name, type, commitment, goals, @@ -205,35 +207,114 @@ router.post("/update-developer", verifyToken, async (req, res) =>{ await developer.save(); console.log(`POST: /update-developer ${req.user.userId} successful`); return res.status(200).json({ success: true, message: 'Developer updated successfully' }); - } catch(error){ + } catch (error) { console.log(`POST: /update-developer ${req.user.userId} failed`) return res.status(500).json({ success: false, message: 'Internal server error', error }); } }); -router.get("/get-user", async (req, res) =>{ +router.get("/get-user", async (req, res) => { const userId = req.query.userId; - try{ + try { const user = await User.findById(userId); console.log(`GET: /get-user ${req.query.userId} successful`); return res.status(200).json({ success: true, message: 'User retrieved', user }); - } catch(error){ + } catch (error) { console.log(`GET: /get-user ${req.query.userId} failed`) return res.status(500).json({ success: false, message: 'Internal server error', error }); } }); //route to get mulitple users, specified in array -router.get("/get-users", async (req, res) =>{ +router.get("/get-users", async (req, res) => { const userIds = req.query.userIds; - try{ + try { const users = await User.find({ _id: { $in: userIds } }); console.log(`GET: /get-users ${req.query.userId} successful`); return res.status(200).json({ success: true, message: 'Users retrieved', users }); - } catch(error){ + } catch (error) { console.log(`GET: /get-users ${req.query.userId} failed`) return res.status(500).json({ success: false, message: 'Internal server error', error }); } }); +router.post('/create-badge-grant', verifyToken, authorizeRoles('admin'), async (req, res) => { + try { + const { badgeContent, badgeColor, daysValid } = req.body; + + // Input validation + if (!badgeContent || !badgeColor || !daysValid) { + return res.status(400).json({ error: 'All fields are required' }); + } + + const validFrom = new Date(); + const validTo = new Date(); + validTo.setDate(validTo.getDate() + daysValid); + + const badgeGrant = new BadgeGrant({ + badgeContent, + badgeColor, + validFrom, + validTo, + }); + + await badgeGrant.save(); + + res.status(201).json({ + message: 'Badge grant created successfully', + hash: badgeGrant.hash, + validFrom, + validTo, + }); + } catch (error) { + console.error('Error creating badge grant:', error); + res.status(500).json({ error: 'Server error' }); + } +}); + +router.post('/grant-badge', verifyToken, async (req, res) => { + try { + const { hash } = req.body; + const userId = req.user.userId; + + if (!hash) { + return res.status(400).json({ error: 'Hash is required' }); + } + + const badgeGrant = await BadgeGrant.findOne({ hash }); + + if (!badgeGrant) { + return res.status(404).json({ error: 'Invalid badge grant' }); + } + + const currentDate = new Date(); + + //check if the today's date is within the valid period + if (currentDate < badgeGrant.validFrom || currentDate > badgeGrant.validTo) { + return res.status(400).json({ error: 'Badge grant is not valid at this time' }); + } + + const user = await User.findById(userId); + + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + if(user.tags.includes(badgeGrant.badgeContent)){ + return res.status(406).json({ error: 'You\'ve already been granted this badge' }); + } + + // Append the badge to the user's badges array + user.tags.push(badgeGrant.badgeContent); + + await user.save(); + console.log(`POST: /grant-badge ${req.user.userId} successful`); + + res.status(200).json({ message: 'Badge granted successfully', badges: user.badges, badge: {badgeContent:badgeGrant.badgeContent, badgeColor: badgeGrant.badgeColor} }); + } catch (error) { + console.error('Error granting badge:', error); + res.status(500).json({ error: 'Server error' }); + } +}); + + module.exports = router; diff --git a/backend/schemas/OIEConfig.js b/backend/schemas/OIEConfig.js new file mode 100644 index 00000000..b4ba894b --- /dev/null +++ b/backend/schemas/OIEConfig.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const OIESchema = new mongoose.Schema({ + config : { + type: Object, + required: true + }, +}, { + timestamps: true +}); + +const OIEConfig = mongoose.model('OIEConfig', OIESchema , 'OIEConfig'); + +module.exports = OIEConfig; \ No newline at end of file diff --git a/backend/schemas/badgeGrant.js b/backend/schemas/badgeGrant.js new file mode 100644 index 00000000..665ba2a1 --- /dev/null +++ b/backend/schemas/badgeGrant.js @@ -0,0 +1,14 @@ +const mongoose = require('mongoose'); +const { v4: uuidv4 } = require('uuid'); + +const BadgeGrantSchema = new mongoose.Schema({ + badgeContent: { type: String, required: true }, + badgeColor: { type: String, required: true }, + validFrom: { type: Date, required: true }, + validTo: { type: Date, required: true }, + hash: { type: String, required: true, unique: true, default: uuidv4 }, +}); + +const BadgeGrant = mongoose.model('BadgeGrant', BadgeGrantSchema); + +module.exports = BadgeGrant; diff --git a/backend/schemas/event.js b/backend/schemas/event.js index d98a390c..ec14a9a7 100644 --- a/backend/schemas/event.js +++ b/backend/schemas/event.js @@ -70,8 +70,11 @@ const eventSchema = new mongoose.Schema({ OIEAcknowledgementItems: { type: Array, default: [] - } - + }, + contact:{ + type:String, + required:false, + }, }, { timestamps: true // automatically adds 'createdAt' and 'updatedAt' fields }); diff --git a/backend/schemas/org.js b/backend/schemas/org.js new file mode 100644 index 00000000..a727c5ae --- /dev/null +++ b/backend/schemas/org.js @@ -0,0 +1,37 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const OrgSchema= new Schema({ + org_name:{ + type:String, + required: true + }, + org_profile_image:{ + type: String, + required: true + }, + org_description:{ + type: String, + required: true + }, + positions: { + type: Array, // [regular, treasurer, secretary] + required: true, + default:['chair', 'officer', 'member'] //add more complex roles, include permissions + }, + weekly_meeting:{ + type: Object, //Times,Data, Room Location + required: false + }, + owner: { + type: Schema.Types.ObjectId, + required: true, + ref: 'User' + } + +}); + + +const Org = mongoose.model('Org', OrgSchema,'orgs'); + +module.exports=Org; diff --git a/backend/schemas/orgFollower.js b/backend/schemas/orgFollower.js new file mode 100644 index 00000000..e6aec920 --- /dev/null +++ b/backend/schemas/orgFollower.js @@ -0,0 +1,20 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const followerSchema = new Schema({ + org_id: { + type: Schema.Types.ObjectId, + required: true, + ref: 'Club' + }, + user_id: { + type: Schema.Types.ObjectId, + required: true, + ref: 'User' + }, +}); + + +const Followers = mongoose.model("follower", followerSchema,'followers'); + +module.exports =Followers; \ No newline at end of file diff --git a/backend/schemas/orgMember.js b/backend/schemas/orgMember.js new file mode 100644 index 00000000..49f6f5e9 --- /dev/null +++ b/backend/schemas/orgMember.js @@ -0,0 +1,25 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const memberSchema = new Schema({ + org_id: { + type: Schema.Types.ObjectId, + required: true, + ref: 'Org' + }, + user_id: { + type: Schema.Types.ObjectId, + required: true, + ref: 'User' + }, + status: { + //role index, see org schema for roles + type: Number, + required:true, + } +}); + + +const OrgMember = mongoose.model('OrgMember', memberSchema, 'members'); + +module.exports = OrgMember; \ No newline at end of file diff --git a/backend/schemas/repeatedVisit.js b/backend/schemas/repeatedVisit.js new file mode 100644 index 00000000..f83e851f --- /dev/null +++ b/backend/schemas/repeatedVisit.js @@ -0,0 +1,9 @@ +const mongoose = require('mongoose'); + +const RepeatedVisitSchema = new mongoose.Schema({ + visits: { type: Array, required: true }, + user_id: { type: mongoose.Schema.Types.ObjectId, required: false, ref: 'User' }, + hash: { type: String, required: true }, +}); + +module.exports = mongoose.model('RepeatedVisit', RepeatedVisitSchema); diff --git a/backend/schemas/search.js b/backend/schemas/search.js new file mode 100644 index 00000000..f82f77a8 --- /dev/null +++ b/backend/schemas/search.js @@ -0,0 +1,11 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + + +const SearchSchema = new mongoose.Schema({ + timestamp: { type: Date, default: Date.now }, + query: { type: Object, required: true }, + user_id: { type: Schema.Types.ObjectId, required: false, ref: 'User' } +}); + +module.exports = mongoose.model('Search', SearchSchema); diff --git a/backend/schemas/studyHistory.js b/backend/schemas/studyHistory.js index eda6bd7b..ba7ec065 100644 --- a/backend/schemas/studyHistory.js +++ b/backend/schemas/studyHistory.js @@ -10,7 +10,7 @@ const studyHistory = new Schema({ user_id: { type: Schema.Types.ObjectId, required: true, - ref: 'User' + ref: 'User' }, start_time: { type: Date, diff --git a/backend/schemas/user.js b/backend/schemas/user.js index b1eafb3d..5ca05ae8 100644 --- a/backend/schemas/user.js +++ b/backend/schemas/user.js @@ -87,6 +87,21 @@ const userSchema = new mongoose.Schema({ type: Boolean, default: false, }, + clubAssociations: { //clubs that this user has management role in + type: Array, + default: [], + }, + roles: { + type: [String], + default: ['user'], + enum: ['user', 'admin', 'moderator', 'developer', 'oie'], // Adjust roles as needed + }, + clubAssociations:[ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'Org' + } + ] // you can add more fields here if needed, like 'createdAt', 'updatedAt', etc. }, { diff --git a/backend/services/profanityFilterService.js b/backend/services/profanityFilterService.js index 5616c67a..537eaca3 100644 --- a/backend/services/profanityFilterService.js +++ b/backend/services/profanityFilterService.js @@ -2,11 +2,17 @@ const Filter = require('bad-words'); function isProfane(text) { + if(text.length === 0) { + return false; + } const filter = new Filter(); return filter.isProfane(text); } function clean(text) { + if(text.length === 0) { + return text; + } const filter = new Filter(); return filter.clean(text); } diff --git a/backend/services/userServices.js b/backend/services/userServices.js index 2261f502..f52986f5 100644 --- a/backend/services/userServices.js +++ b/backend/services/userServices.js @@ -52,7 +52,7 @@ async function registerUser({ username, email, password }) { }); await user.save(); - const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' }); + const token = jwt.sign({ userId: user._id, roles:user.roles }, process.env.JWT_SECRET, { expiresIn: '5d' }); return { user, token }; } @@ -62,11 +62,13 @@ async function loginUser({ email, password }) { if (!email.includes('@')) { user = await User.findOne({ username: email }) .select('-googleId') // Add fields to exclude - .lean(); + .lean() + .populate('clubAssociations'); } else { user = await User.findOne({ email }) .select('-googleId') // Add fields to exclude - .lean(); + .lean() + .populate('clubAssociations'); } if (!user) { throw new Error('User not found'); @@ -77,7 +79,7 @@ async function loginUser({ email, password }) { throw new Error('Invalid credentials'); } delete user.password; - const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '12h' }); + const token = jwt.sign({ userId: user._id, roles: user.roles }, process.env.JWT_SECRET, { expiresIn: '5d' }); return { user, token }; } @@ -124,7 +126,7 @@ async function authenticateWithGoogle(code, isRegister = false, url) { sendDiscordMessage(`New user registered`, `user ${user.username} registered`, "newUser"); } - const jwtToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '12h' }); + const jwtToken = jwt.sign({ userId: user._id, roles: user.roles }, process.env.JWT_SECRET, { expiresIn: '5d' }); return { user, token: jwtToken }; } diff --git a/dump/studycompass/OIEConfig.bson b/dump/studycompass/OIEConfig.bson new file mode 100644 index 00000000..c9968e39 Binary files /dev/null and b/dump/studycompass/OIEConfig.bson differ diff --git a/dump/studycompass/OIEConfig.metadata.json b/dump/studycompass/OIEConfig.metadata.json new file mode 100644 index 00000000..a6a25446 --- /dev/null +++ b/dump/studycompass/OIEConfig.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"c4023c5b817d468393a00d389675dcb7","collectionName":"OIEConfig","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/OIEStatuses.bson b/dump/studycompass/OIEStatuses.bson new file mode 100644 index 00000000..d4bba276 Binary files /dev/null and b/dump/studycompass/OIEStatuses.bson differ diff --git a/dump/studycompass/OIEStatuses.metadata.json b/dump/studycompass/OIEStatuses.metadata.json new file mode 100644 index 00000000..8631a1bd --- /dev/null +++ b/dump/studycompass/OIEStatuses.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"50de14233c7d4e31b231aeb7325b1713","collectionName":"OIEStatuses","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/badgegrants.bson b/dump/studycompass/badgegrants.bson new file mode 100644 index 00000000..8733ad1d Binary files /dev/null and b/dump/studycompass/badgegrants.bson differ diff --git a/dump/studycompass/badgegrants.metadata.json b/dump/studycompass/badgegrants.metadata.json new file mode 100644 index 00000000..70e27649 --- /dev/null +++ b/dump/studycompass/badgegrants.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"hash":{"$numberInt":"1"}},"name":"hash_1","background":true,"unique":true}],"uuid":"26c7ce2d4a2244819a98d990a1a8e241","collectionName":"badgegrants","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/classrooms.bson b/dump/studycompass/classrooms.bson new file mode 100644 index 00000000..bf936bd4 Binary files /dev/null and b/dump/studycompass/classrooms.bson differ diff --git a/dump/studycompass/classrooms.metadata.json b/dump/studycompass/classrooms.metadata.json new file mode 100644 index 00000000..d145e75b --- /dev/null +++ b/dump/studycompass/classrooms.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"a027709bb23d472695b7615fb0c0d95a","collectionName":"classrooms","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/classrooms1.bson b/dump/studycompass/classrooms1.bson new file mode 100644 index 00000000..415f360f Binary files /dev/null and b/dump/studycompass/classrooms1.bson differ diff --git a/dump/studycompass/classrooms1.metadata.json b/dump/studycompass/classrooms1.metadata.json new file mode 100644 index 00000000..0ecae107 --- /dev/null +++ b/dump/studycompass/classrooms1.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"211d783a653b43ae9ad81dd463251172","collectionName":"classrooms1","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/clubs.bson b/dump/studycompass/clubs.bson new file mode 100644 index 00000000..6cf3dbc3 Binary files /dev/null and b/dump/studycompass/clubs.bson differ diff --git a/dump/studycompass/clubs.metadata.json b/dump/studycompass/clubs.metadata.json new file mode 100644 index 00000000..afac5001 --- /dev/null +++ b/dump/studycompass/clubs.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"a3a2b14953b34b1ba97595c0fa6dfdb9","collectionName":"clubs","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/date.bson b/dump/studycompass/date.bson new file mode 100644 index 00000000..2679a0af Binary files /dev/null and b/dump/studycompass/date.bson differ diff --git a/dump/studycompass/date.metadata.json b/dump/studycompass/date.metadata.json new file mode 100644 index 00000000..7dcb4f09 --- /dev/null +++ b/dump/studycompass/date.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"20442d39e50d4cceb5ee4d398a869f76","collectionName":"date","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/developers.bson b/dump/studycompass/developers.bson new file mode 100644 index 00000000..974cee7a Binary files /dev/null and b/dump/studycompass/developers.bson differ diff --git a/dump/studycompass/developers.metadata.json b/dump/studycompass/developers.metadata.json new file mode 100644 index 00000000..97e54747 --- /dev/null +++ b/dump/studycompass/developers.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"026a2b32cd6748f78f31a370ea655e5d","collectionName":"developers","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/events.bson b/dump/studycompass/events.bson new file mode 100644 index 00000000..2793707b Binary files /dev/null and b/dump/studycompass/events.bson differ diff --git a/dump/studycompass/events.metadata.json b/dump/studycompass/events.metadata.json new file mode 100644 index 00000000..2b4de1ff --- /dev/null +++ b/dump/studycompass/events.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"ac57a8019e3945649dd1e92e27718ea8","collectionName":"events","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/followers.bson b/dump/studycompass/followers.bson new file mode 100644 index 00000000..e69de29b diff --git a/dump/studycompass/followers.metadata.json b/dump/studycompass/followers.metadata.json new file mode 100644 index 00000000..221817a3 --- /dev/null +++ b/dump/studycompass/followers.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"a6eeaa7f1f76423cb028e5f41f76c622","collectionName":"followers","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/friendships.bson b/dump/studycompass/friendships.bson new file mode 100644 index 00000000..d5ffe4ff Binary files /dev/null and b/dump/studycompass/friendships.bson differ diff --git a/dump/studycompass/friendships.metadata.json b/dump/studycompass/friendships.metadata.json new file mode 100644 index 00000000..7fdb13b5 --- /dev/null +++ b/dump/studycompass/friendships.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"7c066ad1a9d14d22a76c115f26410ecb","collectionName":"friendships","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/history.bson b/dump/studycompass/history.bson new file mode 100644 index 00000000..b53a5df6 Binary files /dev/null and b/dump/studycompass/history.bson differ diff --git a/dump/studycompass/history.metadata.json b/dump/studycompass/history.metadata.json new file mode 100644 index 00000000..acda3521 --- /dev/null +++ b/dump/studycompass/history.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"96638bc329c145c5914e03f7fc546550","collectionName":"history","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/members.bson b/dump/studycompass/members.bson new file mode 100644 index 00000000..2fdd935e Binary files /dev/null and b/dump/studycompass/members.bson differ diff --git a/dump/studycompass/members.metadata.json b/dump/studycompass/members.metadata.json new file mode 100644 index 00000000..046a1399 --- /dev/null +++ b/dump/studycompass/members.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"e32c538f29654ad19105ae2f069bae04","collectionName":"members","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/org.bson b/dump/studycompass/org.bson new file mode 100644 index 00000000..968b532d Binary files /dev/null and b/dump/studycompass/org.bson differ diff --git a/dump/studycompass/org.metadata.json b/dump/studycompass/org.metadata.json new file mode 100644 index 00000000..d95d9876 --- /dev/null +++ b/dump/studycompass/org.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"278b0025425e405589f11556ef5df516","collectionName":"org","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/orgs.bson b/dump/studycompass/orgs.bson new file mode 100644 index 00000000..58810c03 Binary files /dev/null and b/dump/studycompass/orgs.bson differ diff --git a/dump/studycompass/orgs.metadata.json b/dump/studycompass/orgs.metadata.json new file mode 100644 index 00000000..086c1a8a --- /dev/null +++ b/dump/studycompass/orgs.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"dcb4aba1eafc46e48bc3617287db04cc","collectionName":"orgs","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/ratings.bson b/dump/studycompass/ratings.bson new file mode 100644 index 00000000..5a49c675 Binary files /dev/null and b/dump/studycompass/ratings.bson differ diff --git a/dump/studycompass/ratings.metadata.json b/dump/studycompass/ratings.metadata.json new file mode 100644 index 00000000..83d7511b --- /dev/null +++ b/dump/studycompass/ratings.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"e38e93d61113428097304bd74e9d75a6","collectionName":"ratings","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/repeatedvisits.bson b/dump/studycompass/repeatedvisits.bson new file mode 100644 index 00000000..564951cc Binary files /dev/null and b/dump/studycompass/repeatedvisits.bson differ diff --git a/dump/studycompass/repeatedvisits.metadata.json b/dump/studycompass/repeatedvisits.metadata.json new file mode 100644 index 00000000..2ef5293b --- /dev/null +++ b/dump/studycompass/repeatedvisits.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"35b6a087e33744c89bdcf7689a500121","collectionName":"repeatedvisits","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/reports.bson b/dump/studycompass/reports.bson new file mode 100644 index 00000000..292d8e38 Binary files /dev/null and b/dump/studycompass/reports.bson differ diff --git a/dump/studycompass/reports.metadata.json b/dump/studycompass/reports.metadata.json new file mode 100644 index 00000000..8597fe60 --- /dev/null +++ b/dump/studycompass/reports.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"e23a295635be42d5a03da4f13ce33f37","collectionName":"reports","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/scans.bson b/dump/studycompass/scans.bson new file mode 100644 index 00000000..a028b52d Binary files /dev/null and b/dump/studycompass/scans.bson differ diff --git a/dump/studycompass/scans.metadata.json b/dump/studycompass/scans.metadata.json new file mode 100644 index 00000000..f192ee47 --- /dev/null +++ b/dump/studycompass/scans.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"8a591a963fb34ced9fe843403e2f8464","collectionName":"scans","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/schedules.bson b/dump/studycompass/schedules.bson new file mode 100644 index 00000000..4d8b3493 Binary files /dev/null and b/dump/studycompass/schedules.bson differ diff --git a/dump/studycompass/schedules.metadata.json b/dump/studycompass/schedules.metadata.json new file mode 100644 index 00000000..40980319 --- /dev/null +++ b/dump/studycompass/schedules.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"5966f6e27f4a4e66a951e8314a756acb","collectionName":"schedules","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/searches.bson b/dump/studycompass/searches.bson new file mode 100644 index 00000000..b678cfcf Binary files /dev/null and b/dump/studycompass/searches.bson differ diff --git a/dump/studycompass/searches.metadata.json b/dump/studycompass/searches.metadata.json new file mode 100644 index 00000000..d755a569 --- /dev/null +++ b/dump/studycompass/searches.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"c6c7aca98d1f48ff9f5d58ba9413db0d","collectionName":"searches","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/users.bson b/dump/studycompass/users.bson new file mode 100644 index 00000000..495d81f0 Binary files /dev/null and b/dump/studycompass/users.bson differ diff --git a/dump/studycompass/users.metadata.json b/dump/studycompass/users.metadata.json new file mode 100644 index 00000000..13028482 --- /dev/null +++ b/dump/studycompass/users.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"username":{"$numberInt":"1"}},"name":"username_1","background":true,"unique":true},{"v":{"$numberInt":"2"},"key":{"email":{"$numberInt":"1"}},"name":"email_1","background":true,"unique":true}],"uuid":"1ffd161d3eff466baf6d538c54bc710e","collectionName":"users","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/version.bson b/dump/studycompass/version.bson new file mode 100644 index 00000000..5f0e15bf Binary files /dev/null and b/dump/studycompass/version.bson differ diff --git a/dump/studycompass/version.metadata.json b/dump/studycompass/version.metadata.json new file mode 100644 index 00000000..10ccf1b4 --- /dev/null +++ b/dump/studycompass/version.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"c8d58c422ca2427d9fb94decde92a297","collectionName":"version","type":"collection"} \ No newline at end of file diff --git a/dump/studycompass/visits.bson b/dump/studycompass/visits.bson new file mode 100644 index 00000000..409be52a Binary files /dev/null and b/dump/studycompass/visits.bson differ diff --git a/dump/studycompass/visits.metadata.json b/dump/studycompass/visits.metadata.json new file mode 100644 index 00000000..6052b9b6 --- /dev/null +++ b/dump/studycompass/visits.metadata.json @@ -0,0 +1 @@ +{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"f94eb17dc71543e197e333d8f0018952","collectionName":"visits","type":"collection"} \ No newline at end of file diff --git a/frontend/public/MockPoster.png b/frontend/public/MockPoster.png new file mode 100644 index 00000000..5d9c9ec4 Binary files /dev/null and b/frontend/public/MockPoster.png differ diff --git a/frontend/public/Satoshi-Black.otf b/frontend/public/Satoshi-Black.otf new file mode 100644 index 00000000..4f5f852c Binary files /dev/null and b/frontend/public/Satoshi-Black.otf differ diff --git a/frontend/public/email-header.png b/frontend/public/email-header.png new file mode 100644 index 00000000..b54fefb4 Binary files /dev/null and b/frontend/public/email-header.png differ diff --git a/frontend/src/App.css b/frontend/src/App.css index 6b7436d1..cc847b7b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800&display=swap'); +/* @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800&display=swap'); */ :root{ --light:rgba(238, 238, 238, 1); @@ -18,6 +18,7 @@ --lighterborder:#E4E5E6; /* --green: #4DAA57; */ --green: #64AB6C; + --dark-blue: #6D8EFA; --shadow: 0px 7px 29px 0px rgba(100, 100, 111, 0.1); --lighter: #F5F5F5; --offwhite: #F6F6F6; @@ -27,7 +28,8 @@ --rating-yellow : #FFC700; --yellow: #FBBC05; --offwhite: #F6F6F6; - --offwhite: #F6F6F6; + --lightest: #FBFBFB; + --developerBlue: #45A1FC; --developerPurple: #8052FB; @@ -142,7 +144,7 @@ a:hover{ .shimmer { animation: shimmering 2s linear infinite; background: var(--dark); - background: linear-gradient(90deg, var(--dark) 9%, var(--light) 18%, var(--dark) 31%); + background: linear-gradient(90deg, var(--lightborder) 9%, var(--light) 18%, var(--lightborder) 31%); background-size: 1300px 100%; opacity: 0.8; } @@ -322,6 +324,15 @@ button.active{ } } +@keyframes fromLeft{ + from{ + transform: translateX(-100%); + } + to{ + transform: translateX(0); + } +} + .row{ display:flex; flex-direction:row; @@ -332,4 +343,43 @@ button.active{ display:flex; flex-direction:column; gap:5px; - } \ No newline at end of file + } + + +button{ + outline:none; + border:none; + border-radius:5px; + +} + +input{ + outline:none; + border:none; + padding:5px 15px; + font-family: 'Satoshi'; + font-weight:normal; + background-color: var(--light); + font-size:15px; + border-radius: 8px; +} + +textarea{ + outline:none; + border:none; + padding:5px 15px; + font-family: 'Satoshi'; + font-weight:normal; + background-color: var(--light); + font-size:15px; + border-radius: 8px; + resize:none; + +} + +.hover-event{ + position:fixed; + z-index: 9999; + top:0; + left:0; +} \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js index 58d7888e..ea65a1ac 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,5 +1,6 @@ import React, { useEffect } from 'react'; import './App.css'; +import './assets/fonts.css'; import Room from './pages/Room/Room'; import Room1 from './pages/Room/Room1'; import Login from './pages/Login'; @@ -12,10 +13,14 @@ import Friends from './pages/Friends/Friends'; import Profile from './pages/Profile/Profile'; import Landing from './pages/Landing/Landing'; import Events from './pages/Events/Events'; +import Club from './pages/Club/Club'; import DeveloperOnboard from './pages/DeveloperOnboarding/DeveloperOnboarding'; import QR from './pages/QR/QR'; import Admin from './pages/Admin/Admin'; import OIEDash from './pages/OIEDash/OIEDash'; +import NewBadge from './pages/NewBadge/NewBadge'; +import CreateOrg from './pages/CreateOrg/CreateOrg'; +import ClubDash from './pages/ClubDash/ClubDash'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { AuthProvider } from './AuthContext'; import { CacheProvider } from './CacheContext'; @@ -24,6 +29,7 @@ import { NotificationProvider } from './NotificationContext'; import { ErrorProvider } from './ErrorContext'; import { ProfileCreationProvider } from './ProfileCreationContext'; import { WebSocketProvider } from './WebSocketContext'; +import { DarkModeProvider, useDarkMode } from './DarkModeContext'; import Layout from './pages/Layout/Layout'; import axios from 'axios'; import CreateEvent from './pages/CreateEvent/CreateEvent'; @@ -46,7 +52,46 @@ function App() { .catch(error => { console.error('Error logging visit', error); }); + } else { + // console.log('User has already visited'); + // generate 10 char hash + // store in local storage + // send to backend + console.log('User has already visited'); + let hash = localStorage.getItem('hash'); + let timestamp = localStorage.getItem('timestamp'); + if (!hash) { + // generate hash + hash = Math.random().toString(36).substring(2, 12); + // store hash + localStorage.setItem('hash', hash); + } + if (!timestamp) { + timestamp = new Date().toISOString(); + localStorage.setItem('timestamp', timestamp); + } + + //log how many minutes it has been since last visit + console.log("minutes since last visit: ", (new Date().getTime() - new Date(timestamp).getTime()) / 1000 / 60); + + + //if 20 minutes from last timestamp + if (new Date().getTime() - new Date(timestamp).getTime() > 20 * 60 * 1000) { + //send to backend + localStorage.setItem('timestamp', new Date().toISOString()); + axios.post('/log-repeated-visit', { + hash: hash + }) + .then(response => { + localStorage.setItem('timestamp', new Date().toISOString()); + }) + .catch(error => { + console.error('Error logging visit', error); + }); + } } + + }, []); // document.documentElement.classList.add('dark-mode'); return ( @@ -56,33 +101,40 @@ function App() { - - - - }> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> + + + + + }> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> - - - + + + + diff --git a/frontend/src/DBInteractions.js b/frontend/src/DBInteractions.js index db36bd33..908c0d7c 100644 --- a/frontend/src/DBInteractions.js +++ b/frontend/src/DBInteractions.js @@ -66,9 +66,9 @@ const save = async (roomId, userId, operation) => { } }; -const saveUser = async (name, username, email, password, recommendation, classroom) => { +const saveUser = async (name, username, email, password, recommendation, classroom, darkModePreference) => { try{ - const response = await axios.post('/update-user', {name, email, username, classroom, recommendation, onboarded :null}, {headers: {'Authorization': `Bearer ${localStorage.getItem('token')}`}}); + const response = await axios.post('/update-user', {name, email, username, classroom, recommendation, onboarded :null, darkModePreference}, {headers: {'Authorization': `Bearer ${localStorage.getItem('token')}`}}); const responseBody = response.body; if (response.data.success) { console.log("User saved successfully"); @@ -79,7 +79,6 @@ const saveUser = async (name, username, email, password, recommendation, classro console.error("Error saving user"); throw error; } - } const checkUsername = async (username) => { diff --git a/frontend/src/DarkModeContext.js b/frontend/src/DarkModeContext.js new file mode 100644 index 00000000..40961b62 --- /dev/null +++ b/frontend/src/DarkModeContext.js @@ -0,0 +1,70 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import useAuth from './hooks/useAuth'; + +// Create the context +const DarkModeContext = createContext(); + +// Function to use the dark mode context +export const useDarkMode = () => useContext(DarkModeContext); + +// Dark Mode provider component +export const DarkModeProvider = ({ children }) => { + const [notifications, setNotifications] = useState([]); + + // check for auth + const { isAuthenticating, isAuthenticated, user, getDeveloper } = useAuth(); + + const [darkMode, setDarkMode] = useState(() => { + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + return systemPrefersDark; + }); + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleChange = () => setDarkMode(mediaQuery.matches); + + // Listen for changes in system preference + mediaQuery.addEventListener('change', handleChange); + console.log("hello from dark mode"); + + // Clean up event listener + return () => mediaQuery.removeEventListener('change', handleChange); + + }, []); + + useEffect(() => { + if (isAuthenticating) { + return; + } + + if (isAuthenticated && user) { + const userPreference = user.darkModePreference; + if (userPreference !== undefined) { + if(userPreference === true){ + setDarkMode(true); + document.documentElement.classList.add('dark-mode'); + } + } + + } + }, [isAuthenticating, isAuthenticated, user]) + + // check for user system preference + // use code from display settings + + // const toggleDarkMode = () => { + // setDarkMode(prevMode => !prevMode); + // }; + + const setSpecificMode = (mode) => { + setDarkMode(mode); + }; + + + return ( + + {children} + + + ); +}; diff --git a/frontend/src/NotificationContext.js b/frontend/src/NotificationContext.js index c043b79c..cab2f577 100644 --- a/frontend/src/NotificationContext.js +++ b/frontend/src/NotificationContext.js @@ -31,7 +31,7 @@ export const NotificationProvider = ({ children }) => { const savedNotification = localStorage.getItem('notifications'); if (savedNotification) { addNotification(JSON.parse(savedNotification)); - setTimeout(() => localStorage.removeItem('notifications'), 3000); + setTimeout(() => localStorage.removeItem('notifications'), 100); } }, []); diff --git a/frontend/src/assets/ClubGradient.png b/frontend/src/assets/ClubGradient.png new file mode 100644 index 00000000..61e539e4 Binary files /dev/null and b/frontend/src/assets/ClubGradient.png differ diff --git a/frontend/src/assets/Gradients/StudentCardGrad.png b/frontend/src/assets/Gradients/StudentCardGrad.png new file mode 100644 index 00000000..4f9b5d8a Binary files /dev/null and b/frontend/src/assets/Gradients/StudentCardGrad.png differ diff --git a/frontend/src/assets/Gradients/StudentCardGrad.svg b/frontend/src/assets/Gradients/StudentCardGrad.svg new file mode 100644 index 00000000..60a906ec --- /dev/null +++ b/frontend/src/assets/Gradients/StudentCardGrad.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/Icons/DarkMode.svg b/frontend/src/assets/Icons/DarkMode.svg new file mode 100644 index 00000000..4f7293fe --- /dev/null +++ b/frontend/src/assets/Icons/DarkMode.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/Icons/DisplaySettings.svg b/frontend/src/assets/Icons/DisplaySettings.svg new file mode 100644 index 00000000..764b60a8 --- /dev/null +++ b/frontend/src/assets/Icons/DisplaySettings.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/assets/Icons/LightMode.svg b/frontend/src/assets/Icons/LightMode.svg new file mode 100644 index 00000000..e9ede3f3 --- /dev/null +++ b/frontend/src/assets/Icons/LightMode.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/Icons/Locate.svg b/frontend/src/assets/Icons/Locate.svg new file mode 100644 index 00000000..9abbee3f --- /dev/null +++ b/frontend/src/assets/Icons/Locate.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/Icons/Person.svg b/frontend/src/assets/Icons/Person.svg new file mode 100644 index 00000000..e41573b8 --- /dev/null +++ b/frontend/src/assets/Icons/Person.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/src/assets/Icons/RPI.svg b/frontend/src/assets/Icons/RPI.svg new file mode 100644 index 00000000..d959b62c --- /dev/null +++ b/frontend/src/assets/Icons/RPI.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/Icons/rpiLogo.svg b/frontend/src/assets/Icons/rpiLogo.svg new file mode 100644 index 00000000..1351cd0f --- /dev/null +++ b/frontend/src/assets/Icons/rpiLogo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/assets/Inter-VariableFont_opsz,wght.ttf b/frontend/src/assets/Inter-VariableFont_opsz,wght.ttf new file mode 100644 index 00000000..e31b51e3 Binary files /dev/null and b/frontend/src/assets/Inter-VariableFont_opsz,wght.ttf differ diff --git a/frontend/src/assets/RedBottomRight.png b/frontend/src/assets/RedBottomRight.png new file mode 100644 index 00000000..f5682e02 Binary files /dev/null and b/frontend/src/assets/RedBottomRight.png differ diff --git a/frontend/src/assets/RedTopRight.png b/frontend/src/assets/RedTopRight.png new file mode 100644 index 00000000..56eac8da Binary files /dev/null and b/frontend/src/assets/RedTopRight.png differ diff --git a/frontend/src/assets/fonts.css b/frontend/src/assets/fonts.css index e35dec92..176c2062 100644 --- a/frontend/src/assets/fonts.css +++ b/frontend/src/assets/fonts.css @@ -8,4 +8,12 @@ font-family: 'Satoshi'; font-weight: normal; src: url(Satoshi-Bold.otf); -} \ No newline at end of file +} + +@font-face{ + font-family:'Inter'; + src:url(Inter-VariableFont_opsz\,wght.ttf); + font-weight:100 900; + font-style:normal; +} + diff --git a/frontend/src/assets/people.svg b/frontend/src/assets/people.svg new file mode 100644 index 00000000..96874473 --- /dev/null +++ b/frontend/src/assets/people.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/AcccountSettings/AccountSettings.jsx b/frontend/src/components/AcccountSettings/AccountSettings.jsx index 3df3c4a5..f12e3318 100644 --- a/frontend/src/components/AcccountSettings/AccountSettings.jsx +++ b/frontend/src/components/AcccountSettings/AccountSettings.jsx @@ -24,7 +24,7 @@ function AccountSettings({ settingsRightSide, width, handleBackClick, userInfo } // } ); const handleNameChange = (e) => { - // setName(e.target.value); + // setName(e.target.value); }; const handleUsernameChange = (e) => { @@ -37,7 +37,7 @@ function AccountSettings({ settingsRightSide, width, handleBackClick, userInfo } const saveUsername = () => { if (editUsername){ - saveUser(name, username, email, null, null, null); + saveUser(name, username, email, null, null, null, null); } setEditUsername(!editUsername); diff --git a/frontend/src/components/Analytics/Analytics.jsx b/frontend/src/components/Analytics/Analytics.jsx index 40c39d07..c140fd72 100644 --- a/frontend/src/components/Analytics/Analytics.jsx +++ b/frontend/src/components/Analytics/Analytics.jsx @@ -26,6 +26,8 @@ function Analytics() { + + ); } diff --git a/frontend/src/components/Analytics/VisitsChart/AnalyticsChart.jsx b/frontend/src/components/Analytics/VisitsChart/AnalyticsChart.jsx index 108f8096..d47ddbcc 100644 --- a/frontend/src/components/Analytics/VisitsChart/AnalyticsChart.jsx +++ b/frontend/src/components/Analytics/VisitsChart/AnalyticsChart.jsx @@ -160,7 +160,7 @@ const AnalyticsChart = ({endpoint, heading, color}) => { Stats

{heading}

- +

{chartData.datasets ? chartData.datasets[0].data.reduce((a, b) => a + b, 0) : 0} {endpoint}

diff --git a/frontend/src/components/Badges/Badges.scss b/frontend/src/components/Badges/Badges.scss index 81d962ca..646c97cd 100644 --- a/frontend/src/components/Badges/Badges.scss +++ b/frontend/src/components/Badges/Badges.scss @@ -19,6 +19,7 @@ } + .badge.normal p{ margin:0 7px !important; color:white !important; @@ -49,4 +50,8 @@ .badge.developer{ background-color: var(--developerBlue); +} + +.badge.RCOS{ + background-color: #D51F2B; } \ No newline at end of file diff --git a/frontend/src/components/Classroom/Classroom.jsx b/frontend/src/components/Classroom/Classroom.jsx index 352f3937..523dd5b7 100644 --- a/frontend/src/components/Classroom/Classroom.jsx +++ b/frontend/src/components/Classroom/Classroom.jsx @@ -53,7 +53,8 @@ function Classroom({ room, state, setState, schedule, roomName, width, setShowMo //get all users currently checked in const getCheckedInUsers = async () => { try { - if (room.checked_in.length === 0) { + + if (room.name !== null && room.checked_in.length === 0) { return; } const users = await getUsers(room.checked_in); @@ -357,11 +358,11 @@ function Classroom({ room, state, setState, schedule, roomName, width, setShowMo
); })} - {user && user.admin ?
{ setEdit(!edit) }}>
: ""} + {user && user.roles.includes("admin") ?
{ setEdit(!edit) }}>
: ""} {userRating && } { - defaultImage && (!isAuthenticating) && isAuthenticated && user.admin ? : "" + defaultImage && (!isAuthenticating) && isAuthenticated && user.roles.includes('admin') ? : "" }
@@ -390,7 +391,7 @@ function Classroom({ room, state, setState, schedule, roomName, width, setShowMo
- {user && user.admin ? room ? edit ? : "" : "" : ""} + {user && user.roles.includes('admin') ? room ? edit ? : "" : "" : ""}
diff --git a/frontend/src/components/Classroom/EditAttributes/EditAttributes.jsx b/frontend/src/components/Classroom/EditAttributes/EditAttributes.jsx index 888c9d2b..50227226 100644 --- a/frontend/src/components/Classroom/EditAttributes/EditAttributes.jsx +++ b/frontend/src/components/Classroom/EditAttributes/EditAttributes.jsx @@ -83,7 +83,7 @@ function EditAttributes({room, attributes, setEdit}){ {attribute in attributeIcons ? {attribute}: ""} {attribute}
- {user && user.admin && delete { + {user && user.roles.includes('admin') && delete { const newAttributes = attributesAdmin.filter((item) => item !== attribute); console.log(newAttributes); setAttributes(newAttributes); diff --git a/frontend/src/components/CreateEvent/Review/Review.jsx b/frontend/src/components/CreateEvent/Review/Review.jsx index e6f315e0..66455888 100644 --- a/frontend/src/components/CreateEvent/Review/Review.jsx +++ b/frontend/src/components/CreateEvent/Review/Review.jsx @@ -12,6 +12,7 @@ function Review({info, visible, setInfo, onSubmit}){ const [pspeak, setPspeak] = useState(false); const [catering, setCatering] = useState(false); + const [alumni, setAlumni] = useState(false); const handleChange = (e) => { const {name} = e.target; @@ -22,6 +23,9 @@ function Review({info, visible, setInfo, onSubmit}){ case "pspeak": setPspeak(!pspeak); break; + case "alumni": + setAlumni(!alumni); + break; default: break; } @@ -70,6 +74,15 @@ function Review({info, visible, setInfo, onSubmit}){
+
+ + +
@@ -78,7 +91,7 @@ function Review({info, visible, setInfo, onSubmit}){ {visible &&
-

{pspeak || catering ? "request OIE approval" : "publish event"}

+

{pspeak || catering || info.expectedAttendance > 99 || alumni ? "request OIE approval" : "publish event"}

diff --git a/frontend/src/components/CreateEvent/Review/Review.scss b/frontend/src/components/CreateEvent/Review/Review.scss index b09741b2..f38bda9d 100644 --- a/frontend/src/components/CreateEvent/Review/Review.scss +++ b/frontend/src/components/CreateEvent/Review/Review.scss @@ -8,8 +8,8 @@ } .preview{ display:flex; - max-width:400px; - width:400px; + max-width:300px; + width:3 00px; } .oie-acknowledgement{ color:var(--text); @@ -57,6 +57,7 @@ label{ font-family: 'Inter'; font-weight:500; + -webkit-user-select: none; user-select: none; cursor: pointer; } @@ -146,7 +147,6 @@ transition: all 0.5s; margin:0; font-size:18px; - } } diff --git a/frontend/src/components/DisplaySettings/DisplaySettings.css b/frontend/src/components/DisplaySettings/DisplaySettings.css new file mode 100644 index 00000000..49642969 --- /dev/null +++ b/frontend/src/components/DisplaySettings/DisplaySettings.css @@ -0,0 +1,80 @@ + +.display-settings.settings-right{ + .settings-left .display{ + display: flex; + flex-direction: row; + align-items: center; + padding: 10px 18px; + gap: 10px; + /* height:43px; */ + width:100%; + box-sizing: border-box; + font-family: 'Satoshi'; + font-size: 14px; + color: var(--text); + transition: all 0.3s; + border-radius: 10px; + cursor:pointer; + } + + .settings-left .display button { + background: none; + border: none; + } + + .profile .mode{ + display: flex; + flex-direction: row; + } + .profile .light-mode{ + margin: 10px; + align-items: center; + position: relative; + } + + .profile .dark-mode{ + margin: 10px; + align-items: center; + position: relative; + } + + .profile button{ + display: flex; + background: darkgray; + font-family: "Inter"; + font-weight: 500; + color: var(--text); + font-size: 13px; + padding: 5px 9px; + height: -moz-fit-content; + height: fit-content; + outline: none; + border: none; + border-radius: 6px; + } + + img { + border-radius: 0%; + width: 100px; + height: 70px; + margin-left: 0px; + } + + .profile .mode button { + background: none; + /* padding: 3px; */ + border: none; + + } + .mode .selected::before{ + content:''; + border-radius: 10px; + border: 2px solid var(--red); + position: absolute; + top:-2px; + right: -2px; + width:calc(100%); + height: calc(100%); + } + +} \ No newline at end of file diff --git a/frontend/src/components/DisplaySettings/DisplaySettings.jsx b/frontend/src/components/DisplaySettings/DisplaySettings.jsx new file mode 100644 index 00000000..5183bcb2 --- /dev/null +++ b/frontend/src/components/DisplaySettings/DisplaySettings.jsx @@ -0,0 +1,93 @@ +import React, { useEffect, useState } from 'react'; +import './DisplaySettings.css'; +import light from '../../assets/Icons/LightMode.svg'; +import dark from '../../assets/Icons/DarkMode.svg'; +import { useDarkMode } from '../../DarkModeContext'; +import { saveUser } from '../../DBInteractions'; + +const DisplaySettings = ( {settingsRightSide, width, handleBackClick, rightarrow} ) => { + const {darkMode, setSpecificMode} = useDarkMode(); + const [selectedMode, setSelectedMode] = useState(darkMode ? "dark" : "light"); + + useEffect(() => { + // const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + setSelectedMode(darkMode ? 'dark' : 'light'); + }, [darkMode] ); + + // const handleChange = (e) => { + // setSelectedMode(e.matches ? 'dark' : 'light'); + // }; + + // mediaQuery.addEventListener('change', handleChange); + + // return () => { + // mediaQuery.removeEventListener('change', handleChange); + // }; + // }, []); + + + useEffect( () => { + if (selectedMode === 'dark'){ + document.documentElement.classList.add('dark-mode'); + } else { + document.documentElement.classList.remove('dark-mode'); + } + }, [selectedMode] ); + + const handleModeSelect = (mode) => { + setSelectedMode(mode); + setSpecificMode(mode === 'dark'); + }; + + // database takes pritority over system + const savePreference = async () => { + const isDarkMode = selectedMode === 'dark'; + const response = await saveUser(null, null, null, null, null, null, isDarkMode); + console.log(response); + console.log(isDarkMode); + } + + + return( +
+
+

Display Settings

+ {width <= 700 && settingsRightSide && ( + + )} +
+ +
+

light-dark mode preference

+
+ +
+
+ +
+ +
+ + + +
+ +
+ + + +
+ +
+ ); +} + + + +export default DisplaySettings; diff --git a/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/CreateEvent/CreateEvent.scss b/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/CreateEvent/CreateEvent.scss index 87742893..0d1b432b 100644 --- a/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/CreateEvent/CreateEvent.scss +++ b/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/CreateEvent/CreateEvent.scss @@ -7,7 +7,7 @@ overflow: show; z-index: 1; animation: glint 3s infinite linear; - + transition: border 0.3s ease; &::after{ transition: all 0.5s; @@ -24,7 +24,7 @@ } &:hover{ - border:1px solid transparent; + border: 1px solid transparent; } &:hover::after{ @@ -69,10 +69,16 @@ transition: all 0.5s; } + iconify-icon{ + transition: all 0.5s; + } } &:hover .info{ color: var(--background); + iconify-icon{ + color: var(--background); + } h1{ color: var(--background); } diff --git a/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/Event/Event.jsx b/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/Event/Event.jsx index a16048da..6b5460d3 100644 --- a/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/Event/Event.jsx +++ b/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/Event/Event.jsx @@ -22,7 +22,7 @@ function Event({event}){ return(
handleEventClick(event)}> - + {event.image && } diff --git a/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/Event/Event.scss b/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/Event/Event.scss index 2f100737..bb90c0ff 100644 --- a/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/Event/Event.scss +++ b/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/Event/Event.scss @@ -14,8 +14,6 @@ font-size: 16px; color: var(--text); } - - img{ width:calc(100%); border-radius: 5px; diff --git a/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/FullEvent/FullEvent.scss b/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/FullEvent/FullEvent.scss index 50af0eeb..8661fa6b 100644 --- a/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/FullEvent/FullEvent.scss +++ b/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/FullEvent/FullEvent.scss @@ -1,8 +1,15 @@ .full-event{ display:flex; - width:100%; + width:calc(100% - 50px); height:100%; gap:20px; + position: relative; + margin-right:50px; + background-color: var(--background); + border-radius: 20px; + overflow:hidden; + padding:20px; + box-sizing: border-box; iconify-icon{ font-size: 20px; @@ -28,6 +35,11 @@ font-size: 25px; margin-bottom:0; } + p{ + font-size: 15px; + font-weight: 500; + font-family: 'Inter'; + } } .gradient{ diff --git a/frontend/src/components/Forms/LoginForm/LoginForm.jsx b/frontend/src/components/Forms/LoginForm/LoginForm.jsx index 3fe3a03b..c74c2e40 100644 --- a/frontend/src/components/Forms/LoginForm/LoginForm.jsx +++ b/frontend/src/components/Forms/LoginForm/LoginForm.jsx @@ -23,7 +23,8 @@ function LoginForm() { const location = useLocation(); const googleLogo = generalIcons.google; - + const from = location.state?.from?.pathname || '/room/none'; + console.log(from); useEffect(() => { if (isAuthenticated){ console.log("logged in already"); @@ -51,7 +52,7 @@ function LoginForm() { try { await login(formData); console.log("logged in"); - navigate('/room/none',{ replace: true }) + navigate(from,{ replace: true }) // Handle success (e.g., store the token and redirect to a protected page) } catch (error) { console.error('Login failed:', error); @@ -143,7 +144,7 @@ function LoginForm() { -

Don’t have an account? Register

+

Don’t have an account? Register

@@ -156,7 +157,7 @@ function LoginForm() {
-

Don’t have an account? Register

+

Don’t have an account? Register

diff --git a/frontend/src/components/Forms/RegisterForm/RegisterForm.jsx b/frontend/src/components/Forms/RegisterForm/RegisterForm.jsx index 4b9842ff..d9f691b4 100644 --- a/frontend/src/components/Forms/RegisterForm/RegisterForm.jsx +++ b/frontend/src/components/Forms/RegisterForm/RegisterForm.jsx @@ -8,8 +8,6 @@ import circleWarning from '../../../assets/circle-warning.svg'; import { generalIcons } from '../../../Icons'; import Flag from '../../Flag/Flag'; - - function RegisterForm() { const { isAuthenticated, googleLogin, login } = useAuth(); const [valid, setValid] = useState(false); @@ -28,6 +26,7 @@ function RegisterForm() { let navigate = useNavigate(); const location = useLocation(); + const from = location.state?.from?.pathname || '/room/none'; useEffect(() => { async function google(code) { @@ -61,8 +60,13 @@ function RegisterForm() { useEffect(() => { if (isAuthenticated && isAuthenticated !== null) { - console.log("logged in already"); - navigate('/room/none', { replace: true }) + // console.log("logged in already"); + // const redirectto = localStorage.getItem('redirectto'); + // if(redirectto){ + // navigate(redirectto, { replace: true }); + // } else { + navigate('/room/none', { replace: true }); + // } } }, [isAuthenticated, navigate]); @@ -98,7 +102,7 @@ function RegisterForm() { console.log(response.data); // Handle success (e.g., redirect to login page or auto-login) await login(formData); - navigate('/onboard', { replace: true }); + navigate('/onboard', { state: {from:location.state?.from} }); } catch (error) { if(error.response.status === 400){ setErrorText("Username or Email already exists"); @@ -115,7 +119,8 @@ function RegisterForm() { flow: 'auth-code', ux_mode: 'redirect', onFailure: () => { console.log("failed") }, - }) + }); + function failed(message){ navigate('/login'); @@ -136,7 +141,7 @@ function RegisterForm() { {errorText !== "" && } - +

diff --git a/frontend/src/components/Header/Header.jsx b/frontend/src/components/Header/Header.jsx index dae6351d..36080b9f 100644 --- a/frontend/src/components/Header/Header.jsx +++ b/frontend/src/components/Header/Header.jsx @@ -65,7 +65,7 @@ const Header = React.memo(()=>{
diff --git a/frontend/src/components/ImageUpload/ImageUpload.jsx b/frontend/src/components/ImageUpload/ImageUpload.jsx index edcf6318..781a3910 100644 --- a/frontend/src/components/ImageUpload/ImageUpload.jsx +++ b/frontend/src/components/ImageUpload/ImageUpload.jsx @@ -5,7 +5,7 @@ import Upload from '../../assets/Icons/Upload.svg'; import { Icon } from '@iconify-icon/react/dist/iconify.mjs'; import CircleX from '../../assets/Icons/Circle-X.svg'; -const ImageUpload = ({ classroomName, onUpload, uploadText}) => { +const ImageUpload = ({ classroomName, onUpload, uploadText="Upload Classroom Image"}) => { const [selectedFile, setSelectedFile] = useState(null); const [message, setMessage] = useState(''); const [fileName, setFileName] = useState(''); @@ -76,7 +76,7 @@ const ImageUpload = ({ classroomName, onUpload, uploadText}) => { > {image ? preview : } -

{selectedFile ? fileName : "Upload Classroom Image"}

+

{selectedFile ? fileName : uploadText}

{ + if(day === "Time"){ + return ( +
+
+ {/* append child for time period from 12 am to 11:59 pm, one child every 30 minutes */} + {Array.from({ length: 48 }, (_, i) => ( +
+ {i % 2 === 0 ? `${i / 2}:00` : `${(i - 1) / 2}:30`} +
+ ))} +
+
+ ); + } + return ( +
+ {/*
+

{day} {date}

+
*/} + {/*
+ {events && events.map((event, index) => ( + + ))} +
*/} +
+ +
+ {/* append grid item for each 30 minute time period */} + {Array.from({ length: 48 }, (_, i) => ( +
+
+ ))} +
+
+ +
+ ); +}; + +export default DayComponent; \ No newline at end of file diff --git a/frontend/src/components/NewCalendar/DayComponent/DayComponent.scss b/frontend/src/components/NewCalendar/DayComponent/DayComponent.scss new file mode 100644 index 00000000..bc475dc3 --- /dev/null +++ b/frontend/src/components/NewCalendar/DayComponent/DayComponent.scss @@ -0,0 +1,42 @@ +.day-component{ + --calendar-line: #F2F3F7; + --calendar-background-2: #FDFDFE; + + box-sizing: border-box; + border-right: 1px solid var(--calendar-line); + border-bottom: 1px solid var(--calendar-line); + + .header{ + border-bottom: 1px solid var(--lighterborder); + border-top: 1px solid var(--lighterborder); + border-right: 1px solid var(--lighterborder); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height:40px; + margin-bottom:0; + } + .events{ + } + + .grid-container{ + height:calc(100% - 40px); + max-height:calc(100% - 40px); + display:flex; + padding:0; + .time-grid{ + .time-period{ + height:50px; + border-bottom: 1px solid var(--calendar-line) + } + } + } + + &.time-col{ + .header{ + border-left: 1px solid var(--lighterborder); + transform:translateX(-1px); + } + } +} \ No newline at end of file diff --git a/frontend/src/components/NewCalendar/MonthComponent/MonthComponent.jsx b/frontend/src/components/NewCalendar/MonthComponent/MonthComponent.jsx new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/components/NewCalendar/MonthComponent/MonthComponent.scss b/frontend/src/components/NewCalendar/MonthComponent/MonthComponent.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/components/NewCalendar/WeekComponent/WeekComponent.jsx b/frontend/src/components/NewCalendar/WeekComponent/WeekComponent.jsx new file mode 100644 index 00000000..72881ab6 --- /dev/null +++ b/frontend/src/components/NewCalendar/WeekComponent/WeekComponent.jsx @@ -0,0 +1,103 @@ +import React, { useState, useEffect } from 'react'; +import './WeekComponent.scss'; +import DayComponent from '../DayComponent/DayComponent'; + +const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +const getMonthName = (month) => { + const months = [ + "Jan", "Feb", "Mar", "April", + "May", "June", "July", "Aug", + "Sept", "Oct", "Nov", "Dec" + ]; + return months[month]; +}; + +const getDayEvents = (events, day) => { + if (events.loading || !events.data) return []; + return events.data.events.filter((event) => { + const eventDate = new Date(event.start_time); + return eventDate.getDate() === day; + }); +}; + +const getDaysArray = (startDate, weekends) => { + const startDayIndex = new Date(startDate).getDay(); + const totalDays = weekends ? 7 : 5; + const filteredDays = weekends ? daysOfWeek : daysOfWeek.slice(1, 6); + // add dummy first entry for time column + return ["Time", ...Array.from({ length: totalDays }, (_, i) => { + const dayIndex = (startDayIndex + i) % 7; + return filteredDays.includes(daysOfWeek[dayIndex]) ? daysOfWeek[dayIndex] : null; + }).filter(Boolean)]; +}; + +const getDateNumbers = (startDate, length) => { + //add 1 day to the start date + const start = new Date(startDate); + const dateNumbers = []; + for (let i = 0; i < length; i++) { + const date = new Date(start); + date.setDate(start.getDate() + i); + dateNumbers.push({ + date: date.getDate(), + month: date.getMonth() + }); + } + return dateNumbers; +}; + +function WeekComponent({ height, start, events, weekends = true, changeToDay }) { + const [days, setDays] = useState([]); + const [dateNumbers, setDateNumbers] = useState([]); + const [month, setMonth] = useState(""); + + useEffect(() => { + const correctedStart = new Date(start); + + const newDays = getDaysArray(correctedStart, weekends); + setDays(newDays); + + const newDateNumbers = getDateNumbers(correctedStart, newDays.length); + setDateNumbers(newDateNumbers); + + setMonth(getMonthName(correctedStart.getMonth())); + }, [start, weekends]); + + return ( +
+
+ {days.map((day, index) => ( + //add condition if day == "time" + day === "Time" ? +
+

+ EST +

+
+ : +
+

+ {day} {dateNumbers[index]?.date} +

+
+ + ))} +
+
+ {days.map((day, index) => ( + + ))} +
+
+ ); +} + +export default WeekComponent; diff --git a/frontend/src/components/NewCalendar/WeekComponent/WeekComponent.scss b/frontend/src/components/NewCalendar/WeekComponent/WeekComponent.scss new file mode 100644 index 00000000..9336f9a5 --- /dev/null +++ b/frontend/src/components/NewCalendar/WeekComponent/WeekComponent.scss @@ -0,0 +1,65 @@ +.week{ + --calendar-line: #F2F3F7; + --calendar-background-2: #FDFDFE; + + .calendar-header{ + display:grid; + grid-template-columns: 50px repeat(5, 1fr); + width:100%; + height:60px; + box-sizing: border-box; + border-right: 1px solid var(--lighterborder); + border-bottom: 1px solid var(--lighterborder); + border-radius: 0 10px 0 0 ; + + & > div:first-child{ + border-radius: 10px 0 0 0; + } + + & > div:last-child{ + border-radius: 0 10px 0 0 ; + + } + + .day{ + box-sizing: border-box; + display:flex; + align-items: center; + justify-content: center; + border-top: 1px solid var(--lighterborder); + border-left: 1px solid var(--lighterborder); + } + + p{ + font-family: 'Inter'; + font-size: 16px; + color: var(--text); + font-weight:500; + margin:0; + text-align: center; + display: flex; + flex-direction:column; + } + } + .content{ + grid-template-columns: 50px repeat(5, 1fr); + display: grid; + border-left: 1px solid var(--calendar-line); + max-height:100%; + overflow:hidden; + .time-col{ + min-width:50px; + width:50px; + border-bottom: 1px solid var(--calendar-line); + border-right: 1px solid var(--calendar-line); + } + } + + &.weekend{ + .calendar-header, + .content{ + grid-template-columns: 50px repeat(7, 1fr); + } + } + +} \ No newline at end of file diff --git a/frontend/src/components/Popup/Popup.jsx b/frontend/src/components/Popup/Popup.jsx index 7efb2b78..3527a8ce 100644 --- a/frontend/src/components/Popup/Popup.jsx +++ b/frontend/src/components/Popup/Popup.jsx @@ -4,10 +4,13 @@ import './Popup.scss'; // Assuming this contains your animation and styling import useOutsideClick from '../../hooks/useClickOutside'; import X from '../../assets/x.svg'; -const Popup = ({ children, isOpen, onClose, defaultStyling=true, customClassName="" }) => { +const Popup = ({ children, isOpen, onClose, defaultStyling=true, customClassName="", popout=false, waitForLoad=false}) => { const [render, setRender] = useState(isOpen); const [show, setShow] = useState(false); + const [topPosition, setTopPosition] = useState(null); + const [rightPosition, setRightPosition] = useState(null); + const ref = useRef(); useOutsideClick(ref, ()=>{ @@ -26,6 +29,18 @@ const Popup = ({ children, isOpen, onClose, defaultStyling=true, customClassName }, 100); },[render]); + useEffect(() => { + setTimeout(() => { + if(ref.current){ + const rect = ref.current.getBoundingClientRect(); + setTopPosition(rect.top); + setRightPosition(rect.right); + } + }, 300); + + }, [show, ref.current]); + + const handleClose = () => { setShow(false); setTimeout(() => { @@ -47,8 +62,9 @@ const Popup = ({ children, isOpen, onClose, defaultStyling=true, customClassName return ReactDOM.createPortal(
+ {popout && }
- + {!popout && } {renderChildrenWithClose()} {/* Render children with handleClose prop */}
, diff --git a/frontend/src/components/Popup/Popup.scss b/frontend/src/components/Popup/Popup.scss index 7e41c841..ec1dd142 100644 --- a/frontend/src/components/Popup/Popup.scss +++ b/frontend/src/components/Popup/Popup.scss @@ -12,6 +12,28 @@ opacity: 0; } + .popup-content-overlay{ + background-color: white; + padding: 20px; + border-radius: 20px; + position: absolute; + max-width:500px; + box-sizing: border-box; + display:flex; + flex-direction: column; + height:fit-content; + /* opacity: 0; */ + margin: 0 20px; + overflow:hidden; + + &.wide-content{ + max-width: 1000px; + width:95%; + + + } + } + .popup-overlay .popup-content { background-color: white; @@ -30,6 +52,33 @@ &.wide-content{ max-width: 1000px; width:95%; + .close-popup{ + background-color: var(--background); + padding:7px; + top:5px; + right:10px; + border-radius:50%; + animation: popout 0.3s forwards cubic-bezier(0.075, 0.82, 0.165, 1); + animation-delay:0.1s; + transform:translateX(-50px); + opacity:0; + } + @keyframes popout { + from { + transform:translateX(-50px); + opacity: 0; + } + to { + transform:translateX(0); + opacity: 1; + } + + } + + } + + &.oie{ + max-height:600px; } } @@ -37,12 +86,28 @@ background-color: transparent; padding: 0; } + + + .popup-overlay .popup-content.no-padding { + padding: 0; + } .popup-content .close-popup{ position: absolute; width:15px; height:15px; top:15px; right:15px; + z-index: 100; + cursor: pointer; + } + + .close-popup.popout{ + width:15px; + height:15px; + background-color: var(--background); + padding:7px; + border-radius:50%; + position: absolute; } /* Entry and Exit Animations */ diff --git a/frontend/src/components/ProfileCard/CardHeader/CardHeader.jsx b/frontend/src/components/ProfileCard/CardHeader/CardHeader.jsx index 7c231807..b64b7dbc 100644 --- a/frontend/src/components/ProfileCard/CardHeader/CardHeader.jsx +++ b/frontend/src/components/ProfileCard/CardHeader/CardHeader.jsx @@ -5,15 +5,19 @@ import defaultAvatar from "../../../assets/defaultAvatar.svg" import Badges from '../../Badges/Badges'; import '../../ProfilePicture/ProfilePicture.scss'; import GrainTexture from '../../../assets/Grain-Texture.png'; +import StudentCardGrad from '../../../assets/Gradients/StudentCardGrad.png'; +import logo from '../../../assets/Logo.svg' function CardHeader({userInfo, settings}){ console.log(userInfo); return (
-
+ {/*
{settings ? settings-icon :

study compass

} -
+
*/} + +
profile-icon diff --git a/frontend/src/components/ProfileCard/CardHeader/CardHeader.scss b/frontend/src/components/ProfileCard/CardHeader/CardHeader.scss index a843093a..5c581eda 100644 --- a/frontend/src/components/ProfileCard/CardHeader/CardHeader.scss +++ b/frontend/src/components/ProfileCard/CardHeader/CardHeader.scss @@ -10,10 +10,27 @@ border-radius: 21px; background-color: var(--background); position: relative; + overflow:hidden; +} + +.card-header .grad{ + position:absolute; + z-index:1; + top:0; + left:0; + width:60%; +} + +.card-header .logo{ + height:35px; + position: absolute; + z-index: 1; + top:10px; + right:10px; } .card-header:hover{ - box-shadow: 0px 7px 29px 0px var(--red); + // box-shadow: 0px 7px 29px 0px var(--red); } .card-header h2.watermark{ @@ -30,16 +47,17 @@ justify-content: flex-start; width: 100%; height: 100%; + z-index: 1; } .card-header .personal .pfp{ - height :95%; + height:95%; aspect-ratio: 1/1; width:fit-content; margin-left: 4%; - margin-top: 2%; + margin-top: 10%; position: relative; - height: 100px; + height: 80px; } .card-header .personal img{ @@ -68,6 +86,7 @@ flex-direction: column; align-items: flex-start; justify-content: center; + gap:5px; } .card-header p.user{ @@ -112,6 +131,7 @@ box-sizing: border-box; padding: 12px; gap: 5px; + z-index: 2; } .card-header .stats p{ diff --git a/frontend/src/components/ProfilePicture/ProfilePicture.jsx b/frontend/src/components/ProfilePicture/ProfilePicture.jsx index d5da39e1..9e91c8bb 100644 --- a/frontend/src/components/ProfilePicture/ProfilePicture.jsx +++ b/frontend/src/components/ProfilePicture/ProfilePicture.jsx @@ -14,6 +14,8 @@ import Stats from '../../assets/Icons/Stats.svg'; import useOutsideClick from '../../hooks/useClickOutside'; import { useNotification } from '../../NotificationContext'; +import {Icon} from '@iconify-icon/react'; +import RPI from '../../assets/Icons/RPI.svg'; import {Link} from 'react-router-dom' @@ -53,6 +55,7 @@ function ProfilePicture(){

+

GENERAL

profile @@ -65,26 +68,62 @@ function ProfilePicture(){

Settings

-
- -
- guide -

Guide

+ +
+ settings +

Create an Org

- { - user && user.admin && + {user && (user.roles.includes('admin')||user.roles.includes('oie')) && <>
- +

ADMINISTRATION

+ + } + + { + user && user.roles.includes('admin') && + <>
- guide -

Admin

+ +

Analytics

+
+ + + } + { + user && user.roles.includes('oie') && + <> + +
+ log out +

OIE Admin

} + { + user && user.clubAssociations.length > 0 && + <> +
+

ORGS

+ {user.clubAssociations.map( + (org)=>{ + const url = `/club-dashboard/${org.org_name}` + return( + +
+ +

{org.org_name}

+
+ + ) + } + )} + + + }
diff --git a/frontend/src/components/ProfilePicture/ProfilePicture.scss b/frontend/src/components/ProfilePicture/ProfilePicture.scss index 5ec18ea8..a0a0b6ba 100644 --- a/frontend/src/components/ProfilePicture/ProfilePicture.scss +++ b/frontend/src/components/ProfilePicture/ProfilePicture.scss @@ -83,6 +83,14 @@ z-index: 1000; } +.popup p.section{ + font-size:10px; + color: var(--darkborder); + font-weight: 500; + margin:3px 0 0 10px; + +} + .popup-content h3{ transform:translateY(-2px); flex-grow:1; @@ -122,6 +130,11 @@ hr{ margin: 0px 5px; } +.menu-item iconify-icon{ + color:var(--darkborder); + font-size:22px; +} + .menu-item .icon{ height:22px; width:22px; diff --git a/frontend/src/components/Report/Report.scss b/frontend/src/components/Report/Report.scss index 9c619e9d..e201ee6c 100644 --- a/frontend/src/components/Report/Report.scss +++ b/frontend/src/components/Report/Report.scss @@ -25,7 +25,7 @@ margin-bottom: 15px; } -textarea{ +.whole_page textarea{ padding: 8px 20px; background-color: var(--lighter); border: none; diff --git a/frontend/src/components/StudyPreferences/StudyPreferences.jsx b/frontend/src/components/StudyPreferences/StudyPreferences.jsx index 34f98318..38e1f792 100644 --- a/frontend/src/components/StudyPreferences/StudyPreferences.jsx +++ b/frontend/src/components/StudyPreferences/StudyPreferences.jsx @@ -101,8 +101,6 @@ const StudyPreferences = ({ settingsRightSide, width, handleBackClick, userInfo disabled={!active && (sliderValue == initialSliderValue)} > save
- -
diff --git a/frontend/src/components/Switch/Switch.jsx b/frontend/src/components/Switch/Switch.jsx index f4d50d75..66523e3e 100644 --- a/frontend/src/components/Switch/Switch.jsx +++ b/frontend/src/components/Switch/Switch.jsx @@ -1,7 +1,7 @@ import React, { useState, useRef, useEffect } from 'react'; import './Switch.css'; -function Switch({ options, onChange }) { +function Switch({ options, onChange, selectedPass, setSelectedPass}) { const [selected, setSelected] = useState(0); const optionRefs = useRef([]); const containerRef = useRef(null); @@ -22,15 +22,21 @@ function Switch({ options, onChange }) { } }, [selected]); + useEffect(() => { + setSelected(selectedPass); + }, [selectedPass]); + const handleClick = (index) => { if(options.length === 2){ if(index === selected){ setSelected(selected === 0 ? 1 : 0); onChange(selected === 0 ? 1 : 0); + setSelectedPass(selected === 0 ? 1 : 0); return; } } setSelected(index); + setSelectedPass(index); onChange(index); if (optionRefs.current[index] && containerRef.current) { const containerRect = containerRef.current.getBoundingClientRect(); diff --git a/frontend/src/hooks/useFetch.js b/frontend/src/hooks/useFetch.js new file mode 100644 index 00000000..3a94d6f7 --- /dev/null +++ b/frontend/src/hooks/useFetch.js @@ -0,0 +1,32 @@ +import { useState, useEffect, useCallback } from "react"; +import axios from "axios"; + +export const useFetch = (url, options = { method: "GET", data: null }) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchData = useCallback(async () => { + setLoading(true); + setError(null); + try { + const response = await axios({ + url, + method: options.method || "GET", + data: options.data || null, + headers: options.headers || { Authorization: `Bearer ${localStorage.getItem("token")}` }, + }); + setData(response.data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }, [url, options.method, options.data, options.headers]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { data, loading, error, refetch: fetchData }; +}; diff --git a/frontend/src/index.js b/frontend/src/index.js index 7400e9bc..d2c34ea4 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -6,9 +6,7 @@ import reportWebVitals from './reportWebVitals'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - - ); // If you want to start measuring performance in your app, pass a function diff --git a/frontend/src/pages/Club/Club.jsx b/frontend/src/pages/Club/Club.jsx new file mode 100644 index 00000000..19fca93b --- /dev/null +++ b/frontend/src/pages/Club/Club.jsx @@ -0,0 +1,39 @@ +import React, {useEffect, useState} from 'react'; +import ClubDisplay from './ClubDisplay/ClubDisplay'; +import { useParams } from 'react-router-dom'; +import { useFetch } from '../../hooks/useFetch'; + +const Club = () => { + const orgName = useParams().name; + const orgData = useFetch(`/get-org-by-name/${orgName}`); + + const org = { + "_id": { + "$oid": "675ce4871958af1a0199505e" + }, + "org_name": "Study Compass Devs", + "org_profile_image": "/Logo.svg", + "org_description": "asdasd", + "positions": [ + "chair", + "officer", + "member" + ], + "weekly_meeting": null, + "owner": { + "$oid": "65f474445dca7aca4fb5acaf" + }, + "__v": 0 + } + + + return ( + <> + { + !orgData.loading && + } + + ); +}; + +export default Club; diff --git a/frontend/src/pages/Club/ClubDisplay/Club.scss b/frontend/src/pages/Club/ClubDisplay/Club.scss new file mode 100644 index 00000000..3c9e53fc --- /dev/null +++ b/frontend/src/pages/Club/ClubDisplay/Club.scss @@ -0,0 +1,203 @@ +.club-page { + font-family: satoshi; +} + + +.club-content{ + display: flex; + flex-direction: column; + align-items: center; + width: calc(100% - 200px); + height: 100%; + max-height: calc(100% - 80px); + max-width: 1400px; +} + + +.top-header-box { + display: flex; + align-items: center; + width: 100%; + padding: 20px; + background-color: #9d8484; + border-radius: 10px; + color: #fff; + box-sizing: border-box; + +} + +.club-info{ + width:100%; +} + +.club-logo { + display: flex; + justify-content: center; + align-items: center; + width: 120px; + height: 120px; + border-radius: 50%; + margin-right: 20px; + transform: translate(80px) img; + + img{ + width: 120%; + // height: 100px; + padding-top: 185px; + } + +} + +.club-header { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; + position: relative; + padding-left: 180px; +} + +.name { + font-size: 24px; + font-weight: bold; + margin: 0; +} + +.status { + color: #ff5c5c; + font-weight: bold; + font-size: 12px; + padding-left: 10px; + +} + +.description { + margin: 10px 0; + padding-left: 180px; +} + +.stats { + display: flex; + flex-direction: row; + position: relative; + margin-top: 40px; + font-size: 12px; + + img{ + height: 15px; + padding-right: 10px; + } +} + +.actions { + display: flex; + gap: 10px; + margin: 10px 0; + position: relative; + + button{ + padding: 6px 30px; + border: none; + border-radius: 5px; + cursor: pointer; + background-color: var(--red); + font-family: satoshi; + color: white; + } + +} + +.event-info{ + display: flex; + align-items: center; + width: calc(100%); + height: 100%; + max-height: calc(100%); + max-width: 1400px; +} + +.upcoming{ + // height: 100px; + // width: 100px; +} + +// .meeting-schedule{ +// display: flex; +// flex-direction: column; +// width: calc(100%); +// height: 100%; +// max-height: calc(100%); +// max-width: 1400px; + +// h1{ +// font: satoshi; +// font-size: 18px; +// } + +// p{ +// font: satoshi; +// font-size: 15px; +// } +// } + +.meeting-schedule{ + display: flex; + flex-direction: column; + width: calc(100%); + height: 100%; + max-height: calc(100%); + max-width: 1400px; + + h3{ + font-size: 18px; + } + + .meeting-card{ + display: flex; + flex-direction: column; + padding: 15px; + border-radius: 10px; + box-shadow: var(--shadow); + border: 1px solid var(--lightborder); + + .title{ + display: flex; + flex-direction: center; + gap: 10px; + + .logo{ + width: 50px; + height: 50px; + border-radius: 50%; + + } + + } + .info{ + display: flex; + flex-direction: column; + padding-left: 11px; + margin-left: 47px; + .item{ + display: flex; + flex-direction: center; + gap: 5px; + } + h4{ + font-size: 12px; + } + p{ + color: #666; + font-size: 10px; + + } + img{ + width: 20px; + } + + } + img{ + width: 10%; + } + } +} diff --git a/frontend/src/pages/Club/ClubDisplay/ClubDisplay.jsx b/frontend/src/pages/Club/ClubDisplay/ClubDisplay.jsx new file mode 100644 index 00000000..9ce6604f --- /dev/null +++ b/frontend/src/pages/Club/ClubDisplay/ClubDisplay.jsx @@ -0,0 +1,83 @@ +import React from 'react'; +import Header from '../../../components/Header/Header'; +import rpiLogo from "../../../assets/Icons/rpiLogo.svg"; +import person from "../../../assets/Icons/Person.svg"; +import calendar from "../../../assets/Icons/Calendar.svg"; +import locate from "../../../assets/Icons/Locate.svg"; +import './Club.scss'; + +const ClubDisplay = ({org}) => { + console.log(org); + return ( +
+
+
+ +
+
+ +
+
+ +
+ +
+

{org.overview.org_name}

+
Union Recognized
+
+ +

+ {org.overview.org_description} +

+

+ + 250 followers • 50 members +

+
+ + +
+ +
+ +
+
+ +
+ +
+ + {/*
+

meetings schedule

+
+

YDSA Weekly GBM

+ +
+ +
*/} + +
+

Meetings Schedule

+
+
+ +

YDSA Weekly GBM

+
+
+
+ +

Weekly on Thursday at 5:00

+ +

Phalanx

+
+ {/*

Next Meeting: Thursday 10/24

*/} + +
+
+
+
+
+ ); +}; + +export default ClubDisplay; diff --git a/frontend/src/pages/ClubDash/ClubDash.jsx b/frontend/src/pages/ClubDash/ClubDash.jsx new file mode 100644 index 00000000..8acb2ebb --- /dev/null +++ b/frontend/src/pages/ClubDash/ClubDash.jsx @@ -0,0 +1,131 @@ +import React, { useEffect, useState} from 'react'; +import './ClubDash.scss'; +import useAuth from '../../hooks/useAuth'; +import { useNavigate, useParams } from 'react-router-dom'; +import Dashboard from '../../assets/Icons/Dashboard.svg'; +import logo from '../../assets/red_logo.svg'; +import { getAllEvents } from '../../components/EventsViewer/EventHelpers'; +import { useNotification } from '../../NotificationContext'; +import {Icon} from '@iconify-icon/react'; +import Dash from './Dash/Dash'; +import Members from './Members/Members'; +import {useFetch} from '../../hooks/useFetch'; +import { use } from 'react'; + +function ClubDash(){ + const clubId = useParams().id; + const [expanded, setExpanded] = useState(false); + const [expandedClass, setExpandedClass] = useState(""); + const{isAuthenticated, isAuthenticating, user} = useAuth(); + const navigate = useNavigate(); + const [userInfo, setUserInfo] = useState(null); + + const [currentPage, setCurrentPage] = useState('dash'); + const { addNotification } = useNotification(); + + const orgData = useFetch(`/get-org-by-name/${clubId}`); + + useEffect(()=>{ + if(isAuthenticating){ + return; + } + if(!isAuthenticated){ + navigate('/'); + } + if(!user){ + return; + } else { + setUserInfo(user); + } + + },[isAuthenticating, isAuthenticated, user]); + + useEffect(()=>{ + if(orgData){ + if(orgData.error){ + addNotification({title: "Error", message: orgData.error, type: "error"}); + navigate('/'); + } + if(orgData.data){ + console.log(orgData.data); + } + } + } + ,[orgData]); + + useEffect(()=>{ + if(!userInfo){ + return; + } + if(userInfo.clubAssociations){ + if(userInfo.clubAssociations.find(club => club.org_name === clubId)){ + return; + } else { + addNotification({title: "Unauthorized", message: "you are not authorized to manage this club", type: "error"}); + navigate('/'); + } + } + },[userInfo]); + + const onExpand = () => { + if(expanded){ + setExpandedClass("minimized"); + setTimeout(() => { + setExpanded(false); + }, 200); + } else { + setExpanded(true); + setTimeout(() => { + setExpandedClass("maximized"); + }, 200); + + } + } + + const openMembers = () =>{ + setCurrentPage('members'); + } + + if(orgData.loading){ + return null; + } + + return ( +
+
+
+ +

club admin

+
+ +
+
+ { + currentPage === "dash" && + + } + { + currentPage === 'members' && + + } +
+ +
+
+
+ ) +} + + +export default ClubDash; \ No newline at end of file diff --git a/frontend/src/pages/ClubDash/ClubDash.scss b/frontend/src/pages/ClubDash/ClubDash.scss new file mode 100644 index 00000000..6da98e67 --- /dev/null +++ b/frontend/src/pages/ClubDash/ClubDash.scss @@ -0,0 +1,170 @@ +:root{ + --club-background: #EFEFEF +} + + +.club-dash{ + overflow: hidden; + height:100vh; + width:100vw; + display:flex; + background-color: var(--club-background); + z-index: 0; + .dash-left{ + width:220px; + display: flex; + flex-direction: column; + align-items: center; + padding-top:20px; + box-sizing: border-box; + animation: show 1s forwards; + .logo{ + position: relative; + width:85%; + img{ + width:100%; + } + .club-badge{ + font-size:12px; + background-color: var(--red); + width:fit-content; + padding:0px 8px; + border-radius: 7px; + position: absolute; + left:25%; + bottom:0; + p{ + color: var(--background); + font-family: 'Inter'; + font-weight:500; + } + } + } + .nav{ + width:100%; + display: flex; + padding:0 5px; + box-sizing: border-box; + margin-top:20px; + flex-direction: column; + + ul{ + list-style: none; + box-sizing: border-box; + padding: 0 8px; + li{ + display:flex; + align-items: center; + justify-content: flex-start; + gap:10px; + padding:5px 10px; + cursor: pointer; + &.selected{ + background-color: var(--background); + border-radius: 10px; + border: 1px solid var(--lightborder) + } + + img{ + width:25px; + height:25px + } + + p{ + font-family: 'Inter'; + font-weight: 600; + color:var(--text); + font-size: 14px; + } + + } + } + } + &.hidden{ + animation: hide 1s forwards; + } + } + .dash-right{ + position: relative; + flex-grow: 1; + display:flex; + background-color: var(--background); + margin: 10px 10px 10px 0; + border-radius: 15px; + border: 1px solid var(--lightborder); + box-shadow: var(--shadow); + + .expand{ + position: absolute; + bottom:20px; + right:20px; + background-color: var(--background); + border-radius: 50%; + padding: 5px; + box-shadow: var(--shadow); + display:flex; + cursor: pointer; + box-sizing: border-box; + border: 1px solid var(--lightborder); + font-size:20px; + color: var(--text); + &:hover{ + font-size:22px; + } + z-index: 1; + + } + + &::after{ + content: ''; + position: absolute; + width:100%; + height:100%; + background-color: var(--background); + top:0; + left:0; + z-index: 0; + border-radius: 20px; + transition: all 1s; + } + &.minimized::after{ + transform:scale(1); + } + &.maximized::after{ + transform:scale(1.2); + } + } +} + + +/* Showing animation: width first, then opacity */ +@keyframes show { + 0% { + width: 10px; + opacity: 0; + } + 80% { + width: 200px; + opacity: 0; + } + 100% { + width: 200px; + opacity: 1; + } + } + + /* Hiding animation: opacity first, then width */ + @keyframes hide { + 0% { + width: 200px; + opacity: 1; + } + 20% { + width: 200px; + opacity: 0; + } + 100% { + width: 10px; + opacity: 0; + } + } diff --git a/frontend/src/pages/ClubDash/ClubEventsComponents/CreateEvent/CreateEvent.jsx b/frontend/src/pages/ClubDash/ClubEventsComponents/CreateEvent/CreateEvent.jsx new file mode 100644 index 00000000..9892ebf7 --- /dev/null +++ b/frontend/src/pages/ClubDash/ClubEventsComponents/CreateEvent/CreateEvent.jsx @@ -0,0 +1,28 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import './CreateEvent.scss'; +import {Icon} from '@iconify-icon/react'; +import GradientButtonCover from '../../../../../assets/GradientButtonCover.png'; + +function CreateEvent(){ + const navigate = useNavigate(); + + const handleEventClick = () => { + navigate(`/create-event`); + } + + return( +
+
+ +

create event

+
+
+ +
+
+ ); + +} + +export default CreateEvent; \ No newline at end of file diff --git a/frontend/src/pages/ClubDash/ClubEventsComponents/CreateEvent/CreateEvent.scss b/frontend/src/pages/ClubDash/ClubEventsComponents/CreateEvent/CreateEvent.scss new file mode 100644 index 00000000..87742893 --- /dev/null +++ b/frontend/src/pages/ClubDash/ClubEventsComponents/CreateEvent/CreateEvent.scss @@ -0,0 +1,103 @@ +.event-component.create{ + display:flex; + align-items: center; + justify-content: center; + padding: 20px; + position: relative; + overflow: show; + z-index: 1; + animation: glint 3s infinite linear; + + + &::after{ + transition: all 0.5s; + content: ''; + background-image: radial-gradient( circle farthest-corner at 10% 20%, #FD1E86 17.8%, #FCD38C 100.2% ); + filter: blur(20px); + width: 100%; + height: 100%; + z-index: -1; + position: absolute; + left: 0; + top: 0; + opacity: 0; + } + + &:hover{ + border:1px solid transparent; + } + + &:hover::after{ + opacity:1; + } + + .gradient-cover{ + width:100%; + height:100%; + position: absolute; + overflow: hidden; + border-radius: 9px; + + img{ + top:0; + width:130%; + left:0; + z-index: 1; + opacity: 0; + transition: all 0.5s; + } + } + + &:hover .gradient-cover img{ + top:0; + width:130%; + left:0; + opacity: 1; + } + + .info{ + display:flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size:20px; + color: var(--text); + z-index: 2; + transition: all 0.5s; + h1{ + color:var(--text); + transition: all 0.5s; + + } + } + + &:hover .info{ + color: var(--background); + h1{ + color: var(--background); + } + } +} + +@keyframes glint { + 0% { + box-shadow: 0 0 10px rgba(255, 255, 255, 0.8), 0 0 15px rgba(255, 255, 255, 0.8), + 0 0 20px rgba(255, 255, 255, 0.8); + } + 25% { + box-shadow: 10px 0 20px rgba(255, 255, 255, 0.8), 15px 0 25px rgba(255, 255, 255, 0.8), + 20px 0 30px rgba(255, 255, 255, 0.8); + } + 50% { + box-shadow: 0 10px 20px rgba(255, 255, 255, 0.8), 0 15px 25px rgba(255, 255, 255, 0.8), + 0 20px 30px rgba(255, 255, 255, 0.8); + } + 75% { + box-shadow: -10px 0 20px rgba(255, 255, 255, 0.8), -15px 0 25px rgba(255, 255, 255, 0.8), + -20px 0 30px rgba(255, 255, 255, 0.8); + } + 100% { + box-shadow: 0 -10px 20px rgba(255, 255, 255, 0.8), 0 -15px 25px rgba(255, 255, 255, 0.8), + 0 -20px 30px rgba(255, 255, 255, 0.8); + } + } \ No newline at end of file diff --git a/frontend/src/pages/ClubDash/ClubEventsComponents/Event/ClubEvent.jsx b/frontend/src/pages/ClubDash/ClubEventsComponents/Event/ClubEvent.jsx new file mode 100644 index 00000000..38f3b784 --- /dev/null +++ b/frontend/src/pages/ClubDash/ClubEventsComponents/Event/ClubEvent.jsx @@ -0,0 +1,55 @@ +import React, { useState, useEffect } from 'react'; +import './ClubEvent.scss'; +import { useNavigate } from 'react-router-dom'; +import Popup from '../../../../components/Popup/Popup'; +import ClubFullEvent from '../FullEvent/ClubFullEvent'; +import { Icon } from '@iconify-icon/react/dist/iconify.mjs'; +import defaultAvatar from '../../../../assets/defaultAvatar.svg'; + +function ClubEvent({event}){ + const [popupOpen, setPopupOpen] = useState(false); + const navigate = useNavigate(); + + const handleEventClick = (event) => { + setPopupOpen(true); + } + + const onPopupClose = () => { + setPopupOpen(false); + } + + const date = new Date(event.date); + + + return( +
+ + + +
+

{event.name}

+ {/*

{event.location }

*/} + {/* display date in day of the week, month/day */} +
+ +

{event.user_id.name}

+
+
+ +

{date.toLocaleString('default', {weekday: 'long'})} {date.toLocaleString('default', {month: 'numeric'})}/{date.getDate()}

+ +

{event.location}

+
+ {/* time */} + {/*

{date.toLocaleString('default', {hour: 'numeric', minute: 'numeric', hour12: true})}

*/} +
+ +
+ ); + +} + +export default ClubEvent; \ No newline at end of file diff --git a/frontend/src/pages/ClubDash/ClubEventsComponents/Event/ClubEvent.scss b/frontend/src/pages/ClubDash/ClubEventsComponents/Event/ClubEvent.scss new file mode 100644 index 00000000..76b1a56f --- /dev/null +++ b/frontend/src/pages/ClubDash/ClubEventsComponents/Event/ClubEvent.scss @@ -0,0 +1,76 @@ +.club-event-component{ + display:flex; + flex-direction: column; + width:250px; + gap:10px; + padding:10px; + box-sizing: border-box; + box-shadow:var(--shadow); + background-color: var(--background); + border-radius: 10px; + border: 1px solid var(--lightborder); + justify-content: space-between; + + img{ + width:calc(100%); + border-radius: 5px; + box-shadow:0px 7px 29px 0px rgba(150, 150, 157, 0.1); + + } + .info{ + display:flex; + flex-direction: column; + gap:5px; + h1{ + font-size: 15px; + font-weight: 600; + margin: 0; + color: var(--text); + } + + + .row{ + display:flex; + gap:5px; + align-items: center; + font-size:18px; + color: var(--text); + .user-name{ + font-weight: 700; + font-size:13px; + } + img{ + width: 20px; + height: 20px; + } + p{ + font-size: 12px; + font-weight: 500; + margin: 0; + color: var(--text); + font-family: 'Inter'; + } + } + } + .button{ + height:fit-content; + width:fit-content; + padding:5px 15px; + font-size: 18px; + color:var(--text); + background-color: var(--club-background); + pointer-events: all; + iconify-icon{ + transition: transform 0.2s; + } + p{ + font-size:13px; + font-weight: 700; + margin-right:10px; + } + &:hover iconify-icon{ + transform: scale(1.2); + } + } + +} \ No newline at end of file diff --git a/frontend/src/pages/ClubDash/ClubEventsComponents/FullEvent/ClubFullEvent.jsx b/frontend/src/pages/ClubDash/ClubEventsComponents/FullEvent/ClubFullEvent.jsx new file mode 100644 index 00000000..9d18abdf --- /dev/null +++ b/frontend/src/pages/ClubDash/ClubEventsComponents/FullEvent/ClubFullEvent.jsx @@ -0,0 +1,35 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import './ClubFullEvent.scss'; +import {Icon} from '@iconify-icon/react'; +import StarGradient from '../../../../assets/StarGradient.png'; + +function ClubFullEvent({ event }){ + const navigate = useNavigate(); + + const handleEventClick = () => { + navigate(`/create-event`); + } + const date = new Date(event.date); + + return( +
+
+ +
+
+

{event.name}

+ {/*

{event.location }

*/} + {/* display date in day of the week, month/day */} +

{date.toLocaleString('default', {weekday: 'long'})}, {date.toLocaleString('default', {month: 'long'})} {date.getDate()}

+ {/* time */} +

{date.toLocaleString('default', {hour: 'numeric', minute: 'numeric', hour12: true})}

+ +
+ +
+ ); + +} + +export default ClubFullEvent; \ No newline at end of file diff --git a/frontend/src/pages/ClubDash/ClubEventsComponents/FullEvent/ClubFullEvent.scss b/frontend/src/pages/ClubDash/ClubEventsComponents/FullEvent/ClubFullEvent.scss new file mode 100644 index 00000000..ac266794 --- /dev/null +++ b/frontend/src/pages/ClubDash/ClubEventsComponents/FullEvent/ClubFullEvent.scss @@ -0,0 +1,26 @@ +.full-event{ + display:flex; + width:100%; + height:100%; + gap:20px; + .image{ + max-width: 35%; + img{ + width:100%; + border-radius: 10px; + } + } + .content{ + flex-grow: 1; + padding-top:5px; + box-sizing: border-box; + h1{ + font-size: 25px; + } + } + .gradient{ + position: absolute; + bottom:0; + right:0; + } +} \ No newline at end of file diff --git a/frontend/src/pages/ClubDash/Dash/Dash.jsx b/frontend/src/pages/ClubDash/Dash/Dash.jsx new file mode 100644 index 00000000..ea285cd7 --- /dev/null +++ b/frontend/src/pages/ClubDash/Dash/Dash.jsx @@ -0,0 +1,71 @@ +import React, { useEffect, useState } from 'react'; +import './Dash.scss'; +import OIEGradient from '../../../assets/ClubGradient.png'; +import { getAllEvents } from '../../../components/EventsViewer/EventHelpers'; +import clubEvent from '../ClubEventsComponents/Event/ClubEvent'; +import people from '../../../assets/people.svg' + +function Dash({expandedClass, openMembers, clubName}){ + + const [events, setEvents] = useState([]); + useEffect(() => { + const fetchEvents = async () => { + try{ + const allEvents = await getAllEvents(); + //sort by date + allEvents.sort((a, b) => { + return new Date(a.date) - new Date(b.date); + }); + allEvents.reverse(); + //add dummy first element + setEvents(allEvents); + console.log(allEvents); + } catch (error){ + console.log("Failed to fetch events", error); + } + } + fetchEvents(); + }, []); + + + return ( +
+
+ +

Org Dashboard

+
+
+
+

manage membership

+
+

200 members

+

8 officers

+
+
+
+

meetings coming up

+
+

Random Student Event

+
+ +
+ +
+
+
+

quick actions

+
+
+
+
+ +
+ + + + + ) +} + + +export default Dash; \ No newline at end of file diff --git a/frontend/src/pages/ClubDash/Dash/Dash.scss b/frontend/src/pages/ClubDash/Dash/Dash.scss new file mode 100644 index 00000000..9ec56ff5 --- /dev/null +++ b/frontend/src/pages/ClubDash/Dash/Dash.scss @@ -0,0 +1,87 @@ +.dash{ + height: 100%; + width:100%; + padding:20px; + box-sizing: border-box; + z-index: 1; + display: flex; + flex-direction: column; + gap:30px; + transition: padding 1s ease-in-out; + + &.maximized{ + padding:5px; + } + + header.header{ + height:110px; + position: relative; + border-radius: 13px; + overflow: hidden; + display:flex; + align-items: center; + h1{ + z-index: 2; + padding-left:13%; + font-size: 30px; + } + img{ + position: absolute; + top:0; + left:0; + height:100%; + } + } + .row{ + display: flex; + flex-direction: row; + gap:10px; + .column{ + display: flex; + flex-direction: column; + } + h1{ + font-size: 20px; + margin-left:10px; + margin-bottom:5px; + } + .content{ + display:flex; + border-radius: 10px; + box-sizing: border-box; + border: 1px solid var(--lighterborder); + box-shadow: var(--shadow); + align-items: center; + align-items: center; + justify-content: center; + height:100%; + &.meeting{ + } + h2{ + display: flex; + flex-direction: column; + font-size: 14px; + align-items: center; + gap: 5px; + img{ + height: 22px; + } + button{ + border-radius: 7px; + background-color: var(--light); + width: 100px; + font-weight:600; + font-family: 'Inter'; + padding: 3px 13px; + color: var(--text); + cursor: pointer; + } + } + + &.membership{ + padding: 10px 30px; + gap: 40px; + } + } + } +} \ No newline at end of file diff --git a/frontend/src/pages/ClubDash/Members/Members.jsx b/frontend/src/pages/ClubDash/Members/Members.jsx new file mode 100644 index 00000000..33baa3ff --- /dev/null +++ b/frontend/src/pages/ClubDash/Members/Members.jsx @@ -0,0 +1,87 @@ +import React, { useEffect, useState } from 'react'; +import './Members.scss'; +import profile from '../../../assets/defaultAvatar.svg'; +import CardHeader from '../../../components/ProfileCard/CardHeader/CardHeader'; + +function Members({expandedClass, people, positions}){ + const [showMembers, setShowMembers] = useState(0); + const [selectedMember, setSelectedMember] = useState(0); + const [officers, setOfficers] = useState([]); + const [members, setMembers] = useState([]); + + useEffect(()=>{ + if(people){ + const officers = people.filter(member => member.status !== positions.length-1); + const members = people.filter(member => member.status === positions.length-1); + setOfficers(officers); + setMembers(members); + } + },[people]); + + useEffect(()=>{ + setSelectedMember(0); + }, [showMembers]); + + if(!people){ + return null; + } + return ( +
+
+

Membership Managment

+
+
+
+

setShowMembers(0)}>members

+

setShowMembers(1)}>officers

+
+
+
+ {members.map((member, index) => { + const user = member.user_id; + return ( +
+ +
+

{user.name}

+

{user.email}

+

{positions[member.status]}

+
+
+ ) + })} + +
+
+ {officers.map((member, index) => { + const user = member.user_id; + return ( +
setSelectedMember(index)}> + +
+

{user.name}

+

{user.email}

+

{positions[member.status]}

+
+
+ ) + })} +
+
+
+
+ {showMembers === 0 ? + members[selectedMember] && + : + officers[selectedMember] && + } +
+
+ +
+
+ ) +} + + +export default Members; diff --git a/frontend/src/pages/ClubDash/Members/Members.scss b/frontend/src/pages/ClubDash/Members/Members.scss new file mode 100644 index 00000000..cced3d95 --- /dev/null +++ b/frontend/src/pages/ClubDash/Members/Members.scss @@ -0,0 +1,120 @@ +.members{ + display: flex; + flex-direction: column; + height:100%; + .content{ + flex-grow:1; + display: flex; + gap: 20px; + .member-col{ + border-radius: 10px; + box-sizing: border-box; + border: 1px solid var(--lightborder); + width: 300px; + height:100%; + .topbox{ + display: flex; + gap: 15px; + justify-content: flex-start; + align-items: center; + padding: 8px 10px; + border-bottom: 1px solid var(--lighterborder); + height:45px; + box-sizing: border-box; + + h2{ + cursor: pointer; + font-size: 15px; + //color: white; + font-family: Inter, sans-serif; + background-color: var(--background); + display: flex; + align-items: center; + margin: 0; + padding: 5px 20px; + border-radius: 8px; + transition:all 0.3s; + &:hover{ + filter: brightness(0.9); + } + &.selected{ + background-color: var(--red); + color: white; + } + } + } + .members-list{ + display: flex; + height:calc(100% - 45px); + box-sizing: border-box; + overflow-x: hidden; + width:300px; + .members, + .officers{ + flex-direction: column; + overflow-y:auto; + min-width:300px; + // transform:translateX(-100%); + margin:0; + transition: all 0.5s; + &.show{ + transform:translateX(-100%); + } + } + .list{ + box-sizing: border-box; + border-bottom: 1px solid var(--lighterborder); + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + padding: 15px; + gap: 10px; + padding-left:20px; + position: relative; + cursor:pointer; + + .box{ + display: flex; + flex-direction: column; + h3{ + margin: 0px; + font-size: 15px; + } + h4{ + color: var(--darkborder); + margin: 0px; + font-size: 12px; + } + + + } + img{ + height: 30px; + } + &::after{ + content:''; + position: absolute; + width:4px; + height: 80%; + background-color: var(--white); + left:0; + top:10%; + border-radius: 0 3px 3px 0; + transition: all 0.3s + } + &.selected{ + &::after{ + background-color: var(--red); + } + } + } + } + + } + .col{ + height:fit-content; + width:350px; + } + } +} \ No newline at end of file diff --git a/frontend/src/pages/CreateEvent/CreateEvent.jsx b/frontend/src/pages/CreateEvent/CreateEvent.jsx index 2e00b211..cf8b5aac 100644 --- a/frontend/src/pages/CreateEvent/CreateEvent.jsx +++ b/frontend/src/pages/CreateEvent/CreateEvent.jsx @@ -9,11 +9,40 @@ import GenInfo from '../../components/CreateEvent/GenInfo/GenInfo'; import Review from '../../components/CreateEvent/Review/Review'; import { useNotification } from '../../NotificationContext'; import { createEvent } from './CreateEventHelpers'; +import { useNavigate } from 'react-router-dom'; +import useAuth from '../../hooks/useAuth'; +import { Icon } from '@iconify-icon/react/dist/iconify.mjs'; +import defaultAvatar from '../../assets/defaultAvatar.svg' function CreateEvent(){ const [step, setStep] = useState(0); const [info, setInfo] = useState({}); const [finishedStep, setFinishedStep] = useState(0); + const {isAuthenticated, isAuthenticating, user} = useAuth(); + const [alias, setAlias] = useState(null); + const navigate = useNavigate(); + const [showDrop, setShowDrop] = useState(false); + + useEffect(()=>{ + if(isAuthenticating){ + return; + } + if(!isAuthenticated){ + navigate('/'); + } + if(!user){ + return; + } + if(!(user.roles.includes('oie') || user.roles.includes('admin') || user.roles.includes('developer'))){ + navigate('/'); + } + setAlias({ + img: user.pfp ? user.pfp : defaultAvatar, + text: user.username, + id: user._id, + type: 'user' + }) + }, [isAuthenticating, isAuthenticated, user]); const {addNotification} = useNotification(); @@ -44,22 +73,24 @@ function CreateEvent(){ } const onSubmit = async () => { - console.log(info); const location1 = info.location; console.log(location1); const formattedInfo = { ...info, - location : location1[0], - classroomId : location1[1], image:null } const response = await createEvent(formattedInfo); if(response){ - addNotification({title: "Event created", message: "Your event has been created", type: "success"}); + // addNotification({title: "Event created", message: "Your event has been created", type: "success"}); + } else { addNotification({title: "Failed to create event", message: "An error occurred while creating your event", type: "error"}); } + } + const onSelectAlias = (alias) => { + setAlias(alias); + setShowDrop(false); } return( @@ -70,6 +101,42 @@ function CreateEvent(){

create event

+
+

as

+
+
setShowDrop(!showDrop)}> + { + alias && +
+ +

{alias.text}

+
+ } + +
+ { + showDrop && +
+ {user && +
onSelectAlias({img: user.pfp ? user.pfp : defaultAvatar, text: user.username, id: user._id, type: 'user'})}> + +

{user.username}

+
+ } + { + user && user.clubAssociations && user.clubAssociations.map((org)=>{ + return( +
onSelectAlias({img: org.org_profile_image, text: org.org_name, id: org._id, type: "club"})}> + +

{org.org_name}

+
+ ) + }) + } +
+ } +
+
{handleSwitch(0)}}> diff --git a/frontend/src/pages/CreateEvent/CreateEvent.scss b/frontend/src/pages/CreateEvent/CreateEvent.scss index dd9b2023..344406dc 100644 --- a/frontend/src/pages/CreateEvent/CreateEvent.scss +++ b/frontend/src/pages/CreateEvent/CreateEvent.scss @@ -22,21 +22,113 @@ max-height: min(100%, 1000px); overflow:hidden; position: relative; - box-shadow: inset 0px 0px 0px 1px var(--lightborder),var(--shadow); + // box-shadow: inset 0px 0px 0px 1px var(--lightborder),var(--shadow); + border:1px solid var(--lighterborder); + box-shadow: var(--shadow); flex-grow: 1; .create-steps{ - height: calc(100% - 20px); + height: 100%; width:180px; min-width: 180px; background-color: var(--lighter); - margin: 10px; - border-radius: 15px; + border-radius: 14px 0 0 14px; + border-right: 1px solid var(--lighterborder); h1{ font-size:23px; } .create-header{ box-sizing: border-box; padding: 16px 16px; + .alias{ + display:flex; + gap: 10px; + p{ + font-weight:500; + } + .choice-container{ + display: flex; + flex-direction: row; + align-items: center; + flex-grow:1; + position: relative; + .choose{ + cursor: pointer; + display: flex; + flex-direction: row; + align-items: center; + flex-grow:1; + position:relative; + background-color: var(--background); + padding:3px 10px; + box-sizing: border-box; + border-radius: 5px; + border:1px solid var(--lighterborder); + iconify-icon{ + position:absolute; + right:5px; + cursor:pointer; + } + .choice{ + display: flex; + gap: 5px; + img{ + height:20px; + } + p{ + -webkit-user-select: none; + user-select: none; + display: -webkit-box; /* Establishes a flexbox-like context in WebKit */ + -webkit-box-orient: vertical; /* Ensures that the box is laid out vertically */ + -webkit-line-clamp: 1; + overflow: hidden; + } + } + } + .dropdown{ + animation: fadeIn 0.3s forwards ease-in; + position:absolute; + background-color: var(--background); + top:120%; + z-index: 99; + width:200%; + left:0%; + border: 1px solid var(--lighterborder); + border-radius: 8px; + box-shadow: var(--shadow); + overflow:hidden; + .drop-option{ + display: -webkit-box; /* Establishes a flexbox-like context in WebKit */ + -webkit-box-orient: vertical; /* Ensures that the box is laid out vertically */ + -webkit-line-clamp: 1; /* Number of lines you want to display */ + overflow: hidden; /* Hides the overflowing text */ + text-overflow: ellipsis; + display: flex; + gap: 5px; + padding:5px 10px; + cursor: pointer; + transition: all 0.3s; + background-color: var(--background); + &:hover{ + filter:brightness(0.95); + } + &:not(:first-child){ + border-top:1px solid var(--lighterborder); + } + img{ + -webkit-user-select: none; + user-select: none; + height:20px; + } + p{ + -webkit-user-select: none; + user-select: none; + + } + } + } + } + + } } .steps{ display:flex; diff --git a/frontend/src/pages/CreateOrg/CreateOrg.jsx b/frontend/src/pages/CreateOrg/CreateOrg.jsx new file mode 100644 index 00000000..36af7b20 --- /dev/null +++ b/frontend/src/pages/CreateOrg/CreateOrg.jsx @@ -0,0 +1,277 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import '../OnBoarding/Onboard.scss'; +import './CreateOrg.scss'; +import PurpleGradient from '../../assets/RedBottomRight.png'; +import YellowRedGradient from '../../assets/RedTopRight.png'; +import Loader from '../../components/Loader/Loader.jsx'; +import useAuth from '../../hooks/useAuth.js'; +import { useNavigate } from 'react-router-dom'; +import { useError } from '../../ErrorContext.js'; +import { useNotification } from '../../NotificationContext.js'; +import CardHeader from '../../components/ProfileCard/CardHeader/CardHeader.jsx'; +import ImageUpload from '../../components/ImageUpload/ImageUpload.jsx'; +import axios from 'axios'; +import { debounce } from '../../Query.js'; +import check from '../../assets/Icons/Check.svg'; +import waiting from '../../assets/Icons/Waiting.svg'; +import error from '../../assets/circle-warning.svg'; +import unavailable from '../../assets/Icons/Circle-X.svg'; + +function CreateOrg(){ + const [start, setStart] = useState(false); + const [current, setCurrent] = useState(0); + const [show, setShow] = useState(0); + const [currentTransition, setCurrentTransition] = useState(0); + const [containerHeight, setContainerHeight] = useState(250); + const { isAuthenticated, isAuthenticating, user } = useAuth(); + const [userInfo, setUserInfo] = useState(null); + const [name, setName] = useState(""); + const [timeCommitment, setTimeCommitment] = useState(null); + const [description, setDescription] = useState(""); + const [org, setOrg] = useState(null); + + const navigate = useNavigate(); + const {addNotification} = useNotification(); + const { newError } = useError(); + + const [buttonActive, setButtonActive] = useState(true); + const [validNext, setValidNext] = useState(true); + const [nameValid, setNameValid] = useState(null); + + const containerRef = useRef(null); + const contentRefs = useRef([]); + + + useEffect(()=>{ + if (containerRef.current) { + setContainerHeight(contentRefs.current[0].clientHeight+10); + } + }, []); + + useEffect(() => { + setTimeout(() => { + setStart(true); + }, 500); + },[]); + + useEffect(() => { + if(isAuthenticating){ + return; + } + if (!isAuthenticated) { + navigate('/login'); + } else { + if(user){ + if(user.developer === 0){ + navigate('/settings'); + + } + setUserInfo(user); + } + } + }, [isAuthenticating, isAuthenticated, user]); + + useEffect(()=>{ + if(current === 0){return;} + setTimeout(() => { + setCurrentTransition(currentTransition+1); + }, 500); + if (contentRefs.current[current] && current !== 0) { + setTimeout(() => { + setContainerHeight(contentRefs.current[current].offsetHeight); + }, 500); + console.log(contentRefs.current[current].offsetHeight); + console.log(current); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [current]); + + async function handleOrgCreation(name, description){ + try { + const response = await axios.post('/create-org', {org_name: name, org_profile_image: '/Logo.svg', org_description: description}, {headers: {"Authorization": `Bearer ${localStorage.getItem("token")}`}}); + setOrg(response.data.org); + } catch (error) { + addNotification({ title: 'Error', message: error.message, type: 'error' }); + navigate('/'); + } + } + + useEffect(()=>{ + if(current === 0 || current===3 || current === 4){ + return; + } + if(current === 2){ + if(description === ""){ + setValidNext(false); + } else { + setValidNext(true); + } + return; + } + if(current === 3){ + if(timeCommitment === null){ + setValidNext(false); + } else { + setValidNext(true); + } + return; + } + if(current === 4){ + if("" === ""){ + setValidNext(false); + } else { + setValidNext(true); + } + return; + } + + + },[current, name, description]); + + useEffect(()=>{ + if(show === 0){return;} + setTimeout(() => { + setCurrent(current+1); + }, 500); + + if(current === 4){ + try{ + handleOrgCreation(name, description); + } catch (error){ + newError(error, navigate); + } + } + if(current === 5){ + navigate('/room/none'); + } + setButtonActive(false); + setTimeout(() => { + setButtonActive(true); + }, 1000); + }, [show]); + + const [viewport, setViewport] = useState("100vh"); + + useEffect(() => { + setViewport((window.innerHeight) + 'px'); + },[]); + + const validOrgName = async (name) => { + try { + const response = await axios.post('/check-org-name', {orgName: name}, {headers: {"Authorization": `Bearer ${localStorage.getItem("token")}`}}); + console.log(response); + setNameValid(1); + setValidNext(true); + return response.data.valid; + } catch (error) { + setNameValid(3); + setValidNext(false); + // addNotification({ title: 'Error', message: error.message, type: 'error' }); + } + } + + const debounced = useCallback(debounce(validOrgName, 500),[]); + + useEffect(()=>{ + if(name === ""){ + setNameValid(3); + if(current === 1){ + debounced(name); + setValidNext(false); + } + return; + } + setNameValid(0); + debounced(name); + }, [name]); + + if(isAuthenticating || !userInfo){ + return( +
+ ) + } + + const handleNameChange = (e) => { + setName(e.target.value); + } + + const handleDescChange = (e) => { + // limit to 500 chars + if(e.target.value.length > 500){ + return; + } + setDescription(e.target.value); + } + + return ( +
+ + + +
+
+ { current === 0 && +
contentRefs.current[0] = el}> + +

let's set up your study compass organization!

+

Study Compass provides a variety of tools designed to make the management of your organization as smooth as possible. We'll just need some information from you before you get started.

+
+ } + { current === 1 && +
contentRefs.current[1] = el}> + +

what should we call your organization?

+

This name will be publicly visible to users, and should be unique as well

+
+ +
+ { nameValid === 0 &&

checking name...

} + { nameValid === 1 &&

name is available

} + { nameValid === 2 &&

name is taken

} + { nameValid === 3 &&

invalid name

} +
+
+
+ } + { current === 2 && +
contentRefs.current[2] = el}> + +

tell us a little bit about your organization

+

Give users a description of what your org is about, feel free to be descriptive!

+