diff --git a/.gitignore b/.gitignore index 2fd2304..c255fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ build # misc .DS_Store .env +.env.development .env.local .env.development.local .env.test.local diff --git a/README.md b/README.md index 86e8218..6af967c 100644 --- a/README.md +++ b/README.md @@ -1 +1,108 @@ -# Coderspace MERN WebRTC Project +![coderspace](https://socialify.git.ci/mrExplorist/coderspace/image?description=1&descriptionEditable=Elevate%20your%20Coding%20Experience%3A%20Coderspace%20-%20Unleash%20the%20Potential%20of%20Real-time%20Collaboration%20and%20Conference%20Meets&font=Source%20Code%20Pro&forks=1&issues=1&language=1&name=1&owner=1&pattern=Plus&theme=Light) + +# Coderspace 🚀 + +Elevate your Coding Experience: Coderspace - Unleash the Potential of Real-time Collaboration and Conference Meets! (Actively building 🏗 ) 💻🔥 + +## Features 🔥 + +- 💬 **Conference Meetings**: Conduct virtual conference meetings with multiple participants, including audio and video capabilities, screen sharing, and chat functionality. 🎤🎥🖥️💬 +- 🤝 **Pair Programming**: Collaborate with other developers in real-time, allowing simultaneous code editing, chat, and audio communication. 👥💻💬🎤 +- 🔄 **Code Sync**: Enable synchronized code editing in real-time, allowing all participants to see and interact with code changes as they happen. 🔄💻 +- 📝 **Live Whiteboard**: Facilitate brainstorming and visual explanations through a shared whiteboard where participants can draw, write, and annotate. 📝🎨 +- ✋ **Raise Hand**: Participants can raise their hand to indicate that they have a question or need assistance, triggering a visual notification for others. + +## Technologies Used 💻 + +- **Frontend**: React, Redux +- **Backend**: Node.js, Express.js, MongoDB +- **Real-time Communication**: WebRTC, Socket.IO +- **Authentication**: Twilio API for OTP (One-Time Password) authentication +- **Deployment**: Docker, AWS (Amazon Web Services), or any preferred hosting service + +## Getting Started 🚀 + +To set up Coderspace locally for development and testing, follow these steps: + +- This documentation will guide you through the process of setting up Coderspace locally on your machine. By following these steps, you will be able to run the application, and integrate Twilio for OTP service. + +Prerequisites: + +- Node.js (v14.x or higher) and npm (v6.x or higher) installed on your machine +- MongoDB installed locally (v4.x or higher) +- Twilio account with Account SID, Auth Token, and a registered phone number + +Step 1: Clone the Repository + +1. Open your terminal or command prompt. +2. Change to the directory where you want to clone the repository. +3. Run the following command to clone the repository: + ``` + git clone https://github.com/your-username/coderspace.git + ``` + Replace `your-username` with your actual GitHub username. + +Step 2: Install Dependencies on frontend and backend: + +1. Navigate to the project's root directory: + ``` + cd coderspace + ``` +2. Run the following command to install the project dependencies in both frontend and backend directory: + ``` + npm install + ``` + +Step 3: Configure Twilio for OTP Service + +1. Sign up for a Twilio account at https://www.twilio.com and obtain your Account SID, Auth Token, and a registered phone number. +2. Open the `.env` file in the project's root directory. +3. Set the `SMS_SID` variable to your Twilio Account SID. +4. Set the `SMS_AUTH_TOKEN` variable to your Twilio Auth Token. +5. Set the `SMS_FROM_NUMBER` variable to your registered Twilio phone number. + +Step 4: Set Up Environment Variables + +1. In the project's root directory, locate the `.env.example` file. +2. Create a new file named `.env` in both backend and frontend directory and copy the contents of `.env.example` and `.env.dev` respectively. +3. Modify the values in the `.env` file in backend directory to match your local configuration. Make sure to set the necessary variables, such as database connection information, Twilio Account SID, Auth Token, and Twilio phone number. +4. Set the SECRETS in the `.env` file by `crypto.randomBytes(length).toString('hex')`. It is a Node.js method that generates a random string of bytes with the specified length and converts it to a hexadecimal string representation which you can use to generate strong SECRET VARIABLES. You can set secrets to be any random generated string. + +Step 5: Start MongoDB + +1. Open a new terminal or command prompt window. +2. Start the MongoDB service by running the following command: + ``` + mongod + ``` + +Step 6: Run the Application + +1. In the original terminal or command prompt window, navigate to the project's root directory if you're not already there. +2. Run the following command to start the application: + ``` + cd /frontend + npm start + cd ../backend + npm run dev + ``` + This will start the server and the client development server. + +Step 7: Access the Application + +1. Open your web browser and visit `http://localhost:3000` to access the Coderspace application. + +Congratulations! You have successfully set up Coderspaces locally, including the integration of Twilio for OTP service and now it's a time to collaborate .. + +Note: Remember to keep your local environment variables and configurations secure and do not commit them to any version control system. + +Step 8: Collaborate: + +Contributions to the collaboration feature in our Coderspace project are welcome! If you would like to contribute, please follow these guidelines: + +- Fork the repository and create a new branch for your feature or bug fix. +- Commit your changes with descriptive commit messages. +- Push your changes to your forked repository. +- Submit a pull request detailing your changes and explaining the purpose and benefits of the contribution. + +We appreciate your contributions to making Coderspace an even better coding community app! diff --git a/backend/.env.dev b/backend/.env.dev deleted file mode 100644 index dbe78c4..0000000 --- a/backend/.env.dev +++ /dev/null @@ -1,13 +0,0 @@ -HASH_SECRET=4842eb067444da837acf4832311e0bbd6a8e6452da633ed3bc93afe0f66ea0095f4c2f4538b3a951211a9b3c463db70aae22117e9f14f391b91ab6ee72da2d1a -SMS_SID=ACfac66ed39655343fad1bf93e46bfe4f8 -SMS_AUTH_TOKEN=8859740e6881fc19ee280ae32e799e01 -SMS_FROM_NUMBER="+13203355676" - - -DB_URL=mongodb://mongodb:27017/coderspace -# DB_URL=mongodb+srv://MrExplorist:EcS81gdiDOFDBzkH@coderspace.hxef2w1.mongodb.net/?retryWrites=true&w=majority -JWT_ACCESS_TOKEN_SECRET=2cd9026bab6153cecdeb3ef1a2981b4b759819d774cde2a72252068f2f2a06e5885152e27de8a8dbb8090874a7f00e4aa92e6de7f4349276a645362ff33dcb32 -JWT_REFRESH_TOKEN_SECRET=a386573153bf950abcc028ab147959f695bcede7d2f7a24f35c9182cccc19d0421b6ab2977342f1bf0b8c607afbc5fe3cacb1fa802828dd62797d9c001ec4d08 -BASE_URL=http://localhost:5500 -FRONT_URL=http://localhost:3000 -NODE_ENV=development \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..433952c --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,17 @@ + +#Secrets you will create by running crypto.randomBytes(anylengthhererecommended:12).toString('hex') by running node in terminal please ensure that each secret is unique +HASH_SECRET= +JWT_ACCESS_TOKEN_SECRET= +JWT_REFRESH_TOKEN_SECRET= + +# You will get this from twilio setup +SMS_SID= +SMS_AUTH_TOKEN= +SMS_FROM_NUMBER="" + +# Running mongodb locally + +DB_URL=mongodb://localhost:27017/coderspace +BASE_URL=http://localhost:5500 +FRONT_URL=http://localhost:3000 +NODE_ENV=development \ No newline at end of file diff --git a/backend/actions.js b/backend/actions.js index 465f850..4ed2713 100644 --- a/backend/actions.js +++ b/backend/actions.js @@ -1,5 +1,10 @@ const ACTIONS = { JOIN: "join", + JOINED: "joined", + DISCONNECTED: "disconnected", + CODE_CHANGE: "code-change", + SYNC_CODE: "sync-code", + LEAVE: "leave", ADD_PEER: "add-peer", REMOVE_PEER: "remove-peer", diff --git a/backend/server.js b/backend/server.js index 9106a58..9dc422b 100644 --- a/backend/server.js +++ b/backend/server.js @@ -55,7 +55,7 @@ const io = require("socket.io")(server, { const socketUserMap = {}; -io.on("connection", (socket) => { +io.on("connection", socket => { console.log("new connection", socket.id); socket.on(ACTIONS.JOIN, ({ roomId, user }) => { socketUserMap[socket.id] = user; @@ -66,7 +66,7 @@ io.on("connection", (socket) => { // Getting all the clients - clients.forEach((clientId) => { + clients.forEach(clientId => { io.to(clientId).emit(ACTIONS.ADD_PEER, { peerId: socket.id, createOffer: false, @@ -117,7 +117,7 @@ io.on("connection", (socket) => { socket.on(ACTIONS.MUTE, ({ roomId, userId }) => { const clients = Array.from(io.sockets.adapter.rooms.get(roomId) || []); - clients.forEach((clientId) => { + clients.forEach(clientId => { io.to(clientId).emit(ACTIONS.MUTE, { peerId: socket.id, userId, @@ -129,7 +129,7 @@ io.on("connection", (socket) => { socket.on(ACTIONS.UNMUTE, ({ roomId, userId }) => { const clients = Array.from(io.sockets.adapter.rooms.get(roomId) || []); - clients.forEach((clientId) => { + clients.forEach(clientId => { io.to(clientId).emit(ACTIONS.UNMUTE, { peerId: socket.id, userId, @@ -137,16 +137,36 @@ io.on("connection", (socket) => { }); }); + //& setting up an event listener for the ACTIONS.CODE_CHANGE event. When this event is received, it executes a callback function with the provided payload { roomId, userId, message }. + + socket.on(ACTIONS.CODE_CHANGE, ({ roomId, code }) => { + // console.log("Receiving", code); + + // io.to(roomId).emit(ACTIONS.CODE_CHANGE, { + // code, + // }); + + // doing the same thing as above but with socket.in the same room as the client we received from the code from and emitting the code to all the clients in the room except the client we received the code from (socket.id) using socket.in(roomId).emit() instead of io.to(roomId).emit(). This is because we don't want to send the code back to the client who sent it in the first place. This is a common pattern in Socket.IO to send events to all clients in a room except the sender.This is equivalent to the following code: socket.broadcast.to(roomId).emit(ACTIONS.CODE_CHANGE, { code }); + + socket.in(roomId).emit(ACTIONS.CODE_CHANGE, { + code, + }); + }); + socket.on(ACTIONS.SYNC_CODE, ({ peerId, code }) => { + io.to(peerId).emit(ACTIONS.CODE_CHANGE, { + code, + }); + }); // Leaving the room const leaveRoom = ({ roomId }) => { // getting all the rooms in the socket const { rooms } = socket; // room map - Array.from(rooms).forEach((roomId) => { + Array.from(rooms).forEach(roomId => { const clients = Array.from(io.sockets.adapter.rooms.get(roomId) || []); - clients.forEach((clientId) => { + clients.forEach(clientId => { io.to(clientId).emit(ACTIONS.REMOVE_PEER, { peerId: socket.id, userId: socketUserMap[socket.id]?.id, @@ -172,4 +192,3 @@ io.on("connection", (socket) => { }); server.listen(PORT, () => console.log(`Listening on port ${PORT}`)); -// what is this code all about ? diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5fa075b..77948de 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,20 +8,25 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@codemirror/basic-setup": "^0.20.0", "@reduxjs/toolkit": "^1.9.5", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.4.0", + "codemirror": "^5.65.2", "freeice": "^2.2.2", "react": "^18.2.0", + "react-avatar": "^5.0.3", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", "react-icons": "^4.8.0", "react-lottie-player": "^1.5.4", "react-redux": "^8.0.7", "react-router-dom": "^6.10.0", "react-scripts": "5.0.1", "socket.io-client": "^4.6.2", + "uuid": "^9.0.0", "web-vitals": "^2.1.4" } }, @@ -2019,6 +2024,91 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "license": "MIT" }, + "node_modules/@codemirror/autocomplete": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-0.20.3.tgz", + "integrity": "sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw==", + "dependencies": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/basic-setup/-/basic-setup-0.20.0.tgz", + "integrity": "sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg==", + "deprecated": "In version 6.0, this package has been renamed to just 'codemirror'", + "dependencies": { + "@codemirror/autocomplete": "^0.20.0", + "@codemirror/commands": "^0.20.0", + "@codemirror/language": "^0.20.0", + "@codemirror/lint": "^0.20.0", + "@codemirror/search": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.20.0.tgz", + "integrity": "sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==", + "dependencies": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/language": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz", + "integrity": "sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0", + "@lezer/highlight": "^0.16.0", + "@lezer/lr": "^0.16.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-0.20.3.tgz", + "integrity": "sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.2", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.20.1.tgz", + "integrity": "sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz", + "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==" + }, + "node_modules/@codemirror/view": { + "version": "0.20.7", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz", + "integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@csstools/normalize.css": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz", @@ -2868,6 +2958,27 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "license": "MIT" }, + "node_modules/@lezer/common": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz", + "integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==" + }, + "node_modules/@lezer/highlight": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz", + "integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==", + "dependencies": { + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@lezer/lr": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz", + "integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==", + "dependencies": { + "@lezer/common": "^0.16.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -5458,6 +5569,14 @@ "node": ">=10" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/check-types": { "version": "11.2.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.2.tgz", @@ -5639,6 +5758,11 @@ "node": ">=4" } }, + "node_modules/codemirror": { + "version": "5.65.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.2.tgz", + "integrity": "sha512-SZM4Zq7XEC8Fhroqe3LxbEEX1zUPWH1wMr5zxiBuiUF64iYOUH/JI88v4tBag8MiBS8B8gRv8O1pPXGYXQ4ErA==" + }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -5894,6 +6018,11 @@ "node": ">=10" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5908,6 +6037,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -8350,6 +8487,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", + "integrity": "sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -9003,6 +9148,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -9208,6 +9358,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-retina": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-retina/-/is-retina-1.0.3.tgz", + "integrity": "sha512-/tCmbIETZwCd8uHWO+GvbRa7jxwHFHdfetHfiwoP0aN9UDf3prUJMtKn7iBFYipYhqY1bSTjur8hC/Dakt8eyw==" + }, "node_modules/is-root": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", @@ -10743,6 +10898,16 @@ "tmpl": "1.0.5" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", @@ -13189,6 +13354,21 @@ "node": ">=14" } }, + "node_modules/react-avatar": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-avatar/-/react-avatar-5.0.3.tgz", + "integrity": "sha512-DNc+qkWH9QehSEZqHBhqpXWsPY+rU9W7kD68QFHfu8Atfsvx/3ML0DzAePgTUd96nCXQQ3KZMcC3LKYT8FiBIg==", + "dependencies": { + "is-retina": "^1.0.3", + "md5": "^2.0.0" + }, + "peerDependencies": { + "@babel/runtime": ">=7", + "core-js-pure": ">=3", + "prop-types": "^15.0.0 || ^16.0.0", + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -13251,6 +13431,21 @@ "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", "license": "MIT" }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-icons": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz", @@ -14357,6 +14552,14 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -14725,6 +14928,11 @@ "webpack": "^5.0.0" } }, + "node_modules/style-mod": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.3.tgz", + "integrity": "sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==" + }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -15665,10 +15873,9 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "bin": { "uuid": "dist/bin/uuid" } @@ -15714,6 +15921,11 @@ "browser-process-hrtime": "^1.0.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/w3c-xmlserializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 86ec920..25d1e08 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,20 +3,25 @@ "version": "0.1.0", "private": true, "dependencies": { + "@codemirror/basic-setup": "^0.20.0", "@reduxjs/toolkit": "^1.9.5", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.4.0", + "codemirror": "^5.65.2", "freeice": "^2.2.2", "react": "^18.2.0", + "react-avatar": "^5.0.3", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", "react-icons": "^4.8.0", "react-lottie-player": "^1.5.4", "react-redux": "^8.0.7", "react-router-dom": "^6.10.0", "react-scripts": "5.0.1", "socket.io-client": "^4.6.2", + "uuid": "^9.0.0", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/frontend/src/App.css b/frontend/src/App.css index d088f59..fdda73b 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -58,3 +58,10 @@ body { justify-content: center; min-height: calc(100vh - 190px); } + +.CodeMirror { + min-height: calc(100vh - 100px); + font-size: 20px; + line-height: 1.6; + padding-top: 20px; +} diff --git a/frontend/src/App.js b/frontend/src/App.js index 514254b..c87c21f 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -5,6 +5,7 @@ import { Route, BrowserRouter as Router, Routes, + useLocation, } from "react-router-dom"; import Activate from "./pages/Activate/Activate"; @@ -13,11 +14,15 @@ import Home from "./pages/Home/Home"; import Navigation from "./components/shared/Navigation/Navigation"; import Rooms from "./pages/Rooms/Rooms"; import { useSelector } from "react-redux"; -import { useState } from "react"; + import { useLoadingWithRefresh } from "./hooks/useLoadingWithRefresh"; import Loader from "./components/shared/Loader/Loader"; import Room from "./pages/Room/Room"; +import { Toaster } from "react-hot-toast"; +import EditorHome from "./pages/Editor/EditorHome/EditorHome"; +import EditorPage from "./pages/Editor/EditorPage/EditorPage"; + // Root component function App() { @@ -28,67 +33,86 @@ function App() { return loading ? ( ) : ( - - - - - - - } - /> - {/* } /> */} - - {/* element={} */} - - - - } - /> - - - - } - /> - - - - } - /> - - - - - } - /> - - {/* element={} - } /> */} - - {/* - - - } - /> */} - - + <> +
+ +
+ + + + + + + + + } + /> + + + + + } + /> + + + + } + /> + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + } + /> + + + ); } @@ -106,6 +130,7 @@ function SemiProtectedRoute({ children }) { ); } + function ProtectedRoute({ children }) { const { user, isAuth } = useSelector((state) => state.auth); return !isAuth ? ( @@ -117,24 +142,4 @@ function ProtectedRoute({ children }) { ); } -// const GuestRoute = ({ children, ...rest }) => { -// return ( -// { -// return isAuth ? : children; -// }} -// > -// ); -// }; -// function SemiProtectedRoute({ children }) { -// return !isAuth ? ( -// -// ) : isAuth && !user.isActivated ? ( -// <>{children} -// ) : ( -// -// ); -// } - export default App; diff --git a/frontend/src/App.module.css b/frontend/src/App.module.css index 7c17820..e69de29 100644 --- a/frontend/src/App.module.css +++ b/frontend/src/App.module.css @@ -1,6 +0,0 @@ -.cardWrapper { - display: flex; - align-items: center; - justify-content: center; - margin-top: 6rem; -} diff --git a/frontend/src/actions.js b/frontend/src/actions.js index 6c9c0c5..8b09cbb 100644 --- a/frontend/src/actions.js +++ b/frontend/src/actions.js @@ -1,5 +1,10 @@ export const ACTIONS = { JOIN: "join", + JOINED: "joined", + DISCONNECTED: "disconnected", + CODE_CHANGE: "code-change", + SYNC_CODE: "sync-code", + LEAVE: "leave", ADD_PEER: "add-peer", REMOVE_PEER: "remove-peer", diff --git a/frontend/src/components/Client/Client.js b/frontend/src/components/Client/Client.js new file mode 100644 index 0000000..86ccbbc --- /dev/null +++ b/frontend/src/components/Client/Client.js @@ -0,0 +1,14 @@ +import React from "react"; +import styles from "./Client.module.css"; +import Avatar from "react-avatar"; + +const Client = ({ username }) => { + return ( +
+ + {username} +
+ ); +}; + +export default Client; diff --git a/frontend/src/components/Client/Client.module.css b/frontend/src/components/Client/Client.module.css new file mode 100644 index 0000000..a29d71b --- /dev/null +++ b/frontend/src/components/Client/Client.module.css @@ -0,0 +1,8 @@ +.client { + display: flex; + align-items: center; + flex-direction: column; +} +.userName { + color: #a0e4ba; +} diff --git a/frontend/src/components/Editor/Editor.js b/frontend/src/components/Editor/Editor.js new file mode 100644 index 0000000..320a0e4 --- /dev/null +++ b/frontend/src/components/Editor/Editor.js @@ -0,0 +1,86 @@ +import React, { useEffect, useRef } from "react"; +import Codemirror from "codemirror"; +import "codemirror/lib/codemirror.css"; +import "codemirror/theme/dracula.css"; +import "codemirror/mode/javascript/javascript.js"; +import "codemirror/addon/edit/closebrackets.js"; +import "codemirror/addon/edit/matchbrackets.js"; +import "codemirror/addon/edit/closetag.js"; +import "codemirror/addon/edit/matchtags.js"; +import "codemirror/addon/fold/foldgutter.js"; +import "codemirror/addon/fold/foldcode.js"; +import "codemirror/addon/fold/foldgutter.css"; +import "codemirror/addon/fold/brace-fold.js"; +import "codemirror/addon/fold/comment-fold.js"; + +import { ACTIONS } from "../../actions"; + +const Editor = ({ socketRef, roomId, onCodeChange }) => { + const editorRef = useRef(null); + + useEffect(() => { + async function loadEditor() { + editorRef.current = Codemirror.fromTextArea( + document.getElementById("realTimeEditor"), + { + mode: { name: "javascript", json: true }, + lineNumbers: true, + theme: "dracula", + lineWrapping: true, + autoCloseBrackets: true, + matchBrackets: true, + autoCloseTags: true, + matchTags: true, + foldGutter: true, + gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], + }, + ); + + // Listen for changes in the editor and send them to the server over Socket.io server socket connection + + editorRef.current.on("change", (instance, changes) => { + // console.log("changes", changes); + + const { origin } = changes; + + const code = instance.getValue(); + // console.log(code); + + if (origin !== "setValue") { + // console.log("emitting code change", code); + socketRef.current?.emit(ACTIONS.CODE_CHANGE, { + roomId, + code, + }); + } + // console.log(code); + onCodeChange(code); + }); + + // Listen for changes in the editor and send them to the server over Socket.io server socket connection + } + + loadEditor(); + }, []); + + useEffect(() => { + // console.log("socketRef.current", socketRef.current); + if (socketRef.current) { + socketRef.current?.on(ACTIONS.CODE_CHANGE, ({ code }) => { + // console.log("emitting code change", code); + if (code != null) { + editorRef.current?.setValue(code); + } + }); + } + return () => { + if (socketRef.current) { + socketRef.current?.off(ACTIONS.CODE_CHANGE); + } + }; + }, [socketRef.current]); + + return ; +}; + +export default Editor; diff --git a/frontend/src/components/shared/Navigation/Navigation.jsx b/frontend/src/components/shared/Navigation/Navigation.jsx index 65d5489..890547e 100644 --- a/frontend/src/components/shared/Navigation/Navigation.jsx +++ b/frontend/src/components/shared/Navigation/Navigation.jsx @@ -1,7 +1,7 @@ import { Link } from "react-router-dom"; import React from "react"; import { TbBrandSpacehey } from "react-icons/tb"; -import { RiLogoutCircleRFill } from "react-icons/ri"; +import { IoLogOut } from "react-icons/io5"; import styles from "./Navigation.module.css"; import { logout } from "../../../http"; import { setAuth } from "../../../store/authSlice"; @@ -22,7 +22,7 @@ const Navigation = () => { const dispatch = useDispatch(); - const { isAuth, user } = useSelector((state) => state.auth); + const { isAuth, user } = useSelector(state => state.auth); async function logoutUser() { try { @@ -57,7 +57,7 @@ const Navigation = () => { {isAuth && ( + + + If you don't have an invite then create   + + new Room + + + + + + ); +}; + +export default EditorHome; diff --git a/frontend/src/pages/Editor/EditorHome/EditorHome.module.css b/frontend/src/pages/Editor/EditorHome/EditorHome.module.css new file mode 100644 index 0000000..70ad651 --- /dev/null +++ b/frontend/src/pages/Editor/EditorHome/EditorHome.module.css @@ -0,0 +1,124 @@ +.mainWrapper { + display: flex; + align-items: center; + justify-content: center; + min-height: calc(100vh - 190px); + /* back + ground: #fff; */ +} + +/* align logo to the leftwithin the card container */ +.cardContainer { + display: flex; + align-items: center; + justify-content: flex-end; + background-color: #fff; + padding: 16px; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.syncLogo { + display: flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + margin-right: 16px; + background-color: #f8f8f8; + border-radius: 50%; +} +.right { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; +} + +.heading { + width: 100%; + text-align: center; + font-size: 1.8rem; + font-weight: 600; + margin-top: 1rem; +} +.subHeading { + color: rgb(213, 252, 151); + font-size: 1.2rem; + font-weight: 500; +} + +footer { + position: fixed; + bottom: 0; + margin-bottom: 1rem; +} + +.mainLabel { + font-size: 16px; + font-weight: 600; + color: #487; + margin-top: 16px; + margin-right: auto; +} + +footer a { + color: #4aee88; + text-decoration: none; +} + +.inputGroup { + display: flex; + flex-direction: column; + margin-bottom: 1rem; + width: 100%; +} + +.inputBox { + width: 100%; + padding: 0.5rem; + border: 1px solid #4aee88; + border-radius: 0.5rem; + background-color: #eee; + font-size: 1rem; + font-weight: 600; + outline: none; + margin: 0.5rem 0; +} +.joinBtn { + padding: 0.5rem; + border: 1px solid #4aee88; + border-radius: 0.5rem; + color: #000; + font-size: 1rem; + font-weight: 500; + outline: none; + cursor: pointer; + transition: all 0.3s ease-in-out; + margin-left: auto; + width: 100px; +} + +.joinBtn:hover { + background-color: #4aee88; + color: #000; +} + +.createInfo { + margin: 0 auto; + margin-top: 20px; +} + +.createNewBtn { + color: #4aee88; + text-decoration: none; + font-size: 1rem; + font-weight: 600; + border-bottom: 1px solid #4aee88; + transition: all 0.3s ease-in-out; + cursor: pointer; +} +.createNewBtn:hover { + color: #fff; + border-bottom: 1px solid #fff; +} diff --git a/frontend/src/pages/Editor/EditorPage/EditorPage.js b/frontend/src/pages/Editor/EditorPage/EditorPage.js new file mode 100644 index 0000000..ad83f45 --- /dev/null +++ b/frontend/src/pages/Editor/EditorPage/EditorPage.js @@ -0,0 +1,95 @@ +import React from "react"; +import { toast } from "react-hot-toast"; +import { MdScreenShare } from "react-icons/md"; +import { useSelector } from "react-redux"; +import { useNavigate, useParams } from "react-router-dom"; +import Client from "../../../components/Client/Client"; +import Editor from "../../../components/Editor/Editor"; +import { useWebRTC } from "../../../hooks/useWebRTC"; +import styles from "./EditorPage.module.css"; + +const EditorPage = () => { + const { id: roomId } = useParams(); + + // const location = useLocation(); + const reactNavigator = useNavigate(); + + const { user } = useSelector(state => state.auth); //taking user from global state + + const { clients, socket, codeRef } = useWebRTC(roomId, user); + + // const socketRef = useRef(); + // useEffect(() => { + // const init = async () => { + // socketRef.current = await socketInit(); + // socketRef.current.on("connect_error", (err) => handleErrors(err)); + // socketRef.current.on("connect_failed", (err) => handleErrors(err)); + // console.log(socketRef.current); + + // function handleErrors(e) { + // console.log("socket error", e); + // toast.error("Socket connection failed, try again later."); + // reactNavigator("/"); + // } + + // socketRef.current.emit(ACTIONS.JOIN, { + // roomId, + // user: location.state?.username, + // }); + // }; + + // init(); + // }, []); + // if (!location.state) { + // return ; + // } + + const copyRoomId = async () => { + try { + await navigator.clipboard.writeText(roomId); + toast.success("ROOM ID copied to clipboard"); + } catch (error) { + toast.error("Failed to copy ROOM ID"); + console.log(error); + } + }; + + const leaveRoom = () => { + reactNavigator("/"); + }; + + return ( +
+
+
+
+ +
+

Connected

+
+ {clients.map(client => ( + + ))} +
+
+ + +
+
+ { + codeRef.current = code; + }} + /> +
+
+ ); +}; + +export default EditorPage; diff --git a/frontend/src/pages/Editor/EditorPage/EditorPage.module.css b/frontend/src/pages/Editor/EditorPage/EditorPage.module.css new file mode 100644 index 0000000..0d2dd5d --- /dev/null +++ b/frontend/src/pages/Editor/EditorPage/EditorPage.module.css @@ -0,0 +1,68 @@ +.mainWrap { + display: grid; + grid-template-columns: 260px 1fr; + height: 90vh; +} + +.aside { + padding: 16px; + /* border-right: 2px solid #e0e0e0; */ + display: flex; + flex-direction: column; +} + +.asideInner { + flex: 1; +} +.syncLogo { + display: flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + background-color: #f8f8f8; + border-radius: 10%; + margin-bottom: 1rem; + /* justify-content: space-between; */ +} + +.asideInner h3 { + font-size: 1rem; + font-weight: 600; + color: #4aee88; + border-bottom: 1px solid #4aee88; +} +.asideInner .logoName { + font-size: 1.5rem; + font-weight: 600; +} +.clientsList { + display: flex; + flex-wrap: wrap; + margin-top: 1rem; + gap: 10px; +} + +.copyBtn { + padding: 0.3rem; + border: 1px solid #4aee88; + border-radius: 0.5rem; + color: #000; + font-size: 1rem; + font-weight: 600; + outline: none; + cursor: pointer; + transition: all 0.3s ease-in-out; +} +.leaveBtn { + padding: 0.3rem; + border: 1px solid #4aee88; + border-radius: 0.5rem; + color: #000; + font-size: 1rem; + font-weight: 600; + outline: none; + cursor: pointer; + margin-top: 1rem; + background: #4aee88; +} diff --git a/frontend/src/pages/RegSteps/StepOtp/StepOtp.jsx b/frontend/src/pages/RegSteps/StepOtp/StepOtp.jsx index 7766b9a..446cff2 100644 --- a/frontend/src/pages/RegSteps/StepOtp/StepOtp.jsx +++ b/frontend/src/pages/RegSteps/StepOtp/StepOtp.jsx @@ -28,7 +28,7 @@ const StepOtp = ({ onNext }) => { } return ( <> -
+
} diff --git a/frontend/src/pages/Room/Room.jsx b/frontend/src/pages/Room/Room.jsx index 48e73d5..79033e6 100644 --- a/frontend/src/pages/Room/Room.jsx +++ b/frontend/src/pages/Room/Room.jsx @@ -4,7 +4,7 @@ import { useNavigate, useParams } from "react-router-dom"; import { useSelector } from "react-redux"; -import { BsArrowLeft, BsFillMicFill } from "react-icons/bs"; +import { BsArrowLeft } from "react-icons/bs"; import { IoMdMicOff } from "react-icons/io"; import { FaHandPeace, FaHandSparkles } from "react-icons/fa"; import styles from "./Room.module.css"; diff --git a/frontend/src/pages/Rooms/Rooms.jsx b/frontend/src/pages/Rooms/Rooms.jsx index 3fca89c..19fb39e 100644 --- a/frontend/src/pages/Rooms/Rooms.jsx +++ b/frontend/src/pages/Rooms/Rooms.jsx @@ -5,6 +5,8 @@ import { BiSearchAlt2 } from "react-icons/bi"; import RoomCard from "../../components/RoomCard/RoomCard"; import AddRoomModal from "../../components/Modals/AddRoomModal/AddRoomModal"; import { getAllRooms } from "../../http"; +import { TbSourceCode } from "react-icons/tb"; +import { useNavigate } from "react-router-dom"; // const rooms = [ // { @@ -101,6 +103,7 @@ import { getAllRooms } from "../../http"; // ]; const Rooms = () => { + const navigate = useNavigate(); const [showModal, setShowModal] = useState(false); const [rooms, setRooms] = useState([]); @@ -134,6 +137,14 @@ const Rooms = () => { Start a room + +
diff --git a/frontend/src/pages/Rooms/Rooms.module.css b/frontend/src/pages/Rooms/Rooms.module.css index 3ba56c9..ce85fbf 100644 --- a/frontend/src/pages/Rooms/Rooms.module.css +++ b/frontend/src/pages/Rooms/Rooms.module.css @@ -10,7 +10,11 @@ display: flex; align-items: center; } - +.right { + display: flex; + align-items: center; + justify-content: center; +} .heading { font-style: 20px; font-weight: bold; @@ -55,6 +59,22 @@ color: #fff; transition: all 0.3s ease-in-out; } + +.codeEditorButton { + margin-left: 20px; + display: flex; + align-items: center; + background: #d40e18; + padding: 5px 20px; + border-radius: 20px; + color: #fff; + transition: all 0.3s ease-in-out; + width: 160px; +} +.codeEditorButton span { + font-size: 15px; + margin-left: 10px; +} .startRoomButton span { font-size: 15px; margin-left: 10px; @@ -70,3 +90,16 @@ gap: 20px; margin-top: 60px; } + +@media screen and (max-width: 1200px) { + .right { + display: block; + } + .codeEditorButton { + margin-left: 0; + margin-top: 20px; + } + .roomList { + grid-template-columns: repeat(2, 1fr); + } +}