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 76d959fb..cf53ed81 100644 --- a/backend/app.js +++ b/backend/app.js @@ -64,6 +64,7 @@ const analyticsRoutes = require('./routes/analytics.js'); const classroomChangeRoutes = require('./routes/classroomChangeRoutes.js'); const ratingRoutes = require('./routes/ratingRoutes.js'); const searchRoutes = require('./routes/searchRoutes.js'); +const clubRoutes = require('./routes/clubRoutes.js'); const eventRoutes = require('./routes/eventRoutes.js'); const oieRoutes = require('./routes/oie-routes.js'); @@ -77,9 +78,10 @@ app.use(eventRoutes); app.use(classroomChangeRoutes); app.use(ratingRoutes); app.use(searchRoutes); +app.use(clubRoutes); app.use(eventRoutes); app.use(oieRoutes); - +app.use(clubRoutes); // Serve static files from the React app in production if (process.env.NODE_ENV === 'production') { 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/routes/clubRoutes.js b/backend/routes/clubRoutes.js new file mode 100644 index 00000000..2f689508 --- /dev/null +++ b/backend/routes/clubRoutes.js @@ -0,0 +1,426 @@ +const express = require('express'); +const router = express.Router(); +const { verifyToken, verifyTokenOptional } = require('../middlewares/verifyToken'); +const mongoose = require('mongoose'); +const User = require('../schemas/user.js'); +const Club = require('../schemas/club.js'); +const Follower = require('../schemas/clubFollowers.js'); +const Member = require('../schemas/clubMember.js'); +const { cloudiot_v1 } = require('googleapis'); +const { clean, isProfane } = require('../services/profanityFilterService'); + + +//Route to get a specific club by name +router.get("/get-club/:id", verifyToken, async(req,res)=>{ +//May eventually change login permissions + +try{ + const clubId= req.params.id; + + //Find the Club + const club= await Club.find({_id: clubId}); + + if(club !== null){ + // If the club exists, return it + console.log(`GET: /get-club/${clubId}`); + res.json({ success: true, message: "Club found", club: club}); + + } else{ + // If not found, return a 404 with a message + return res.status(404).json({ success: false, message: 'Club not found' }); + } + + } catch(error){ + // Handle any errors that occur during the process + console.log(`GET: /get-club failed`, error); + return res.status(500).json({ success: false, message: 'Error retrieving club data', error: error.message }); + } +}); + + +// Create a new club and store it into database +router.post("/create-club", verifyToken, async(req,res)=>{ + + const{ club_name, club_profile_image, club_description, positions, weekly_meeting } = req.body; + + try { + + //Verify user and have their clubs 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 clubExist = await Club.findOne({club_name: club_name }); + //Check to verify if the club already exists + if (clubExist){ + return res.status(400).json({ success: false, message: 'Club name already exists'}); + } + if (isProfane(req.body.club_name)) { //Check if Bad Words + return res.status(400).json({ success: false, message: 'Club name contains inappropriate language' }); + } + + const cleanClubName = clean(club_name); // Sanitize club name and description verify this right + if (isProfane(club_description)) { //Check if Bad Words + return res.status(400).json({ success: false, message: 'Description contains inappropriate language' }); + } + + const cleanClubDescription = clean(club_description); + + const newClub = new Club({ + club_name: cleanClubName, + club_profile_image: club_profile_image, + club_description: cleanClubDescription, + 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 club + club_id: newClub._id, + user_id: userId, + status: 0, + }); + + await newMember.save(); + + user.clubAssociations.push(newClub._id); + await user.save(); + + const saveClub = await newClub.save(); //Save the club + console.log(`POST: /create-club`); + return res.status(200).json({ success: true, message: "Club created successfully", club: saveClub }); + + } catch(error){ + // Handle any errors that occur during the process + console.log(`POST: /create-club failed`, error); + return res.status(500).json({ success: false, message: 'Error creating club', error: error.message }); + } +}); + + +router.post("/edit-club", verifyToken, async (req, res) => { + try { + const { clubId, club_profile_image, club_description, positions, weekly_meeting, club_name } = req.body; + const userId = req.user?.userId; + + // Validate that the essential fields are present + if (!clubId) { + return res.status(400).json({ success: false, message: "Club ID is required" }); + } + + const club = await Club.findById(clubId); + + // Check if the club exists + console.log + if (!club) { + return res.status(404).json({ success: false, message: "Club not found" }); + } + + // Check if the user is authorized to edit the club + if (club.owner.toString() !== userId) { + return res.status(400).json({ success: false, message: "You are not authorized to edit this club" }); + } + + // If club_name is provided, clean it and check for inappropriate language + if (club_name) { + + const cleanClubName = clean(club_name); + + if (isProfane(club_name)) { + return res.status(400).json({ success: false, message: "Club name contains inappropriate language" }); + } + + // Check if a club with the cleaned name already exists, excluding the current club + const clubExist = await Club.findOne({ club_name: cleanClubName }); + if (clubExist && clubExist._id.toString() !== clubId) { + return res.status(400).json({ success: false, message: "Club name already taken" }); + } + + // Update the club name with the clean version + club.club_name = cleanClubName; + } + + // If club_description is provided, clean it + if (club_description) { + const cleanClubDescription = clean(club_description); + + if (isProfane(club_description)) { + return res.status(400).json({ success: false, message: "Description contains inappropriate language" }); + } + club.club_description = cleanClubDescription; + } + + // Update other fields only if they are provided + if (club_profile_image) { + club.club_profile_image = club_profile_image; + } + if (positions) { + club.positions = positions; + } + if (weekly_meeting) { + club.weekly_meeting = weekly_meeting; + } + + // Save the updated club + await club.save(); + + // Send success response + console.log(`POST: /edit-club`); + res.json({ success: true, message: "Club updated successfully", club }); + + } catch (error) { + // Handle any errors that occur during the process + console.log(`POST: /edit-club failed`, error); + return res.status(500).json({ success: false, message: "Error updating club", error: error.message }); + } +}); + + + +router.delete("/delete-club/:clubId", verifyToken, async(req,res)=>{ +try { + const { clubId } = req.params; + const userId = req.user.userId; + + const club = await Club.findById(clubId); + + //Check if the club exists + if (!club) { + return res.status(404).json({ success: false, message: "Club not found" }); + } + + // Verify that the user is the owner of the club + if (club.owner.toString() !== userId) { + return res.status(400).json({ success: false, message: "You are not authorized to delete this club" }); + } + + // Delete the club + await Club.findByIdAndDelete(clubId); + + console.log(`DELETE: /delete-club`); + res.json({ success: true, message: "Club deleted successfully" }); + +} catch (error) { + console.log(`DELETE: /delete-club failed`, error); + return res.status(500).json({ success: false, message: "Error deleting club", error: error.message }); +} +}); + +router.post("/follow-club/:clubId", verifyToken, async(req,res)=>{ + try { + const { clubId } = req.params; + const userId = req.user.userId; + + const club = await Club.findById(clubId); + + //Check if the club exists + if (!club) { + return res.status(404).json({ success: false, message: "Club not found" }); + } + + const alreadyFollowing = await Follower.findOne({ user_id: userId, club_id: clubId }); //Check if the user is already following the club + if (alreadyFollowing) { + return res.status(400).json({ success: false, message: "You are already following this club" }); + } + + const newFollower = new Follower({ + user_id: userId, + club_id: clubId + }); + await newFollower.save(); + + console.log(`POST: /follow-club`); + res.json({ success: true, message: "Club followed successfully" }); + + } catch (error) { + console.log(`POST: /follow-club failed`, error); + return res.status(500).json({ success: false, message: "Error following club", error: error.message }); + } +}); + +router.post("/unfollow-club/:clubId", verifyToken, async(req,res)=>{ + try { + const { clubId } = req.params; + const userId = req.user.userId; + + const club = await Club.findById(clubId); + + //Check if the club exists + if (!club) { + return res.status(404).json({ success: false, message: "Club not found" }); + } + + const follower = await Follower.findOne({ user_id: userId, club_id: clubId }); //Check if the user is already following the club + if (!follower) { + return res.status(400).json({ success: false, message: "You are not following this club" }); + } + + await Follower.findByIdAndDelete(follower._id); + + console.log(`POST: /unfollow-club`); + res.json({ success: true, message: "Club unfollowed successfully" }); + + } catch (error) { + console.log(`POST: /unfollow-club failed`, error); + return res.status(500).json({ success: false, message: "Error unfollowing club", error: error.message }); + } +}); + +router.get("/get-followed-clubs", verifyToken, async(req,res)=>{ + try { + const userId = req.user.userId; + + const followedClubs = await Follower.find({ user_id: userId }).populate('club_id'); + console.log(`GET: /get-followed-clubs`); + res.json({ success: true, message: "Followed clubs retrieved successfully", clubs: followedClubs }); + + } catch (error) { + console.log(`GET: /get-followed-clubs failed`, error); + return res.status(500).json({ success: false, message: "Error retrieving followed clubs", error: error.message }); + } +}); + +router.get("/get-club-members/:clubId", verifyToken, async(req,res)=>{ + try { + const { clubId } = req.params; + const userId = req.user.userId; + + const club = await Club.findById(clubId); + + //Check if the club exists + if (!club) { + return res.status(404).json({ success: false, message: "Club not found" }); + } + + // Check if the user is a member of the club + const member = await Member.findOne({ club_id: clubId, user_id: userId }); + if (!member) { + return res.status(400).json({ success: false, message: "You are not a member of this club" }); + } + + const members = await Member.find({ club_id: clubId }).populate('user_id'); + console.log(`GET: /get-club-members`); + res.json({ success: true, message: "Club members retrieved successfully", members }); + + } catch (error) { + console.log(`GET: /get-club-members failed`, error); + return res.status(500).json({ success: false, message: "Error retrieving club members", error: error.message }); + } +}); + +router.post("/add-club-member/:clubId", verifyToken, async(req,res)=>{ + try { + const { clubId } = req.params; + const userId = req.user.userId; + const { user_id, role } = req.body; + + const club = await Club.findById(clubId); + + //Check if the club exists + if (!club) { + return res.status(404).json({ success: false, message: "Club not found" }); + } + + // Check if the user is the owner of the club + if (club.owner.toString() !== userId) { + return res.status(400).json({ success: false, message: "You are not authorized to add members to this club" }); + } + + // Check if the user to be added is already a member + const member = await Member.findOne({ club_id: clubId, user_id: user_id }); + if (member) { + return res.status(400).json({ success: false, message: "User is already a member of this club" }); + } + + const newMember = new Member({ + club_id: clubId, + user_id, + role + }); + await newMember.save(); + + console.log(`POST: /add-club-member`); + res.json({ success: true, message: "Member added successfully" }); + + } catch (error) { + console.log(`POST: /add-club-member failed`, error); + return res.status(500).json({ success: false, message: "Error adding member", error: error.message }); + } +}); + +router.delete("/remove-club-member/:clubId", verifyToken, async(req,res)=>{ + try { + const { clubId } = req.params; + const userId = req.user.userId; + const { user_id } = req.body; + + const club = await Club.findById(clubId); + + //Check if the club exists + if (!club) { + return res.status(404).json({ success: false, message: "Club not found" }); + } + + // Check if the user is the owner of the club + if (club.owner.toString() !== userId) { + return res.status(400).json({ success: false, message: "You are not authorized to remove members from this club" }); + } + + // Check if the user to be removed is a member + const member = await Member.findOne({ club_id: clubId, user_id }); + if (!member) { + return res.status(400).json({ success: false, message: "User is not a member of this club" }); + } + + await Member.findByIdAndDelete(member._id); + + console.log(`DELETE: /remove-club-member`); + res.json({ success: true, message: "Member removed successfully" }); + + } catch (error) { + console.log(`DELETE: /remove-club-member failed`, error); + return res.status(500).json({ success: false, message: "Error removing member", error: error.message }); + } +}); + +router.post("/update-club-member/:clubId", verifyToken, async(req,res)=>{ + try { + const { clubId } = req.params; + const userId = req.user.userId; + const { user_id, role } = req.body; + + const club = await Club.findById(clubId); + + //Check if the club exists + if (!club) { + return res.status(404).json({ success: false, message: "Club not found" }); + } + + // Check if the user is the owner of the club + if (club.owner.toString() !== userId) { + return res.status(400).json({ success: false, message: "You are not authorized to update members of this club" }); + } + + // Check if the user to be updated is a member + const member = await Member.findOne({ club_id: clubId, user_id }); + if (!member) { + return res.status(400).json({ success: false, message: "User is not a member of this club" }); + } + + member.role = role; + await member.save(); + + console.log(`POST: /update-club-member`); + res.json({ success: true, message: "Member updated successfully" }); + + } catch (error) { + console.log(`POST: /update-club-member failed`, error); + return res.status(500).json({ success: false, message: "Error updating member", error: error.message }); + } +}); + +module.exports = router; 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/schemas/club.js b/backend/schemas/club.js new file mode 100644 index 00000000..a45a9f27 --- /dev/null +++ b/backend/schemas/club.js @@ -0,0 +1,37 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const clubSchema= new Schema({ + club_name:{ + type:String, + required: true + }, + club_profile_image:{ + type: String, + required: true + }, + club_description:{ + type: String, + required: true + }, + positions: { + type: Array, // [regular, treasurer, secretary] + required: true, + default:['chair', 'officer', 'regular'] //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 Club =mongoose.model('Club',clubSchema,'clubs'); + +module.exports=Club; diff --git a/backend/schemas/clubFollowers.js b/backend/schemas/clubFollowers.js new file mode 100644 index 00000000..34b2df83 --- /dev/null +++ b/backend/schemas/clubFollowers.js @@ -0,0 +1,20 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const followerSchema = new Schema({ + club_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/clubMember.js b/backend/schemas/clubMember.js new file mode 100644 index 00000000..c35d7abf --- /dev/null +++ b/backend/schemas/clubMember.js @@ -0,0 +1,25 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const memberSchema = new Schema({ + club_id: { + type: Schema.Types.ObjectId, + required: true, + ref: 'Club' + }, + user_id: { + type: Schema.Types.ObjectId, + required: true, + ref: 'User' + }, + status: { + //role index, see club schema for roles + type: Number, + required:true, + } +}); + + +const ClubMember = mongoose.model('ClubMember', memberSchema, 'members'); + +module.exports = ClubMember; \ No newline at end of file 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 c202692f..07569fdf 100644 --- a/backend/schemas/user.js +++ b/backend/schemas/user.js @@ -87,6 +87,10 @@ 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'], diff --git a/frontend/src/App.css b/frontend/src/App.css index b07dad62..a457ad4e 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); @@ -350,4 +350,28 @@ button{ 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; + } \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js index 6d4e5491..df16c61b 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'; @@ -16,6 +17,7 @@ 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 CreateClub from './pages/CreateClub/CreateClub'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { AuthProvider } from './AuthContext'; import { CacheProvider } from './CacheContext'; @@ -118,6 +120,7 @@ function App() { }/> }/> }/> + }/> 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/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/components/EventsViewer/EventsGrid/EventsColumn/CreateEvent/CreateEvent.scss b/frontend/src/components/EventsViewer/EventsGrid/EventsColumn/CreateEvent/CreateEvent.scss index c21efb4e..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; } &:hover::after{ 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/pages/ClubDash/Dash/Dash.jsx b/frontend/src/pages/ClubDash/Dash/Dash.jsx new file mode 100644 index 00000000..3bb830ff --- /dev/null +++ b/frontend/src/pages/ClubDash/Dash/Dash.jsx @@ -0,0 +1,72 @@ +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}){ + + 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 ( +
+
+ +

Club Dashboard

+
+
+
+

manage membership

+ +
+

200 members

+

8 officers

+
+ + +

quick actions

+
+
+

meetings coming up

+
+

Random Student Event

+
+ +
+ +
+ +
+ + + + + ) +} + + +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..63164ade --- /dev/null +++ b/frontend/src/pages/ClubDash/Dash/Dash.scss @@ -0,0 +1,84 @@ +.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%; + } + } + .membership{ + 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(--lightborder); + width: 350px; + height: 120px; + justify-content: space-around; + align-items: center; + + &.meeting{ + padding-left: 20px; + width: 250px; + height: 120px; + } + 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; + border: 1px solid var(--lightborder); + + } + } + + + } + } +} \ No newline at end of file diff --git a/frontend/src/pages/CreateClub/CreateClub.jsx b/frontend/src/pages/CreateClub/CreateClub.jsx new file mode 100644 index 00000000..47a7aff0 --- /dev/null +++ b/frontend/src/pages/CreateClub/CreateClub.jsx @@ -0,0 +1,246 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import '../OnBoarding/Onboard.scss'; +import './CreateClub.scss'; +import PurpleGradient from '../../assets/BlueGrad2.png'; +import YellowRedGradient from '../../assets/BlueGrad1.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 { checkUsername } from '../../DBInteractions.js'; +import { useCache } from '../../CacheContext.js'; +import { debounce} from '../../Query.js'; +import CardHeader from '../../components/ProfileCard/CardHeader/CardHeader.jsx'; +import ImageUpload from '../../components/ImageUpload/ImageUpload'; +import axios from 'axios'; + +function CreateClub(){ + 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 [club, setClub] = useState(null); + + const navigate = useNavigate(); + const {addNotification} = useNotification(); + const { newError } = useError(); + + const [buttonActive, setButtonActive] = useState(true); + const [validNext, setValidNext] = useState(true); + + + 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 handleClubCreation(name, description){ + try { + const response = await axios.post('/create-club', {club_name: name, club_profile_image: '/Logo.svg', club_description: description}, {headers: {"Authorization": `Bearer ${localStorage.getItem("token")}`}}); + setClub(response.data.club); + } catch (error) { + addNotification({ title: 'Error', message: error.message, type: 'error' }); + navigate('/'); + } + } + + useEffect(()=>{ + if(current === 0 || current===3 || current === 4){ + return; + } + if(current === 1){ + if(name === ""){ + setValidNext(false); + } else { + setValidNext(true); + } + 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{ + handleClubCreation(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'); + },[]); + + if(isAuthenticating || !userInfo){ + return( +
+ ) + } + + const handleNameChange = (e) => { + setName(e.target.value); + } + + const handleDescChange = (e) => { + 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

+ +
+ } + { 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!

+