├── LICENSE ├── README.md ├── backend ├── .env ├── .gitignore ├── config.js ├── controllers │ ├── comment.js │ ├── game.js │ ├── post.js │ └── user.js ├── database │ └── collections.js ├── index.js ├── middlewares │ ├── auth.js │ ├── comment.js │ ├── game.js │ ├── post.js │ ├── upload.js │ └── user.js ├── models │ ├── comment.js │ ├── game.js │ ├── post.js │ └── user.js ├── package-lock.json ├── package.json ├── routes │ ├── index.js │ ├── private.js │ ├── private │ │ ├── comment.js │ │ ├── game.js │ │ ├── post.js │ │ └── user.js │ ├── public.js │ └── public │ │ ├── comment.js │ │ ├── game.js │ │ ├── post.js │ │ └── user.js └── utils │ ├── authorize.js │ ├── token.js │ └── upload.js └── minigame-frontend ├── .editorconfig ├── .env ├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── src ├── assets │ ├── ThumbsDown.jsx │ ├── ThumbsDownFilled.jsx │ ├── ThumbsUp.jsx │ ├── ThumbsUpFilled.jsx │ ├── Trash.jsx │ └── react.svg ├── components │ ├── CommentCard.jsx │ ├── CommentSection.jsx │ ├── GameDetails.jsx │ ├── GameDetailsPopup.jsx │ ├── GameRowComponent.jsx │ ├── HeroComponent.jsx │ ├── Leaderboard.jsx │ ├── LikedGames.jsx │ ├── LoginModal.jsx │ ├── PlayerRank.jsx │ ├── Popup.jsx │ ├── PostView.jsx │ ├── ProfileBioComponent.jsx │ ├── ProfileDetails.jsx │ ├── ProfilePopup.jsx │ ├── ShowcaseGameCardComponent.jsx │ ├── css │ │ ├── CommentCard.css │ │ ├── Hero.css │ │ ├── Leaderboard.css │ │ ├── ProfileBio.css │ │ ├── ProfileDetails.css │ │ └── ShowcaseGameCard.css │ └── games │ │ └── GameCard.jsx ├── contributors │ └── Contributors.jsx ├── domain │ ├── SiteContext.jsx │ └── UserContext.jsx ├── images │ ├── AdrianPFP.png │ ├── DanielPFP.jpg │ ├── JackPFP.jpg │ ├── MichaelPFP.jpeg │ ├── MinhPFP.jpg │ ├── ThaiPFP.jpg │ ├── TrentonPFP.jpg │ └── gglogo2.png ├── index.css ├── main.jsx └── page │ ├── AllGames.css │ ├── AllGames.jsx │ ├── Contributors.jsx │ ├── CreateGame.jsx │ ├── Editor.jsx │ ├── GamePage.jsx │ ├── Homepage.jsx │ ├── Layout.jsx │ ├── Post.jsx │ ├── PostViewPage.jsx │ ├── Profile.jsx │ ├── contributors.css │ └── profile.css ├── tailwind.config.js └── vite.config.js /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GameGimmel 2 | Hosted Website Link: https://gamegimmel.acmuic.org 3 | 4 | ## Running the project locally 5 | In the terminal: 6 | - move to the frontend or backend directory/folder with `cd .\directory\` 7 | 1. run `npm install` in frontend or backend directory 8 | 2. run `npm run dev` in the frontend and backend directory 9 | 10 | ## Contributors 11 | 12 | - Adrian Knight - [@Ajknight121](https://github.com/Ajknight121) 13 | - Chao Liu - [@JackLiu00331](https://github.com/JackLiu00331) 14 | - Viet Thai Nguyen - [@thai.nguyen07](https://github.com/AlgoriThai07) 15 | - Trenton Coleman - [@tdcoleman127](https://github.com/tdcoleman127) 16 | - Minh Ngo - [@hoangngo-sudo](https://github.com/hoangngo-sudo) 17 | - Daniel Barajas - [@danbarajas](https://github.com/danbarajas) 18 | - Michael Jaimes - [@mike6612](https://github.com/mike6612) 19 | - Dylan Nguyen - [@dyl4915](https://github.com/dyl4915) 20 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | PORT = 2 | MONGODB = 3 | AT_SECRET_KEY = 4 | RT_SECRET_KEY = 5 | CLOUDINARY_CONFIG = -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.local -------------------------------------------------------------------------------- /backend/config.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config({ path: ".env.local" }); 3 | 4 | const AT_SECRET_KEY = process.env.AT_SECRET_KEY; 5 | const RT_SECRET_KEY = process.env.RT_SECRET_KEY; 6 | const CLOUDINARY_CONFIG = JSON.parse(process.env.CLOUDINARY_CONFIG); 7 | const MONGODB = process.env.MONGODB; 8 | 9 | export { AT_SECRET_KEY, RT_SECRET_KEY, CLOUDINARY_CONFIG, MONGODB }; 10 | -------------------------------------------------------------------------------- /backend/controllers/comment.js: -------------------------------------------------------------------------------- 1 | import PostModel from "../models/post.js"; 2 | import CommentModel from "../models/comment.js"; 3 | import { authorizeUser } from "../utils/authorize.js"; 4 | 5 | const CommentControllers = { 6 | createComment: async (req, res) => { 7 | try { 8 | // Get postId, body, and user transferred from req 9 | const { postId } = req.params; 10 | const { body } = req.body; 11 | const { user } = req; 12 | // Find post from postId 13 | const crrPost = await PostModel.findById(postId); 14 | // If cannot find, throw an error 15 | if (!crrPost) throw new Error("Cannot not find post!"); 16 | // Create a new comment 17 | const newComment = await CommentModel.create({ 18 | author: user._id, 19 | postId, 20 | body, 21 | }); 22 | // Send out the new comment and the userName of the author 23 | res.status(201).send({ 24 | message: "Comment created successfully!", 25 | success: true, 26 | data: { 27 | ...newComment.toObject(), 28 | userName: user.userName, 29 | }, 30 | }); 31 | } catch (error) { 32 | res.status(404).send({ 33 | message: error.message, 34 | success: false, 35 | data: null, 36 | }); 37 | } 38 | }, 39 | updateComment: async (req, res) => { 40 | try { 41 | // Get commentId, body, and user transferred from req 42 | const { commentId } = req.params; 43 | const { body } = req.body; 44 | const { user } = req; 45 | // Find comment from the commentId 46 | const crrComment = await CommentModel.findById(commentId); 47 | if (!crrComment) throw new Error("Cannot find comment!"); 48 | // Authorized user 49 | const authorized = authorizeUser(user._id, crrComment.author); 50 | if (!authorized.success) throw new Error(authorized.message); 51 | // Only updated the fields that needs changing 52 | const updatedFields = {}; 53 | if (!body && String(body) !== String(crrComment.body)) { 54 | updatedFields.body = body; 55 | } 56 | // Update comment 57 | const updatedComment = await CommentModel.findByIdAndUpdate( 58 | commentId, 59 | { $set: updatedFields }, 60 | { new: true } 61 | ); 62 | // Return the updatedComment 63 | res.status(200).send({ 64 | message: "Comment updated successfully!", 65 | success: true, 66 | data: updatedComment, 67 | }); 68 | } catch (error) { 69 | res.status(404).send({ 70 | message: error.message, 71 | success: false, 72 | data: null, 73 | }); 74 | } 75 | }, 76 | getAllComment: async (req, res) => { 77 | try { 78 | const admin = req.user?.role === "Admin"; 79 | 80 | const commentFilter = admin ? {} : { isDelete: false }; 81 | 82 | const listComments = await CommentModel.find(commentFilter); 83 | if (listComments.length === 0) throw new Error("No comments found!"); 84 | 85 | res.status(201).send({ 86 | message: "Here is a list of comments!", 87 | success: true, 88 | data: listComments, 89 | }); 90 | } catch (error) { 91 | res.status(500).send({ 92 | message: error.message, 93 | success: false, 94 | data: null, 95 | }); 96 | } 97 | }, 98 | getCommentsInAPost: async (req, res) => { 99 | try { 100 | const { postId } = req.params; 101 | 102 | const admin = req.user?.role === "Admin"; 103 | 104 | const commentFilter = admin 105 | ? { postId: postId } 106 | : { postId: postId, isDelete: false }; 107 | 108 | let listComments = await CommentModel.find(commentFilter); 109 | 110 | if (listComments.length === 0) { 111 | // throw new Error("No comments in this post!"); 112 | listComments = []; 113 | } 114 | 115 | res.status(201).send({ 116 | message: "Here is a list of comments in this post", 117 | success: true, 118 | data: listComments, 119 | }); 120 | } catch (error) { 121 | res.status(500).send({ 122 | message: error.message, 123 | success: false, 124 | data: null, 125 | }); 126 | } 127 | }, 128 | deleteComment: async (req, res) => { 129 | try { 130 | const { user } = req; 131 | const { commentId } = req.params; 132 | 133 | // Get the current post 134 | const crrComment = await CommentModel.findById(commentId); 135 | if (!crrComment) throw new Error("Cannot find comment!"); 136 | 137 | // Check if the user is authorized to delete post 138 | const owner = authorizeUser(user._id, crrComment.author); 139 | const admin = user.role === "Admin"; 140 | 141 | if (!owner.success && !admin) { 142 | throw new Error("Unauthorize to delete comment!"); 143 | } 144 | // Update + Fetch in parallel 145 | const commentFilter = admin ? {} : { isDelete: false }; 146 | 147 | const [_, listComments] = await Promise.all([ 148 | CommentModel.findByIdAndUpdate(commentId, { isDelete: true }), 149 | CommentModel.find(commentFilter), 150 | ]); 151 | 152 | res.status(200).send({ 153 | message: "Comment deleted!", 154 | success: true, 155 | data: listComments, 156 | }); 157 | } catch (error) { 158 | res.status(500).send({ 159 | message: error.message, 160 | success: false, 161 | data: null, 162 | }); 163 | } 164 | }, 165 | }; 166 | 167 | export default CommentControllers; 168 | -------------------------------------------------------------------------------- /backend/controllers/game.js: -------------------------------------------------------------------------------- 1 | import GameModel from "../models/game.js"; 2 | import { handleFileUpload } from "../utils/upload.js"; 3 | 4 | const GameControllers = { 5 | createGame: async (req, res) => { 6 | try { 7 | // Get info from req 8 | const { user } = req; 9 | const { gameName, description, relatedLinks } = req.body; 10 | const coverImage = req.files?.coverImage?.[0]; // single file 11 | const listFile = req.files?.media || []; // array of files 12 | 13 | const existedGame = await GameModel.findOne({ 14 | gameName: gameName, 15 | }); 16 | 17 | if (existedGame) { 18 | throw new Error("This gameName is used. Please use another name"); 19 | } 20 | 21 | // Create a game payload 22 | const gamePayload = { 23 | gameName, 24 | description, 25 | relatedLinks, 26 | author: user._id, 27 | }; 28 | // If user upload media 29 | if (coverImage) { 30 | const response = await handleFileUpload(coverImage); 31 | if (!response.success) throw new Error(response.message); 32 | gamePayload.coverImage = response.data; 33 | } 34 | 35 | if (listFile) { 36 | const listMedia = []; 37 | for (const file of listFile) { 38 | const response = await handleFileUpload(file); 39 | if (!response.success) throw new Error(response.message); 40 | listMedia.push(response.data); 41 | } 42 | gamePayload.media = listMedia; 43 | } 44 | 45 | const newGame = await GameModel.create(gamePayload); 46 | 47 | res.status(201).send({ 48 | message: "Game created successfully", 49 | success: true, 50 | data: newGame, 51 | }); 52 | } catch (error) { 53 | res.status(500).send({ 54 | message: error.message, 55 | success: false, 56 | data: null, 57 | }); 58 | } 59 | }, 60 | getAllGames: async (req, res) => { 61 | try { 62 | const listGames = await GameModel.find(); 63 | if (listGames.length === 0) throw new Error("No games found!"); 64 | 65 | res.status(200).send({ 66 | message: "Here is a list of all the games", 67 | success: true, 68 | data: listGames, 69 | }); 70 | } catch (error) { 71 | res.status(500).send({ 72 | message: error.message, 73 | success: false, 74 | data: null, 75 | }); 76 | } 77 | }, 78 | getGameById: async (req, res) => { 79 | try { 80 | const { gameId } = req.params; 81 | 82 | const crrGame = await GameModel.findById(gameId); 83 | 84 | if (!crrGame) throw new Error("Cannot find game!"); 85 | 86 | res.status(200).send({ 87 | message: "Here is your game", 88 | success: true, 89 | data: crrGame, 90 | }); 91 | } catch (error) { 92 | res.status(500).send({ 93 | message: error.message, 94 | success: false, 95 | data: null, 96 | }); 97 | } 98 | }, 99 | }; 100 | 101 | export default GameControllers; 102 | -------------------------------------------------------------------------------- /backend/controllers/post.js: -------------------------------------------------------------------------------- 1 | import PostModel from "../models/post.js"; 2 | import { handleFileUpload } from "../utils/upload.js"; 3 | import { authorizeUser } from "../utils/authorize.js"; 4 | 5 | const PostControllers = { 6 | createPost: async (req, res) => { 7 | try { 8 | const { user } = req; 9 | const { title, body } = req.body; 10 | const listFile = req.files; 11 | 12 | const listMedia = []; 13 | 14 | // If user upload files 15 | if (listFile) { 16 | for (const file of listFile) { 17 | const response = await handleFileUpload(file); 18 | if (!response.success) throw new Error(response.message); 19 | listMedia.push(response.data); 20 | } 21 | } 22 | 23 | const newPost = await PostModel.create({ 24 | author: user._id, 25 | title, 26 | body, 27 | images: listMedia, 28 | }); 29 | 30 | res.status(201).send({ 31 | message: "Post created successfully", 32 | success: true, 33 | data: { 34 | ...newPost.toObject(), 35 | userName: user.userName, 36 | }, 37 | }); 38 | } catch (error) { 39 | res.status(500).send({ 40 | message: error.message, 41 | success: false, 42 | data: null, 43 | }); 44 | } 45 | }, 46 | updatePost: async (req, res) => { 47 | try { 48 | const { user } = req; 49 | const { title, body } = req.body; 50 | const { postId } = req.params; 51 | const listFile = req.images; 52 | 53 | // Get the current post 54 | const crrPost = await PostModel.findById(postId); 55 | if (!crrPost) throw new Error("Cannot find post!"); 56 | 57 | const authorized = authorizeUser(user._id, crrPost.author); 58 | if (!authorized.success) throw new Error(authorized.message); 59 | 60 | const updatedFields = {}; 61 | // Only update the fields that are different from the original 62 | if (title && String(title) !== String(crrPost.title)) { 63 | updatedFields.title = title; 64 | } 65 | if (body && String(body) !== String(crrPost.body)) { 66 | updatedFields.body = body; 67 | } 68 | // Update the listMedia regardless whether it has been changed 69 | // More efficient 70 | const listMedia = []; 71 | if (listFile) { 72 | for (const file of listFile) { 73 | const response = await handleFileUpload(file); 74 | if (!response.success) throw new Error(response.message); 75 | listMedia.push(response.data); 76 | } 77 | } 78 | updatedFields.images = listMedia; 79 | // Update the post 80 | const updatedPost = await PostModel.findByIdAndUpdate( 81 | postId, 82 | { $set: updatedFields }, 83 | { new: true } 84 | ); 85 | // Return the updated post 86 | res.status(200).send({ 87 | message: "Post updated successfully!", 88 | success: true, 89 | data: updatedPost, 90 | }); 91 | } catch (error) { 92 | res.status(500).send({ 93 | message: error.message, 94 | success: false, 95 | data: null, 96 | }); 97 | } 98 | }, 99 | getAllPosts: async (req, res) => { 100 | try { 101 | const admin = req.user?.role === "Admin"; 102 | 103 | const postFilter = admin ? {} : { isDelete: false }; 104 | 105 | const listPosts = await PostModel.find(postFilter); 106 | if (listPosts.length === 0) throw new Error("No posts found!"); 107 | 108 | res.status(201).send({ 109 | message: "Here is a list of posts!", 110 | success: true, 111 | data: listPosts, 112 | }); 113 | } catch (error) { 114 | res.status(500).send({ 115 | message: error.message, 116 | success: false, 117 | data: null, 118 | }); 119 | } 120 | }, 121 | getPostsByUser: async (req, res) => { 122 | try { 123 | const { userId } = req.query; 124 | const admin = req.user?.role === "Admin"; 125 | 126 | const postFilter = admin 127 | ? { 128 | author: userId, 129 | } 130 | : { 131 | author: userId, 132 | isDelete: false, 133 | }; 134 | 135 | let listPosts = await PostModel.find(postFilter); 136 | 137 | if (listPosts.length === 0) { 138 | // throw new Error("No posts by this user found!"); 139 | listPosts = [] 140 | } 141 | 142 | res.status(201).send({ 143 | message: "Here is a list of posts by this user!", 144 | success: true, 145 | data: listPosts, 146 | }); 147 | } catch (error) { 148 | res.status(500).send({ 149 | message: error.message, 150 | success: false, 151 | data: null, 152 | }); 153 | } 154 | }, 155 | getPostById: async (req, res) => { 156 | try { 157 | const { postId } = req.query; 158 | const admin = req.user?.role === "Admin"; 159 | 160 | const postFilter = admin 161 | ? { 162 | _id: postId, 163 | } 164 | : { 165 | _id: postId, 166 | isDelete: false, 167 | }; 168 | 169 | const crrPost = await PostModel.find(postFilter); 170 | 171 | if (!crrPost) throw new Error("This post doesn't exist!"); 172 | 173 | res.status(201).send({ 174 | message: "Here is your post!", 175 | success: true, 176 | data: crrPost, 177 | }); 178 | } catch (error) { 179 | res.status(500).send({ 180 | message: error.message, 181 | success: false, 182 | data: null, 183 | }); 184 | } 185 | }, 186 | deletePost: async (req, res) => { 187 | try { 188 | const { user } = req; 189 | const { postId } = req.params; 190 | 191 | // Get the current post 192 | const crrPost = await PostModel.findById(postId); 193 | if (!crrPost) throw new Error("Cannot find post!"); 194 | 195 | // Check if the user is authorized to delete post 196 | const owner = authorizeUser(user._id, crrPost.author); 197 | const admin = user.role === "Admin"; 198 | 199 | if (!owner.success && !admin) { 200 | throw new Error("Unauthorize to delete post!"); 201 | } 202 | // Update + Fetch in parallel 203 | const postFilter = admin ? {} : { isDelete: false }; 204 | 205 | const [_, listPosts] = await Promise.all([ 206 | PostModel.findByIdAndUpdate(postId, { isDelete: true }), 207 | PostModel.find(postFilter), 208 | ]); 209 | 210 | res.status(200).send({ 211 | message: "Post deleted!", 212 | success: true, 213 | data: listPosts, 214 | }); 215 | } catch (error) { 216 | res.status(500).send({ 217 | message: error.message, 218 | success: false, 219 | data: null, 220 | }); 221 | } 222 | }, 223 | }; 224 | 225 | export default PostControllers; 226 | -------------------------------------------------------------------------------- /backend/controllers/user.js: -------------------------------------------------------------------------------- 1 | import UserModel from "../models/user.js"; 2 | import bcrypt from "bcrypt"; 3 | import { CLOUDINARY_CONFIG } from "../config.js"; 4 | import { v2 as cloudinary } from "cloudinary"; 5 | import { handleFileUpload } from "../utils/upload.js"; 6 | import { generateToken } from "../utils/token.js"; 7 | 8 | cloudinary.config(CLOUDINARY_CONFIG); 9 | 10 | const UserControllers = { 11 | createUser: async (req, res) => { 12 | try { 13 | // Get the info from the user 14 | const { userName, email, password, bio, role } = req.body; 15 | const avatar = req.file; 16 | // Check if there's any email alr existed 17 | const existedUser = await UserModel.findOne({ 18 | email: email, 19 | }); 20 | 21 | // If alr exists an email, 22 | if (existedUser) 23 | throw new Error("Email existed! Please enter a different email."); 24 | // Create a hash password 25 | const saltRounds = 10; 26 | const salt = bcrypt.genSaltSync(saltRounds); 27 | const hashedPassword = bcrypt.hashSync(password, salt); 28 | // Create a payload for creating newUser 29 | const userPayload = { 30 | userName, 31 | email, 32 | password: hashedPassword, 33 | bio, 34 | role, 35 | }; 36 | // Only add avatar if the user upload avatar file 37 | if (avatar) { 38 | const response = await handleFileUpload(avatar); 39 | if (!response.success) throw new Error(response.message); 40 | userPayload.avatar = response.data; 41 | } 42 | 43 | // Create a new user 44 | const newUser = await UserModel.create(userPayload); 45 | // Hide the password when displaying 46 | newUser.password = undefined; 47 | 48 | res.status(201).send({ 49 | message: "User created successfully", 50 | success: true, 51 | data: { user: newUser }, 52 | }); 53 | } catch (error) { 54 | res.status(409).send({ 55 | message: error.message, 56 | success: false, 57 | data: null, 58 | }); 59 | } 60 | }, 61 | signinUser: async (req, res) => { 62 | try { 63 | // Get the info from the user 64 | const { email, password } = req.body; 65 | 66 | // Check if user exist 67 | const crrUser = await UserModel.findOne({ 68 | email: email, 69 | }); 70 | 71 | // If does not exist user 72 | if (!crrUser) 73 | throw new Error("Account does not exist! Please re-enter your email!"); 74 | 75 | // Check whether the password is correct or not 76 | const comparePassword = bcrypt.compareSync(password, crrUser.password); 77 | if (!comparePassword) 78 | throw new Error( 79 | "Wrong password entered! Please re-enter your password!" 80 | ); 81 | 82 | const user = { 83 | _id: crrUser._id, 84 | email: crrUser.email, 85 | userName: crrUser.userName, 86 | role: crrUser.role, 87 | }; 88 | 89 | const accessToken = generateToken( 90 | { 91 | ...user, 92 | typeToken: "AT", 93 | }, 94 | "AT" 95 | ); 96 | 97 | const refreshToken = generateToken( 98 | { 99 | ...user, 100 | typeToken: "RT", 101 | }, 102 | "RT" 103 | ); 104 | 105 | res.status(200).send({ 106 | message: "User signs in successfully", 107 | success: true, 108 | data: { 109 | user, 110 | accessToken, 111 | refreshToken, 112 | }, 113 | }); 114 | } catch (error) { 115 | res.status(400).send({ 116 | message: error.message, 117 | success: false, 118 | data: null, 119 | }); 120 | } 121 | }, 122 | updateProfile: async (req, res) => { 123 | try { 124 | const { user } = req; 125 | const { userName, email, bio, role } = req.body; 126 | const avatar = req.file; 127 | 128 | // Get the crrUser 129 | const crrUser = await UserModel.findById(user._id); 130 | 131 | const updatedFields = {}; 132 | 133 | // Check and update only changed fields 134 | if (userName && String(userName) !== String(crrUser.userName)) { 135 | updatedFields.userName = userName; 136 | } 137 | if (email && String(email) !== String(crrUser.email)) { 138 | updatedFields.email = email; 139 | } 140 | if (bio && String(bio) !== String(crrUser.bio)) { 141 | updatedFields.bio = bio; 142 | } 143 | if (role && String(role) !== String(crrUser.role)) { 144 | updatedFields.role = role; 145 | } 146 | if (avatar) { 147 | // Get the current file "fileName" 148 | const currentFileName = crrUser.avatar 149 | ? crrUser.avatar.split("/").pop().split(".")[0] 150 | : null; 151 | // Get the new file "fileName" 152 | const newFileName = avatar.originalname.split(".")[0]; 153 | 154 | // If the names are different, replace with the new file 155 | if (newFileName !== currentFileName) { 156 | // Proceed with upload 157 | const response = await handleFileUpload(avatar); 158 | if (!response.success) throw new Error(response.message); 159 | updatedFields.avatar = response.data; 160 | // Delete the old avatar from Cloudinary 161 | if (currentFileName) { 162 | await cloudinary.uploader.destroy(currentFileName); 163 | } 164 | } 165 | } 166 | // Update the only changed fields 167 | const updatedProfile = await UserModel.findByIdAndUpdate( 168 | user._id, 169 | { $set: updatedFields }, 170 | { new: true } 171 | ).select("-password"); 172 | 173 | res.status(200).send({ 174 | message: "Profile updated successfully!", 175 | success: true, 176 | data: updatedProfile, 177 | }); 178 | } catch (error) { 179 | res.status(500).send({ 180 | message: error.message, 181 | success: false, 182 | data: null, 183 | }); 184 | } 185 | }, 186 | getUserInfo: async (req, res) => { 187 | try { 188 | // Get the userId 189 | const { userId } = req.params; 190 | 191 | // Check if user exist 192 | const crrUser = await UserModel.findById(userId); 193 | 194 | // If does not exist user 195 | if (!crrUser) throw new Error("Cannot find user"); 196 | 197 | const user = { 198 | _id: crrUser._id, 199 | email: crrUser.email, 200 | userName: crrUser.userName, 201 | role: crrUser.role, 202 | bio: crrUser.bio, 203 | avatar: crrUser.avatar, 204 | likedGames: crrUser.likedGames, 205 | interests: crrUser.interests, 206 | }; 207 | 208 | res.status(201).send({ 209 | message: "Here is this user info!", 210 | success: true, 211 | data: user, 212 | }); 213 | } catch (error) { 214 | res.status(400).send({ 215 | message: error.message, 216 | success: false, 217 | data: null, 218 | }); 219 | } 220 | }, 221 | likeGame: async (req, res) => { 222 | try { 223 | // Get the userId 224 | const { game } = req.body; 225 | const { user } = req; 226 | 227 | const updatedUser = await UserModel.findByIdAndUpdate( 228 | user._id, 229 | { $addToSet: { likedGames: game } }, // prevents duplicates 230 | { new: true } 231 | ); 232 | 233 | if (!updatedUser) throw new Error("Cannot find user!"); 234 | 235 | res.status(200).send({ 236 | message: "Game liked successfully!", 237 | success: true, 238 | data: updatedUser, 239 | }); 240 | } catch (error) { 241 | res.status(400).send({ 242 | message: error.message, 243 | success: false, 244 | data: null, 245 | }); 246 | } 247 | }, 248 | unlikeGame: async (req, res) => { 249 | try { 250 | // Get the userId 251 | const { game } = req.body; 252 | const { user } = req; 253 | 254 | const updatedUser = await UserModel.findByIdAndUpdate( 255 | user._id, 256 | { $pull: { likedGames: game } }, 257 | { new: true } 258 | ); 259 | 260 | if (!updatedUser) throw new Error("Cannot find user!"); 261 | 262 | res.status(200).send({ 263 | message: "Game unliked successfully!", 264 | success: true, 265 | data: updatedUser, 266 | }); 267 | } catch (error) { 268 | res.status(400).send({ 269 | message: error.message, 270 | success: false, 271 | data: null, 272 | }); 273 | } 274 | }, 275 | addInterest: async (req, res) => { 276 | try { 277 | // Get the userId 278 | const { interest } = req.body; 279 | const { user } = req; 280 | 281 | const updatedUser = await UserModel.findByIdAndUpdate( 282 | user._id, 283 | { $addToSet: { interests: interest } }, // prevents duplicates 284 | { new: true } 285 | ); 286 | 287 | if (!updatedUser) throw new Error("Cannot find user!"); 288 | 289 | res.status(200).send({ 290 | message: "Interest added successfully!", 291 | success: true, 292 | data: updatedUser, 293 | }); 294 | } catch (error) { 295 | res.status(400).send({ 296 | message: error.message, 297 | success: false, 298 | data: null, 299 | }); 300 | } 301 | }, 302 | removeInterest: async (req, res) => { 303 | try { 304 | // Get the userId 305 | const { interest } = req.body; 306 | const { user } = req; 307 | 308 | const updatedUser = await UserModel.findByIdAndUpdate( 309 | user._id, 310 | { $pull: { interests: interest } }, // prevents duplicates 311 | { new: true } 312 | ); 313 | 314 | if (!updatedUser) throw new Error("Cannot find user!"); 315 | 316 | res.status(200).send({ 317 | message: "Interest removed successfully!", 318 | success: true, 319 | data: updatedUser, 320 | }); 321 | } catch (error) { 322 | res.status(400).send({ 323 | message: error.message, 324 | success: false, 325 | data: null, 326 | }); 327 | } 328 | }, 329 | }; 330 | 331 | export default UserControllers; 332 | -------------------------------------------------------------------------------- /backend/database/collections.js: -------------------------------------------------------------------------------- 1 | const Collections = { 2 | users: "User", 3 | posts: "Post", 4 | comments: "Comment", 5 | games: "Game", 6 | }; 7 | 8 | export default Collections; 9 | -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import { MONGODB } from "./config.js"; 4 | import http from "http"; 5 | import mongoose from "mongoose"; 6 | import { RootRouteV1 } from "./routes/index.js"; 7 | 8 | await mongoose.connect(MONGODB); 9 | 10 | const app = express(); 11 | const server = http.createServer(app); 12 | 13 | app.use(express.json()); // Only used for JSON payloads (not needed for form-data) 14 | app.use(express.urlencoded({ extended: true })); // Same, for urlencoded only 15 | app.use(cors()); 16 | 17 | app.use("/api", RootRouteV1); 18 | 19 | server.listen(process.env.PORT, () => { 20 | console.log(`Server is running at port ${process.env.PORT}`); 21 | }); 22 | -------------------------------------------------------------------------------- /backend/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | import { verifyToken } from "../utils/token.js"; 2 | 3 | const AuthMiddlewares = { 4 | verifyAccessToken: (req, res, next) => { 5 | try { 6 | const authHeader = req.headers["authorization"]; 7 | if (!authHeader) throw new Error("Please enter token"); 8 | // Split the access token from the bearer token 9 | // Handle both cases: with or without 'Bearer ' 10 | const token = authHeader.startsWith("Bearer ") 11 | ? authHeader.split(" ")[1] 12 | : authHeader; 13 | 14 | // Validate the access token 15 | const data = verifyToken(token, "AT"); 16 | req.user = data; 17 | return next(); 18 | } catch (error) { 19 | let type = ""; 20 | let getMessage = ""; 21 | switch (error.message) { 22 | case "invalid signature": 23 | getMessage = "Cannot verify token"; 24 | type = "INVALID_TOKEN"; 25 | break; 26 | case "jwt expired": 27 | getMessage = "Token is expired"; 28 | type = "EXP_TOKEN"; 29 | break; 30 | default: 31 | getMessage = "Cannot authenticate user"; 32 | type = "UNAUTH"; 33 | break; 34 | } 35 | res.status(401).send({ 36 | message: getMessage, 37 | type, 38 | success: false, 39 | data: null, 40 | }); 41 | } 42 | }, 43 | verifyRefreshToken: (req, res, next) => { 44 | try { 45 | const authHeader = req.headers["authorization"]; 46 | if (!authHeader) throw new Error("Please enter token"); 47 | // Split the access token from the bearer token 48 | const token = authHeader.split(" ")[1]; 49 | 50 | // Validate the access token 51 | const data = verifyToken(token, "RT"); 52 | req.user = data; 53 | return next(); 54 | } catch (error) { 55 | let type = ""; 56 | let getMessage = ""; 57 | switch (error.message) { 58 | case "invalid signature": 59 | getMessage = "Cannot verify token"; 60 | type = "INVALID_TOKEN"; 61 | break; 62 | case "jwt expired": 63 | getMessage = "Token is expired"; 64 | type = "EXP_TOKEN"; 65 | break; 66 | default: 67 | getMessage = "Cannot authenticate user"; 68 | type = "UNAUTH"; 69 | break; 70 | } 71 | res.status(401).send({ 72 | message: getMessage, 73 | type, 74 | success: false, 75 | data: null, 76 | }); 77 | } 78 | }, 79 | verifyAdmin: (req, res, next) => { 80 | try { 81 | // Get the user info from req 82 | // Assume that user is verified before 83 | const { user } = req; 84 | // If user doesn't exist or not an admin 85 | if (!user || String(user.role) !== "Admin") { 86 | throw new Error("Not_admin"); 87 | } 88 | 89 | return next(); 90 | } catch (error) { 91 | let type = ""; 92 | let getMessage = ""; 93 | switch (error.message) { 94 | case "Not_admin": 95 | getMessage = "Admin access required"; 96 | type = "Forbidden"; 97 | break; 98 | default: 99 | getMessage = "Access denied"; 100 | type = "UNAUTH"; 101 | break; 102 | } 103 | res.status(403).send({ 104 | message: getMessage, 105 | type, 106 | success: false, 107 | data: null, 108 | }); 109 | } 110 | }, 111 | }; 112 | 113 | export default AuthMiddlewares; 114 | -------------------------------------------------------------------------------- /backend/middlewares/comment.js: -------------------------------------------------------------------------------- 1 | const CommentMiddlewares = { 2 | // Middleware for creating a comment 3 | createComment: (req, res, next) => { 4 | try { 5 | // Ask for postId and comment body 6 | const { postId } = req.params; 7 | const { body } = req.body; 8 | // If lack either, throw an error 9 | if (!postId) throw new Error("Please enter postId!"); 10 | if (!body) throw new Error("Please enter your comment!"); 11 | 12 | return next(); 13 | } catch (error) { 14 | res.status(400).send({ 15 | message: error.message, 16 | success: false, 17 | data: null, 18 | }); 19 | } 20 | }, 21 | updateComment: (req, res, next) => { 22 | try { 23 | const { body } = req.body; 24 | const { commentId } = req.params; 25 | 26 | if (!commentId) throw new Error("Please enter commentId on the params!"); 27 | if (!body) throw new Error("Please enter your comment!"); 28 | 29 | return next(); 30 | } catch (error) { 31 | res.status(400).send({ 32 | message: error.message, 33 | success: false, 34 | data: null, 35 | }); 36 | } 37 | }, 38 | getCommentsInAPost: (req, res, next) => { 39 | try { 40 | const { postId } = req.params; 41 | 42 | if (!postId) throw new Error("Please enter postId!"); 43 | 44 | return next(); 45 | } catch (error) { 46 | res.status(400).send({ 47 | message: error.message, 48 | success: false, 49 | data: null, 50 | }); 51 | } 52 | }, 53 | deleteComment: (req, res, next) => { 54 | try { 55 | const { commentId } = req.params; 56 | if (!commentId) throw new Error("Please enter commentId on the params!"); 57 | 58 | return next(); 59 | } catch (error) { 60 | res.status(400).send({ 61 | message: error.message, 62 | success: false, 63 | data: null, 64 | }); 65 | } 66 | }, 67 | }; 68 | 69 | export default CommentMiddlewares; 70 | -------------------------------------------------------------------------------- /backend/middlewares/game.js: -------------------------------------------------------------------------------- 1 | const GameMiddlewares = { 2 | createGame: (req, res, next) => { 3 | try { 4 | const { gameName, description } = req.body; 5 | 6 | if (!gameName) throw new Error("Please enter gameName"); 7 | if (!description) throw new Error("Please enter game description"); 8 | 9 | return next(); 10 | } catch (error) { 11 | res.status(400).send({ 12 | message: error.message, 13 | success: false, 14 | data: null, 15 | error, 16 | }); 17 | } 18 | }, 19 | getGameById: (req, res, next) => { 20 | try { 21 | const { gameId } = req.params; 22 | 23 | if (!gameId) throw new Error("Please enter gameId!"); 24 | 25 | return next(); 26 | } catch (error) { 27 | res.status(400).send({ 28 | message: error.message, 29 | success: false, 30 | data: null, 31 | error, 32 | }); 33 | } 34 | }, 35 | }; 36 | 37 | export default GameMiddlewares; 38 | -------------------------------------------------------------------------------- /backend/middlewares/post.js: -------------------------------------------------------------------------------- 1 | const PostMiddlewares = { 2 | // A middleware for creating a new post 3 | createPost: (req, res, next) => { 4 | try { 5 | const { title, body } = req.body; 6 | // Throw error if missing title or body 7 | if (!title) throw new Error("Please enter post title!"); 8 | if (!body) throw new Error("Please enter post body!"); 9 | 10 | return next(); 11 | } catch (error) { 12 | res.status(400).send({ 13 | message: error.message, 14 | success: false, 15 | data: null, 16 | error, 17 | }); 18 | } 19 | }, 20 | // A middleware for update post 21 | updatePost: (req, res, next) => { 22 | try { 23 | const { title, body } = req.body; 24 | const { postId } = req.params; 25 | const listFile = req.files; 26 | 27 | if (!postId) throw new Error("Please enter postId!"); 28 | // Throw error if the user doesn't update anything 29 | if (!title && !body && !listFile) { 30 | throw new Error("Please enter an updated field!"); 31 | } 32 | 33 | return next(); 34 | } catch (error) { 35 | res.status(400).send({ 36 | message: error.message, 37 | success: false, 38 | data: null, 39 | error, 40 | }); 41 | } 42 | }, 43 | getPostsByUser: (req, res, next) => { 44 | try { 45 | const { userId } = req.query; 46 | if (!userId) throw new Error("Please enter userId"); 47 | 48 | return next(); 49 | } catch (error) { 50 | res.status(400).send({ 51 | message: error.message, 52 | success: false, 53 | data: null, 54 | error, 55 | }); 56 | } 57 | }, 58 | getPostById: (req, res, next) => { 59 | try { 60 | const { postId } = req.query; 61 | if (!postId) throw new Error("Please enter postId"); 62 | 63 | return next(); 64 | } catch (error) { 65 | res.status(400).send({ 66 | message: error.message, 67 | success: false, 68 | data: null, 69 | error, 70 | }); 71 | } 72 | }, 73 | deletePost: (req, res, next) => { 74 | try { 75 | const { postId } = req.params; 76 | if (!postId) throw new Error("Please enter postId"); 77 | 78 | return next(); 79 | } catch (error) { 80 | res.status(400).send({ 81 | message: error.message, 82 | success: false, 83 | data: null, 84 | error, 85 | }); 86 | } 87 | }, 88 | }; 89 | 90 | export default PostMiddlewares; 91 | -------------------------------------------------------------------------------- /backend/middlewares/upload.js: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | 3 | const storage = multer.memoryStorage(); 4 | const upload = multer({ storage: storage }); 5 | 6 | export default upload; 7 | -------------------------------------------------------------------------------- /backend/middlewares/user.js: -------------------------------------------------------------------------------- 1 | const UserMiddlewares = { 2 | // A middleware for the create user function 3 | createUser: (req, res, next) => { 4 | try { 5 | // Get the information from the user 6 | const { userName, email, password } = req.body; 7 | 8 | // Give an error if missing any information 9 | if (!userName) throw new Error("Please enter userName!"); 10 | if (!email) throw new Error("Please enter email!"); 11 | if (!password) throw new Error("Please enter password!"); 12 | 13 | return next(); 14 | } catch (error) { 15 | res.status(400).send({ 16 | message: error.message, 17 | success: false, 18 | data: null, 19 | }); 20 | } 21 | }, 22 | // A middleware for the signing in user function 23 | signinUser: (req, res, next) => { 24 | try { 25 | // Get the information from the user 26 | const { email, password } = req.body; 27 | 28 | // Give an error if missing any information 29 | if (!email) throw new Error("Please enter email!"); 30 | if (!password) throw new Error("Please enter password!"); 31 | 32 | return next(); 33 | } catch (error) { 34 | res.status(400).send({ 35 | message: error.message, 36 | success: false, 37 | data: null, 38 | }); 39 | } 40 | }, 41 | updateProfile: (req, res, next) => { 42 | try { 43 | const { userName, email, bio, role } = req.body; 44 | const avatar = req.file; 45 | 46 | // If the user doesn't update anything 47 | if (!(userName || email || avatar || bio || role)) { 48 | throw new Error("Please enter an updated field!"); 49 | } 50 | 51 | return next(); 52 | } catch (error) { 53 | res.status(400).send({ 54 | message: error.message, 55 | success: false, 56 | data: null, 57 | }); 58 | } 59 | }, 60 | getUserInfo: (req, res, next) => { 61 | try { 62 | const { userId } = req.params; 63 | if (!userId) throw new Error("Please enter userId!"); 64 | 65 | return next(); 66 | } catch (error) { 67 | res.status(400).send({ 68 | message: error.message, 69 | success: false, 70 | data: null, 71 | }); 72 | } 73 | }, 74 | likeGame: (req, res, next) => { 75 | try { 76 | // Get the game name 77 | const { game } = req.body; 78 | // Give an error if missing any information 79 | if (!game) throw new Error("Please enter game!"); 80 | 81 | return next(); 82 | } catch (error) { 83 | res.status(400).send({ 84 | message: error.message, 85 | success: false, 86 | data: null, 87 | }); 88 | } 89 | }, 90 | unlikeGame: (req, res, next) => { 91 | try { 92 | // Get the game name 93 | const { game } = req.body; 94 | // Give an error if missing any information 95 | if (!game) throw new Error("Please enter game!"); 96 | 97 | return next(); 98 | } catch (error) { 99 | res.status(400).send({ 100 | message: error.message, 101 | success: false, 102 | data: null, 103 | }); 104 | } 105 | }, 106 | addInterest: (req, res, next) => { 107 | try { 108 | // Get the interest 109 | const { interest } = req.body; 110 | // Give an error if missing any information 111 | if (!interest) throw new Error("Please enter interest!"); 112 | 113 | return next(); 114 | } catch (error) { 115 | res.status(400).send({ 116 | message: error.message, 117 | success: false, 118 | data: null, 119 | }); 120 | } 121 | }, 122 | removeInterest: (req, res, next) => { 123 | try { 124 | // Get the interest 125 | const { interest } = req.body; 126 | // Give an error if missing any information 127 | if (!interest) throw new Error("Please enter interest!"); 128 | 129 | return next(); 130 | } catch (error) { 131 | res.status(400).send({ 132 | message: error.message, 133 | success: false, 134 | data: null, 135 | }); 136 | } 137 | }, 138 | }; 139 | 140 | export default UserMiddlewares; 141 | -------------------------------------------------------------------------------- /backend/models/comment.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Collections from "../database/collections.js"; 3 | 4 | const CommentSchema = mongoose.Schema( 5 | { 6 | author: { 7 | type: mongoose.Schema.Types.ObjectId, 8 | required: true, 9 | ref: Collections.users, 10 | }, 11 | postId: { 12 | type: mongoose.Schema.Types.ObjectId, 13 | required: true, 14 | ref: Collections.posts, 15 | }, 16 | body: { 17 | type: String, 18 | required: true, 19 | }, 20 | likes: { 21 | type: Number, 22 | default: 0, 23 | }, 24 | dislikes: { 25 | type: Number, 26 | default: 0, 27 | }, 28 | isDelete: { 29 | type: Boolean, 30 | default: false, 31 | }, 32 | }, 33 | { 34 | timestamps: true, 35 | } 36 | ); 37 | 38 | const CommentModel = mongoose.model(Collections.comments, CommentSchema); 39 | export default CommentModel; 40 | -------------------------------------------------------------------------------- /backend/models/game.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Collections from "../database/collections.js"; 3 | 4 | const GameSchema = mongoose.Schema( 5 | { 6 | gameName: { 7 | type: String, 8 | required: true, 9 | unique: true, 10 | }, 11 | description: { 12 | type: String, 13 | required: true, 14 | }, 15 | coverImage: { 16 | type: String, 17 | }, 18 | media: { 19 | type: [String], 20 | }, 21 | author: { 22 | type: mongoose.Schema.Types.ObjectId, 23 | required: true, 24 | ref: Collections.users, 25 | }, 26 | relatedLinks: { 27 | type: [String], 28 | }, 29 | }, 30 | { 31 | timestamps: true, 32 | } 33 | ); 34 | 35 | const GameModel = mongoose.model(Collections.games, GameSchema); 36 | export default GameModel; 37 | -------------------------------------------------------------------------------- /backend/models/post.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Collections from "../database/collections.js"; 3 | 4 | const PostSchema = mongoose.Schema( 5 | { 6 | author: { 7 | type: mongoose.Schema.Types.ObjectId, 8 | required: true, 9 | ref: Collections.users, 10 | }, 11 | title: { 12 | type: String, 13 | required: true, 14 | }, 15 | body: { 16 | type: String, 17 | required: true, 18 | }, 19 | images: { 20 | type: [String], 21 | }, 22 | isDelete: { 23 | type: Boolean, 24 | default: false, 25 | }, 26 | }, 27 | { 28 | timestamps: true, 29 | } 30 | ); 31 | 32 | const PostModel = mongoose.model(Collections.posts, PostSchema); 33 | export default PostModel; 34 | -------------------------------------------------------------------------------- /backend/models/user.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Collections from "../database/collections.js"; 3 | 4 | // Create a schema for the user 5 | const UserSchema = mongoose.Schema({ 6 | userName: { 7 | type: String, 8 | required: true, 9 | }, 10 | email: { 11 | type: String, 12 | unique: true, 13 | required: true, 14 | }, 15 | password: { 16 | type: String, 17 | required: true, 18 | }, 19 | avatar: { 20 | type: String, 21 | default: 22 | "https://static.vecteezy.com/system/resources/thumbnails/009/292/244/small_2x/default-avatar-icon-of-social-media-user-vector.jpg", 23 | }, 24 | bio: { 25 | type: String, 26 | default: "", 27 | }, 28 | role: { 29 | type: String, 30 | required: true, 31 | default: "User", 32 | }, 33 | likedGames: { 34 | type: [String], 35 | default: [], 36 | }, 37 | interests: { 38 | type: [String], 39 | default: [], 40 | }, 41 | }); 42 | 43 | // Creating a user model 44 | const UserModel = mongoose.model(Collections.users, UserSchema); 45 | 46 | export default UserModel; 47 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "nodemon ./index.js" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.1.1", 15 | "cloudinary": "^2.6.0", 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.4.7", 18 | "express": "^4.21.2", 19 | "jsonwebtoken": "^9.0.2", 20 | "mongoose": "^8.10.0", 21 | "multer": "^1.4.5-lts.1", 22 | "nodemon": "^3.1.9" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import AuthMiddlewares from "../middlewares/auth.js"; 3 | import { PublicRoute } from "./public.js"; 4 | import { PrivateRoute } from "./private.js"; 5 | 6 | // Create a root route and then branching to diff routes 7 | const RootRouteV1 = Router(); 8 | 9 | // Public routes 10 | RootRouteV1.use("/public", PublicRoute); 11 | 12 | // For every router after this, need to verify user first 13 | RootRouteV1.use(AuthMiddlewares.verifyAccessToken); 14 | 15 | RootRouteV1.use("/private", PrivateRoute); 16 | 17 | export { RootRouteV1 }; 18 | -------------------------------------------------------------------------------- /backend/routes/private.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import UserPrivateRoute from "./private/user.js"; 3 | import PostPrivateRoute from "./private/post.js"; 4 | import CommentPrivateRoute from "./private/comment.js"; 5 | import GamePrivateRoute from "./private/game.js"; 6 | 7 | const PrivateRoute = Router(); 8 | 9 | // User route 10 | PrivateRoute.use("/users", UserPrivateRoute); 11 | // Post route 12 | PrivateRoute.use("/posts", PostPrivateRoute); 13 | // Comment route 14 | PrivateRoute.use("/comments", CommentPrivateRoute); 15 | // Game route 16 | PrivateRoute.use("/games", GamePrivateRoute); 17 | 18 | export { PrivateRoute }; 19 | -------------------------------------------------------------------------------- /backend/routes/private/comment.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import CommentMiddlewares from "../../middlewares/comment.js"; 3 | import CommentControllers from "../../controllers/comment.js"; 4 | import AuthMiddlewares from "../../middlewares/auth.js"; 5 | 6 | const CommentPrivateRoute = Router(); 7 | 8 | // Create comment 9 | CommentPrivateRoute.post( 10 | "/create/:postId", 11 | CommentMiddlewares.createComment, 12 | CommentControllers.createComment 13 | ); 14 | 15 | CommentPrivateRoute.put( 16 | "/updateComment/:commentId", 17 | CommentMiddlewares.updateComment, 18 | CommentControllers.updateComment 19 | ); 20 | 21 | CommentPrivateRoute.delete( 22 | "/delete/:commentId", 23 | CommentMiddlewares.deleteComment, 24 | CommentControllers.deleteComment 25 | ); 26 | 27 | CommentPrivateRoute.get( 28 | "/admin/getAll", 29 | AuthMiddlewares.verifyAdmin, 30 | CommentControllers.getAllComment 31 | ); 32 | 33 | CommentPrivateRoute.get( 34 | "/admin/get/postId", 35 | AuthMiddlewares.verifyAdmin, 36 | CommentMiddlewares.getCommentsInAPost, 37 | CommentControllers.getCommentsInAPost 38 | ); 39 | 40 | export default CommentPrivateRoute; 41 | -------------------------------------------------------------------------------- /backend/routes/private/game.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import AuthMiddlewares from "../../middlewares/auth.js"; 3 | import GameMiddlewares from "../../middlewares/game.js"; 4 | import GameControllers from "../../controllers/game.js"; 5 | import upload from "../../middlewares/upload.js"; 6 | 7 | const GamePrivateRoute = Router(); 8 | 9 | GamePrivateRoute.post( 10 | "/create", 11 | upload.fields([ 12 | { name: "coverImage", maxCount: 1 }, 13 | { name: "media", maxCount: 10 }, 14 | ]), 15 | AuthMiddlewares.verifyAdmin, 16 | GameMiddlewares.createGame, 17 | GameControllers.createGame 18 | ); 19 | 20 | export default GamePrivateRoute; 21 | -------------------------------------------------------------------------------- /backend/routes/private/post.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import PostMiddlewares from "../../middlewares/post.js"; 3 | import PostControllers from "../../controllers/post.js"; 4 | import upload from "../../middlewares/upload.js"; 5 | import AuthMiddlewares from "../../middlewares/auth.js"; 6 | 7 | const PostPrivateRoute = Router(); 8 | 9 | // Create post route 10 | PostPrivateRoute.post( 11 | "/create", 12 | upload.array("images"), 13 | PostMiddlewares.createPost, 14 | PostControllers.createPost 15 | ); 16 | 17 | PostPrivateRoute.put( 18 | "/updatePost/:postId", 19 | upload.array("images"), 20 | PostMiddlewares.updatePost, 21 | PostControllers.updatePost 22 | ); 23 | 24 | PostPrivateRoute.delete( 25 | "/delete/:postId", 26 | PostMiddlewares.deletePost, 27 | PostControllers.deletePost 28 | ); 29 | 30 | // Get all posts 31 | PostPrivateRoute.get( 32 | "/admin/getAll", 33 | AuthMiddlewares.verifyAdmin, 34 | PostControllers.getAllPosts 35 | ); 36 | 37 | PostPrivateRoute.get( 38 | "/admin/get", 39 | AuthMiddlewares.verifyAdmin, 40 | (req, res, next) => { 41 | // Get posts by userId 42 | if (req.query.userId) { 43 | return PostMiddlewares.getPostsByUser(req, res, () => 44 | PostControllers.getPostsByUser(req, res) 45 | ); 46 | } 47 | // Get post by postId 48 | if (req.query.postId) { 49 | return PostMiddlewares.getPostById(req, res, () => 50 | PostControllers.getPostById(req, res) 51 | ); 52 | } 53 | 54 | res.status(400).json({ 55 | success: false, 56 | message: "Missing query: please provide either userId or postId", 57 | }); 58 | } 59 | ); 60 | 61 | export default PostPrivateRoute; 62 | -------------------------------------------------------------------------------- /backend/routes/private/user.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import UserMiddlewares from "../../middlewares/user.js"; 3 | import UserControllers from "../../controllers/user.js"; 4 | import AuthMiddlewares from "../../middlewares/auth.js"; 5 | import upload from "../../middlewares/upload.js"; 6 | 7 | const UserPrivateRoute = Router(); 8 | 9 | UserPrivateRoute.put( 10 | "/updateProfile", 11 | upload.single("avatar"), 12 | AuthMiddlewares.verifyAccessToken, 13 | UserMiddlewares.updateProfile, 14 | UserControllers.updateProfile 15 | ); 16 | 17 | UserPrivateRoute.post( 18 | "/likeGame", 19 | AuthMiddlewares.verifyAccessToken, 20 | UserMiddlewares.likeGame, 21 | UserControllers.likeGame 22 | ); 23 | 24 | UserPrivateRoute.post( 25 | "/addInterest", 26 | AuthMiddlewares.verifyAccessToken, 27 | UserMiddlewares.addInterest, 28 | UserControllers.addInterest 29 | ); 30 | 31 | UserPrivateRoute.delete( 32 | "/unlikeGame", 33 | AuthMiddlewares.verifyAccessToken, 34 | UserMiddlewares.unlikeGame, 35 | UserControllers.unlikeGame 36 | ); 37 | 38 | UserPrivateRoute.delete( 39 | "/removeInterest", 40 | AuthMiddlewares.verifyAccessToken, 41 | UserMiddlewares.removeInterest, 42 | UserControllers.removeInterest 43 | ); 44 | 45 | export default UserPrivateRoute; 46 | -------------------------------------------------------------------------------- /backend/routes/public.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import UserPublicRoute from "./public/user.js"; 3 | import GamePublicRoute from "./public/game.js"; 4 | import CommentPublicRoute from "./public/comment.js"; 5 | import PostPublicRoute from "./public/post.js"; 6 | 7 | const PublicRoute = Router(); 8 | // User route 9 | PublicRoute.use("/users", UserPublicRoute); 10 | // Post route 11 | PublicRoute.use("/posts", PostPublicRoute); 12 | // Comment route 13 | PublicRoute.use("/comments", CommentPublicRoute); 14 | // Game route 15 | PublicRoute.use("/games", GamePublicRoute); 16 | 17 | export { PublicRoute }; 18 | -------------------------------------------------------------------------------- /backend/routes/public/comment.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import CommentControllers from "../../controllers/comment.js"; 3 | import CommentMiddlewares from "../../middlewares/comment.js"; 4 | 5 | const CommentPublicRoute = Router(); 6 | // Get all comments 7 | CommentPublicRoute.get("/getAll", CommentControllers.getAllComment); 8 | // Get all comments in a post 9 | CommentPublicRoute.get( 10 | "/get/:postId", 11 | CommentMiddlewares.getCommentsInAPost, 12 | CommentControllers.getCommentsInAPost 13 | ); 14 | export default CommentPublicRoute; 15 | -------------------------------------------------------------------------------- /backend/routes/public/game.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import GameControllers from "../../controllers/game.js"; 3 | import GameMiddlewares from "../../middlewares/game.js"; 4 | 5 | const GamePublicRoute = Router(); 6 | // Get all the games 7 | GamePublicRoute.get("/get", GameControllers.getAllGames); 8 | // Get game by its ID 9 | GamePublicRoute.get( 10 | "/get/:gameId", 11 | GameMiddlewares.getGameById, 12 | GameControllers.getGameById 13 | ); 14 | 15 | export default GamePublicRoute; 16 | -------------------------------------------------------------------------------- /backend/routes/public/post.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import PostMiddlewares from "../../middlewares/post.js"; 3 | import PostControllers from "../../controllers/post.js"; 4 | 5 | const PostPublicRoute = Router(); 6 | 7 | // Get all posts 8 | PostPublicRoute.get("/getAll", PostControllers.getAllPosts); 9 | 10 | PostPublicRoute.get("/get", (req, res, next) => { 11 | // Get posts by userId 12 | if (req.query.userId) { 13 | return PostMiddlewares.getPostsByUser(req, res, () => 14 | PostControllers.getPostsByUser(req, res) 15 | ); 16 | } 17 | // Get post by postId 18 | if (req.query.postId) { 19 | return PostMiddlewares.getPostById(req, res, () => 20 | PostControllers.getPostById(req, res) 21 | ); 22 | } 23 | 24 | res.status(400).json({ 25 | success: false, 26 | message: "Missing query: please provide either userId or postId", 27 | }); 28 | }); 29 | 30 | export default PostPublicRoute; 31 | -------------------------------------------------------------------------------- /backend/routes/public/user.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import UserMiddlewares from "../../middlewares/user.js"; 3 | import UserControllers from "../../controllers/user.js"; 4 | import upload from "../../middlewares/upload.js"; 5 | 6 | const UserPublicRoute = Router(); 7 | 8 | // Create user route 9 | UserPublicRoute.post( 10 | "/create", 11 | upload.single("avatar"), 12 | UserMiddlewares.createUser, 13 | UserControllers.createUser 14 | ); 15 | 16 | UserPublicRoute.post( 17 | "/signin", 18 | UserMiddlewares.signinUser, 19 | UserControllers.signinUser 20 | ); 21 | 22 | // Get user info 23 | UserPublicRoute.get( 24 | "/get/:userId", 25 | UserMiddlewares.getUserInfo, 26 | UserControllers.getUserInfo 27 | ); 28 | 29 | export default UserPublicRoute; 30 | -------------------------------------------------------------------------------- /backend/utils/authorize.js: -------------------------------------------------------------------------------- 1 | const authorizeUser = (userId, authorId) => { 2 | try { 3 | if (String(userId) !== String(authorId)) throw new Error("Access denied!"); 4 | return { success: true }; 5 | } catch (error) { 6 | return { success: false, message: error.message, data: null }; 7 | } 8 | }; 9 | 10 | export { authorizeUser }; 11 | -------------------------------------------------------------------------------- /backend/utils/token.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { AT_SECRET_KEY, RT_SECRET_KEY } from "../config.js"; 3 | 4 | const generateToken = (document, type) => { 5 | // If access token, take the first argument, 6 | // Else if refresh token, take the second argument 7 | const getSecretKey = type === "AT" ? AT_SECRET_KEY : RT_SECRET_KEY; 8 | const getExpiration = type === "AT" ? 300 : 3600 * 24 * 7; 9 | const token = jwt.sign(document, getSecretKey, { 10 | expiresIn: getExpiration, 11 | }); 12 | return token; 13 | }; 14 | 15 | const verifyToken = (token, type) => { 16 | const getSecretKey = type === "AT" ? AT_SECRET_KEY : RT_SECRET_KEY; 17 | const verifyToken = jwt.verify(token, getSecretKey); 18 | return verifyToken; 19 | }; 20 | 21 | export { generateToken, verifyToken }; 22 | -------------------------------------------------------------------------------- /backend/utils/upload.js: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from "cloudinary"; 2 | 3 | const handleFileUpload = async (file) => { 4 | try { 5 | const dataUrl = `data:${file.mimetype};base64,${file.buffer.toString( 6 | "base64" 7 | )}`; 8 | const fileName = file.originalname.split(".")[0]; 9 | let data; 10 | await cloudinary.uploader.upload( 11 | dataUrl, 12 | { 13 | public_id: fileName, 14 | resource_type: "auto", 15 | }, 16 | (err, result) => { 17 | if (err) { 18 | // Handle cloudinary upload error 19 | console.error("Cloudinary upload error:", err); 20 | throw new Error("Failed to upload file!"); 21 | } 22 | data = result.secure_url; 23 | } 24 | ); 25 | return { data: data, success: true }; 26 | } catch (error) { 27 | return { success: false, message: error.message, data: null }; 28 | } 29 | }; 30 | 31 | export { handleFileUpload }; 32 | -------------------------------------------------------------------------------- /minigame-frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = crlf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /minigame-frontend/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_SERVER_URL = 2 | -------------------------------------------------------------------------------- /minigame-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /minigame-frontend/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acm-uic/WebMinigames/e1e7199b4aaa16171a4388c5265242a7a5743f22/minigame-frontend/README.md -------------------------------------------------------------------------------- /minigame-frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react/prop-types':'off', 33 | 'react-refresh/only-export-components': [ 34 | 'warn', 35 | { allowConstantExport: true }, 36 | ], 37 | }, 38 | }, 39 | ] 40 | -------------------------------------------------------------------------------- /minigame-frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ACM Minigame 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /minigame-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minigame-frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@lexical/react": "^0.30.0", 14 | "@lexical/rich-text": "^0.30.0", 15 | "lexical": "^0.30.0", 16 | "react": "^18.3.1", 17 | "react-dom": "^18.3.1", 18 | "react-icons": "^5.5.0", 19 | "react-router-dom": "^6.26.2" 20 | }, 21 | "devDependencies": { 22 | "@eslint/js": "^9.9.0", 23 | "@types/react": "^18.3.3", 24 | "@types/react-dom": "^18.3.0", 25 | "@vitejs/plugin-react": "^4.3.4", 26 | "autoprefixer": "^10.4.20", 27 | "eslint": "^9.9.0", 28 | "eslint-plugin-react": "^7.35.0", 29 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 30 | "eslint-plugin-react-refresh": "^0.4.9", 31 | "globals": "^15.9.0", 32 | "postcss": "^8.4.45", 33 | "tailwindcss": "^3.4.10", 34 | "vite": "^6.2.6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /minigame-frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /minigame-frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /minigame-frontend/src/assets/ThumbsDown.jsx: -------------------------------------------------------------------------------- 1 | export function ThumbsDownIcon(props) { 2 | return ( 3 | 4 | 5 | 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /minigame-frontend/src/assets/ThumbsDownFilled.jsx: -------------------------------------------------------------------------------- 1 | export function ThumbsDownFilled(props) { 2 | return ( 3 | 4 | 5 | 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /minigame-frontend/src/assets/ThumbsUp.jsx: -------------------------------------------------------------------------------- 1 | export function ThumbsUpIcon(props) { 2 | return ( 3 | 4 | 5 | 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /minigame-frontend/src/assets/ThumbsUpFilled.jsx: -------------------------------------------------------------------------------- 1 | export function ThumbsUpFilled(props) { 2 | return ( 3 | 4 | 5 | 6 | ) 7 | } -------------------------------------------------------------------------------- /minigame-frontend/src/assets/Trash.jsx: -------------------------------------------------------------------------------- 1 | export function TrashIcon(props) { 2 | return ( 3 | 4 | ) 5 | } -------------------------------------------------------------------------------- /minigame-frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /minigame-frontend/src/components/CommentCard.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import { useState } from "react"; 3 | import { ThumbsDownIcon } from "../assets/ThumbsDown"; 4 | import { ThumbsUpIcon } from "../assets/ThumbsUp"; 5 | import { ThumbsUpFilled } from "../assets/ThumbsUpFilled"; 6 | import { ThumbsDownFilled } from "../assets/ThumbsDownFilled"; 7 | // import {TrashIcon} from "../assets/Trash"; 8 | import "./css/CommentCard.css"; 9 | 10 | const CommentCard = ({ username, comment, likeCount, dislikeCount, lastEditDate, }) => { 11 | 12 | const [isLiked, setIsLiked] = useState(false); 13 | const [isDisliked, setIsDisliked] = useState(false); 14 | const [likeCounter, setLikeCounter] = useState(likeCount); 15 | const [dislikeCounter, setDislikeCounter] = useState(dislikeCount); 16 | 17 | const handleLike = () => { 18 | // If currently disliked, remove the dislike when liking 19 | if (isDisliked) { 20 | setIsDisliked(false); 21 | setDislikeCounter((prevCount) => prevCount - 1); 22 | } 23 | 24 | setIsLiked(!isLiked); 25 | setLikeCounter((prevCount) => (isLiked ? prevCount - 1 : prevCount + 1)); 26 | }; 27 | 28 | const handleDislike = () => { 29 | // If currently liked, remove the like when disliking 30 | if (isLiked) { 31 | setIsLiked(false); 32 | setLikeCounter((prevCount) => prevCount - 1); 33 | } 34 | 35 | setIsDisliked(!isDisliked); 36 | setDislikeCounter((prevCount) => 37 | isDisliked ? prevCount - 1 : prevCount + 1 38 | ); 39 | }; 40 | 41 | return ( 42 |
43 |
44 |
45 |
46 | {username} 47 | Last Edit Date: {lastEditDate} 48 | Report 49 |
50 | 51 |
52 |

{comment}

53 |
54 | 55 |
56 | 60 | 64 |
65 | 66 |
67 |
68 | ); 69 | }; 70 | 71 | CommentCard.propTypes = { 72 | username: PropTypes.string.isRequired, 73 | comment: PropTypes.string.isRequired, 74 | likeCount: PropTypes.number.isRequired, 75 | dislikeCount: PropTypes.number.isRequired, 76 | lastEditDate: PropTypes.string.isRequired, 77 | }; 78 | 79 | export default CommentCard; 80 | -------------------------------------------------------------------------------- /minigame-frontend/src/components/CommentSection.jsx: -------------------------------------------------------------------------------- 1 | import CommentCard from "./CommentCard"; 2 | import "./css/CommentCard.css"; 3 | 4 | const CommentSection = () => { 5 | const commentData = [ 6 | { 7 | username: "Adrian Knight", 8 | comment: 9 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 10 | likeCount: 1, 11 | dislikeCount: 2, 12 | lastEditDate: "March. 06 2024", 13 | }, 14 | { 15 | username: "Thai Nguyen", 16 | comment: 17 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 18 | likeCount: 2, 19 | dislikeCount: 0, 20 | lastEditDate: "March. 05 2024", 21 | }, 22 | { 23 | username: "Trenton Coleman", 24 | comment: 25 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 26 | likeCount: 2, 27 | dislikeCount: 0, 28 | lastEditDate: "March. 04 2024", 29 | }, 30 | { 31 | username: "Minh Ngo", 32 | comment: 33 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 34 | likeCount: 2, 35 | dislikeCount: 0, 36 | lastEditDate: "March. 04 2024", 37 | }, 38 | { 39 | username: "Jack Liu", 40 | comment: 41 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 42 | likeCount: 2, 43 | dislikeCount: 0, 44 | lastEditDate: "March. 03 2024", 45 | }, 46 | { 47 | username: "Daniel Barajas", 48 | comment: 49 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. lorem ispum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", 50 | likeCount: 2, 51 | dislikeCount: 0, 52 | lastEditDate: "March. 03 2024", 53 | }, 54 | ]; 55 | 56 | return ( 57 |
58 | {commentData.map((comment, index) => ( 59 | 66 | ))} 67 |
68 | ); 69 | }; 70 | export default CommentSection; 71 | -------------------------------------------------------------------------------- /minigame-frontend/src/components/GameDetails.jsx: -------------------------------------------------------------------------------- 1 | export default function GameDetails({ game }) { 2 | return ( 3 |
4 | Game Cover 5 | 6 |
7 |

8 | Publisher: {game.publisher} 9 |

10 |

11 | Developer: {game.developer} 12 |

13 |

14 | Release Date: {game.releaseDate} 15 |

16 |
17 | 18 |
19 |

PLAY NOW:

20 | 35 |
36 |
37 | ); 38 | } 39 | 40 | -------------------------------------------------------------------------------- /minigame-frontend/src/components/GameDetailsPopup.jsx: -------------------------------------------------------------------------------- 1 | import GameDetails from "./GameDetails"; 2 | 3 | export default function GameDetailsPopUp({ game, toggleDisplay }) { 4 | return ( 5 |
6 |
7 |
8 |

{game.gameName}

9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 | {game.media.map((img, index) => ( 17 | {`Game 23 | ))} 24 |
25 |
26 |
27 | {game.description} 28 |
29 |
30 |
31 |
32 |
33 | ); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /minigame-frontend/src/components/GameRowComponent.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom" 2 | 3 | export function GameRowComponent(props) { 4 | return ( 5 | 6 |
7 |
8 |
9 |
{props.GameName}
10 |
{props.GameDesc}
11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 | 19 | ); 20 | } -------------------------------------------------------------------------------- /minigame-frontend/src/components/HeroComponent.jsx: -------------------------------------------------------------------------------- 1 | import "./css/Hero.css"; 2 | import { ShowcaseGameCardComponent } from "./ShowcaseGameCardComponent"; 3 | export function HeroComponent(props) { 4 | return ( 5 |
6 |
7 |

Popular games

8 |

9 |

10 | Here's what everyone is playing: 11 |

12 |
13 | 14 |
15 | 19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /minigame-frontend/src/components/Leaderboard.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import "./css/Leaderboard.css"; 3 | import PlayerRank from "./PlayerRank"; 4 | 5 | const Leaderboard = ({ gameName, player = { undefined } }) => { 6 | // state for tracking the selected time range 7 | const [timeRange, setTimeRange] = useState("Today"); 8 | 9 | const examplePlayer = {rank: "1st", name: "Olha", points: 1000} 10 | const allExamplePlayers = [ 11 | examplePlayer, 12 | {rank: "2nd", name: "Adrian", points: 900}, 13 | {rank: "3nd", name: "John", points: 800}, 14 | {rank: "4nd", name: "James", points: 700}, 15 | ] 16 | const handleTimeRangeClick = (range) => { 17 | setTimeRange(range); 18 | }; 19 | 20 | return ( 21 |
22 |
23 |
24 |

{gameName} Game Leaderboard

25 |
26 | 27 | {/* time range selection */} 28 |
29 |
30 | 31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 | Player Icon 42 | 43 | {player.name}Name 44 | 45 |

{player.points}Points

46 |
47 |
48 | Player Icon 52 | 53 | {player.name}Name 54 | 55 |

{player.points}Points

56 |
57 |
58 | Player Icon 62 | 63 | {player.name}Name 64 | 65 |

{player.points}Points

66 |
67 |
68 |
69 |
70 |
71 | {allExamplePlayers.map((item, index) => ( 72 | 73 | ))} 74 |
75 |
76 |
77 |
78 | ); 79 | }; 80 | 81 | export default Leaderboard; 82 | -------------------------------------------------------------------------------- /minigame-frontend/src/components/LikedGames.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom" 2 | 3 | const LikedGames = ({games}) => { 4 | if (!games) { 5 | /* Example data if no games are provided */ 6 | games = [ 7 | {name: "Hotline Miami", link: "/game"}, 8 | {name: "Ori and the Blind Forest", link: "/game"}, 9 | {name: "Little Nightmares", link: "/game"}, 10 | {name: "Metal Gear Solid 3: Snake Eater", link: "/game"}, 11 | {name: "Minecraft", link: "/game"} 12 | ]; 13 | } 14 | 15 | return ( 16 |
17 |

Liked Games

18 | 27 |
28 | ); 29 | }; 30 | 31 | export default LikedGames; -------------------------------------------------------------------------------- /minigame-frontend/src/components/LoginModal.jsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | import { UserContext } from "../domain/UserContext"; 3 | 4 | // Login modal component 5 | const LoginModal = ({ onClose }) => { 6 | const {createUser, loginUser} = useContext(UserContext); 7 | // state variables 8 | const [formMode, setFormMode] = useState("Sign in"); 9 | const [username, setUsername] = useState("") 10 | const [email, setEmail] = useState(""); 11 | const [password, setPassword] = useState(""); 12 | const [confirmPassword, setConfirmPassword] = useState(""); 13 | const [resultError, setResultError] = useState(""); 14 | // errors messages 15 | const [errors, setErrors] = useState({ 16 | username: "", 17 | email: "", 18 | password: "", 19 | confirmPassword: "", 20 | }); 21 | 22 | // password regex for sign up validation 23 | const PASSWORD_REGEX = /^(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+?])[A-Za-z\d!@#$%^&*()_+?]{5,}$/; 24 | 25 | // handle submit for sign in and sign up 26 | const handleSubmit = (e) => { 27 | e.preventDefault(); 28 | setResultError("") 29 | let newErrors = { email: "", password: "", confirmPassword: "" }; 30 | 31 | // validate email 32 | if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { 33 | newErrors.email = "Please enter a valid email address"; 34 | } 35 | 36 | // validate password for sign up 37 | if (formMode === "Sign up" && !PASSWORD_REGEX.test(password)) { 38 | newErrors.password = 39 | "Password needs 1 uppercase, 1 number, 1 special character and 5 characters long"; 40 | } 41 | 42 | // validate confirm password for sign up 43 | if (formMode === "Sign up" && password !== confirmPassword) { 44 | newErrors.confirmPassword = "Passwords do not match"; 45 | } 46 | 47 | // set errors 48 | setErrors(newErrors); 49 | 50 | // if no errors, log validation passed 51 | if (!Object.values(newErrors).some((error) => error)) { 52 | if (formMode == "Sign up") { 53 | createUser(username.trim(), email, password) 54 | .then((res) => res ? onClose() : setResultError("Faied to create account, try a new email")) 55 | } else if (formMode == "Sign in") { 56 | loginUser(email, password) 57 | .then((res) => res ? onClose() : setResultError("Faied to login account, check your username and password")) 58 | } 59 | } 60 | }; 61 | 62 | // handle password change for sign up 63 | const handlePasswordChange = (e) => { 64 | const { value } = e.target; 65 | setPassword(value); 66 | setErrors((prev) => ({ 67 | ...prev, 68 | password: 69 | // if sign up, validate password 70 | formMode === "Sign up" 71 | ? PASSWORD_REGEX.test(value) 72 | ? "" 73 | : prev.password 74 | : "", 75 | })); 76 | }; 77 | 78 | // handle confirm password change for sign up 79 | const handleConfirmPasswordChange = (e) => { 80 | const { value } = e.target; 81 | // set confirm password 82 | setConfirmPassword(value); 83 | setErrors((prev) => ({ 84 | ...prev, 85 | confirmPassword: 86 | // if sign up, validate confirm password equal to password 87 | formMode === "Sign up" && password !== value 88 | ? "Passwords do not match" 89 | : "", 90 | })); 91 | }; 92 | 93 | // handle email change 94 | const handleEmailChange = (e) => { 95 | const { value } = e.target; 96 | // set email 97 | setEmail(value); 98 | setErrors((prev) => ({ 99 | ...prev, 100 | // validate email 101 | email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) 102 | ? "" 103 | : "Invalid email format", 104 | })); 105 | }; 106 | 107 | // handle username change 108 | const handleUsernameChange = (e) => { 109 | const { value } = e.target; 110 | // set username 111 | setUsername(value); 112 | setErrors((prev) => ({ 113 | ...prev, 114 | // validate email 115 | username: username.trim().length >= 3 116 | ? "" 117 | : "At least 3 characters for username", 118 | })); 119 | }; 120 | 121 | // notice words for sign in and sign up 122 | const notice = 123 | formMode === "Sign in" ? "Don't have an account?" : "Have an account?"; 124 | 125 | return ( 126 |
127 |
e.stopPropagation()}> 130 |
131 |

132 | {formMode === "Sign in" ? "Sign in" : "Sign up"} 133 |

134 | {/* Close button */} 135 | 152 |
153 | 154 | {/* Form for sign in and sign up */} 155 |
156 |
157 | 160 | {/* Email input */} 161 | 168 | {/* Email error message */} 169 | {errors.email && ( 170 |

171 | 176 | 182 | 183 | {errors.email} 184 |

185 | )} 186 |
187 | 188 | {/* Form for sign in and sign up */} 189 | {formMode == "Sign up" && ( 190 |
191 | 194 | {/* username input */} 195 | 202 | {/* username error message */} 203 | {errors.username && ( 204 |

205 | 210 | 216 | 217 | {errors.username} 218 |

219 | )} 220 |
221 | )} 222 | 223 | 224 | {/* Password input */} 225 |
226 | 229 | {/* Password input */} 230 | 237 | {/* Password error message */} 238 | {errors.password && ( 239 |

240 | 245 | 251 | 252 | {errors.password} 253 |

254 | )} 255 |
256 | 257 | {/* Confirm password input */} 258 | {formMode === "Sign up" && ( 259 |
260 | 263 | 270 | {/* Confirm password error message */} 271 | {errors.confirmPassword && ( 272 |

273 | 278 | 284 | 285 | {errors.confirmPassword} 286 |

287 | )} 288 |
289 | )} 290 | 291 | {/* Sign in or sign up button */} 292 | {resultError && ( 293 |

294 | 299 | 305 | 306 | {resultError} 307 |

308 | )} 309 | 314 | 315 | {/* Notice words for sign in and sign up */} 316 |
317 | {notice} 318 | 329 |
330 |
331 |
332 |
333 | ); 334 | }; 335 | 336 | export default LoginModal; 337 | -------------------------------------------------------------------------------- /minigame-frontend/src/components/PlayerRank.jsx: -------------------------------------------------------------------------------- 1 | export default function PlayerRank({player}) { 2 | 3 | return ( 4 |
5 |

{player.rank}

6 |

{player.name}

7 | Player Icon 12 |

{player.points}

13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /minigame-frontend/src/components/Popup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | 3 | /* 4 | Popup component acts as a container and is wrapped around the content you want to hover over 5 | and, when hovered, the given component will appear under the initial element. 6 | For example, you can replace the outer div of some elements to make it a popup. 7 | Props: 8 | className: classes to style the element that will trigger the popup 9 | popupClassName: classes to style the element after being hovered over 10 | PopupInfo: component that will be displayed in the popup after the initial element 11 | children: children of the Popup component (automatically added when Popup is wrapped around other elements) 12 | */ 13 | const Popup = ({ className, popupClassName, PopupInfo, children }) => { 14 | const [isVisible, setIsVisible] = useState(false); 15 | 16 | return ( 17 |
setIsVisible(true)} onMouseLeave={() => setIsVisible(false)}> 18 | {/* Initial Element */} 19 |
20 | {children} 21 |
22 | 23 | {/* Popup Element (displays over intial element) */} 24 |
25 | {/* Copy of initial element */} 26 |
27 | {children} 28 |
29 | {/* Given component */} 30 | {PopupInfo} 31 |
32 |
33 | ) 34 | } 35 | 36 | export default Popup -------------------------------------------------------------------------------- /minigame-frontend/src/components/PostView.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom" 2 | import { useState } from "react" 3 | import Popup from "./Popup.jsx" 4 | import ProfilePopup from "./ProfilePopup.jsx" 5 | 6 | const PostView = ({post, username="Guest", icon="https://t4.ftcdn.net/jpg/02/15/84/43/240_F_215844325_ttX9YiIIyeaR7Ne6EaLLjMAmy4GvPC69.jpg", className }) => { 7 | const [imgIndex, setImgIndex] = useState(0) 8 | 9 | /* Format all given images 10 | If none are provided, body will take up image space 11 | If a link or an array of length 1, image will be displayed 12 | If an array of more than one image, images will be displayed as a carousel */ 13 | const displayImgs = () => { 14 | if (post.imgs == null || post.imgs.length == 0) 15 | return 16 | else if (Array.isArray(post.imgs) && post.imgs.length > 1) { 17 | return ( 18 |
19 | 20 | {/* Previous Image Button */} 21 | 23 | {/* Next Image Button */} 24 | 26 |
{imgIndex+1}/{post.imgs.length}
27 |
28 | ) 29 | } else if (Array.isArray(post.imgs)) { 30 | return ( 31 |
32 | 33 |
34 | ) 35 | } else { 36 | return ( 37 |
38 | 39 |
40 | ) 41 | } 42 | } 43 | 44 | /* Post View Div */ 45 | return ( 46 |
47 |
48 | {post.title} 49 |
50 |
51 | 52 |
53 | {/* Username and Body Column */} 54 |
55 | } > 58 | 59 | {username} 60 | 61 | 62 |

63 | {post.body} 64 |

65 |
66 | 67 | {/* Display image only if provided in imgs array */} 68 | {displayImgs()} 69 |
70 |
71 | ) 72 | } 73 | 74 | export default PostView -------------------------------------------------------------------------------- /minigame-frontend/src/components/ProfileBioComponent.jsx: -------------------------------------------------------------------------------- 1 | import "./css/ProfileBio.css"; 2 | import { useEffect, useState } from "react"; 3 | 4 | export function ProfileBioComponent(props) { 5 | const { user,handleEdit, updateBio, canEdit, editing } = props; 6 | const [newImageLink, setNewImageLink] = useState(user.avatar); 7 | const [newUser, setnewUser] = useState(user.userName); 8 | const [newBio, setNewBio] = useState(user.bio); 9 | useEffect(()=> { 10 | if (!editing) { 11 | setNewImageLink(user.avatar); 12 | setNewBio(user.bio); 13 | setnewUser(user.userName); 14 | } 15 | }, [editing, user]) 16 | 17 | if (!editing) { 18 | return ( 19 |
20 |
21 | {canEdit ? : ""} 24 |
25 | Profile Picture 27 | 28 |

{user.userName}

29 |

biography

30 |

31 |

{user.bio}

32 |

33 |
34 | ); 35 | } else { 36 | return ( 37 |
38 |
39 | 42 |
43 | Profile Picture 44 | setNewImageLink(event.target.value)}/> 45 |
46 |

{user.userName}

47 |
48 | setnewUser(event.target.value)}/> 49 |
50 |

biography

51 |

52 |

{user.bio}

53 |