├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend ├── README.md ├── config │ ├── database.js │ └── server.js ├── controllers │ ├── RoomController.js │ └── VideoCallController.js ├── index.js ├── middleware │ ├── errorHandler.js │ └── logger.js ├── models │ ├── Room.js │ └── User.js ├── routes │ ├── httpRoutes.js │ └── socketRoutes.js ├── screenshots │ ├── Feature_Page.png │ └── Home_Page.png ├── server.js └── services │ ├── RoomService.js │ └── UserService.js ├── frontend └── vite-project │ ├── .gitignore │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── vite.svg │ ├── src │ ├── App.css │ ├── App.jsx │ ├── VersionHistory.css │ ├── VersionHistory.jsx │ ├── VideoCall.css │ ├── VideoCall.jsx │ ├── assets │ │ ├── gssoc logo.png │ │ └── react.svg │ ├── components │ │ ├── ChatWindow.css │ │ ├── ChatWindow.jsx │ │ ├── FileExplorer.css │ │ ├── FileExplorer.jsx │ │ ├── FileTab.css │ │ ├── FileTab.jsx │ │ ├── ResizableLayout.css │ │ ├── ResizableLayout.jsx │ │ └── ui │ │ │ ├── BackToTop.jsx │ │ │ ├── aspect-ratio.jsx │ │ │ ├── badge.jsx │ │ │ ├── button.jsx │ │ │ ├── card.jsx │ │ │ ├── input.jsx │ │ │ ├── label.jsx │ │ │ ├── progress.jsx │ │ │ ├── separator.jsx │ │ │ ├── skeleton.jsx │ │ │ ├── textarea.jsx │ │ │ ├── toast.jsx │ │ │ ├── toaster.jsx │ │ │ └── tooltip.jsx │ ├── hooks │ │ ├── use-mobile.jsx │ │ └── use-toast.js │ ├── lib │ │ ├── queryClient.js │ │ └── utils.js │ ├── main.css │ ├── main.jsx │ ├── pages │ │ ├── Landing_page.css │ │ ├── Landing_page.jsx │ │ └── not-found.jsx │ ├── styles │ │ └── BackToTop.css │ └── utils │ │ └── fileTypeDetection.js │ ├── tailwind.config.js │ ├── vercel.json │ └── vite.config.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct for GSSoC 2025 2 | 3 | ## 🤝 Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## 🌟 Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * **Demonstrating empathy and kindness** toward other people 21 | * **Being respectful** of differing opinions, viewpoints, and experiences 22 | * **Giving and gracefully accepting** constructive feedback 23 | * **Accepting responsibility** and apologizing to those affected by our mistakes 24 | * **Focusing on what is best** for the overall community 25 | * **Showing appreciation** for other community members' contributions 26 | * **Being inclusive** and welcoming to newcomers 27 | * **Supporting mentorship** and learning opportunities 28 | * **Celebrating diversity** and different perspectives 29 | 30 | Examples of unacceptable behavior include: 31 | 32 | * The use of sexualized language or imagery, and sexual attention or advances 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a professional setting 37 | 38 | ## 🚀 GSSoC 2025 Specific Guidelines 39 | 40 | ### For Participants 41 | * **Respect Mentors**: Show respect and gratitude to your mentors who are volunteering their time 42 | * **Collaborative Learning**: Share knowledge and help other participants when possible 43 | * **Professional Communication**: Maintain professional communication in all interactions 44 | * **Timely Responses**: Respond to communications within reasonable timeframes 45 | * **Constructive Feedback**: Provide and accept feedback in a constructive manner 46 | 47 | ### For Mentors 48 | * **Equal Treatment**: Treat all participants equally regardless of their background 49 | * **Encouraging Environment**: Create a supportive and encouraging learning environment 50 | * **Clear Communication**: Provide clear, actionable feedback and guidance 51 | * **Patience**: Be patient with participants who are learning and growing 52 | * **Inclusive Mentoring**: Ensure your mentoring approach is inclusive and accessible 53 | 54 | ### For Maintainers 55 | * **Fair Evaluation**: Evaluate contributions fairly and objectively 56 | * **Transparent Processes**: Maintain transparent and clear processes 57 | * **Inclusive Policies**: Ensure all policies are inclusive and accessible 58 | * **Conflict Resolution**: Address conflicts promptly and fairly 59 | * **Community Building**: Foster a positive and inclusive community culture 60 | 61 | ## 🔧 Enforcement Responsibilities 62 | 63 | Community leaders are responsible for clarifying and enforcing our standards of 64 | acceptable behavior and will take appropriate and fair corrective action in 65 | response to any behavior that they deem inappropriate, threatening, offensive, 66 | or harmful. 67 | 68 | Community leaders have the right and responsibility to remove, edit, or reject 69 | comments, commits, code, wiki edits, issues, and other contributions that are 70 | not aligned to this Code of Conduct, and will communicate reasons for moderation 71 | decisions when appropriate. 72 | 73 | ## 📋 Scope 74 | 75 | This Code of Conduct applies within all community spaces, and also applies when 76 | an individual is officially representing the community in public spaces. 77 | Examples of representing our community include using an official e-mail address, 78 | posting via an official social media account, or acting as an appointed 79 | representative at an online or offline event. 80 | 81 | ## 🚨 Enforcement 82 | 83 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 84 | reported to the community leaders responsible for enforcement at 85 | [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated 86 | promptly and fairly. 87 | 88 | All community leaders are obligated to respect the privacy and security of the 89 | reporter of any incident. 90 | 91 | ## 📏 Enforcement Guidelines 92 | 93 | Community leaders will follow these Community Impact Guidelines in determining 94 | the consequences for any action they deem in violation of this Code of Conduct: 95 | 96 | ### 1. **Correction** 97 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 98 | 99 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 100 | 101 | ### 2. **Warning** 102 | **Community Impact**: A violation through a single incident or series of actions. 103 | 104 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 105 | 106 | ### 3. **Temporary Ban** 107 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 108 | 109 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 110 | 111 | ### 4. **Permanent Ban** 112 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 113 | 114 | **Consequence**: A permanent ban from any sort of public interaction within the community. 115 | 116 | ## 📞 Reporting Guidelines 117 | 118 | If you experience or witness unacceptable behavior—or have any other concerns—please report it by contacting the project team at [kushikaagarwalg1080@gmail.com]. All reports will be handled with discretion. You may request to remain anonymous. 119 | 120 | In your report please include: 121 | - Your contact information for follow-up contact 122 | - Names (real, nicknames, or pseudonyms) of any individuals involved 123 | - Description of what occurred 124 | - Description of when this occurred 125 | - Any additional context that would be helpful 126 | - Any other relevant information 127 | 128 | ## 🔍 Investigation Process 129 | 130 | 1. **Acknowledgment**: You will receive an acknowledgment within 24 hours 131 | 2. **Investigation**: The incident will be investigated within 72 hours 132 | 3. **Resolution**: A resolution will be provided within one week 133 | 4. **Follow-up**: Follow-up communication to ensure the issue is resolved 134 | 135 | ## 🌍 GSSoC 2025 Values 136 | 137 | This Code of Conduct is aligned with GSSoC 2025's core values: 138 | 139 | * **Inclusivity**: Welcoming participants from all backgrounds and skill levels 140 | * **Learning**: Fostering a culture of continuous learning and growth 141 | * **Collaboration**: Encouraging teamwork and knowledge sharing 142 | * **Innovation**: Supporting creative problem-solving and new ideas 143 | * **Excellence**: Striving for quality in all contributions 144 | * **Mentorship**: Building strong mentor-participant relationships 145 | * **Diversity**: Celebrating and embracing different perspectives and experiences 146 | 147 | ## 📚 Additional Resources 148 | 149 | * [GSSoC 2025 Official Website](https://gssoc.girlscript.tech/) 150 | * [GSSoC Community Guidelines](https://gssoc.girlscript.tech/community-guidelines) 151 | * [Open Source Code of Conduct](https://www.contributor-covenant.org/) 152 | * [GitHub Community Guidelines](https://docs.github.com/en/github/site-policy/github-community-guidelines) 153 | 154 | ## 📝 Acknowledgment 155 | 156 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), 157 | version 2.0, available at 158 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 159 | 160 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 161 | 162 | 163 | 164 | 165 | **GSSoC 2025 Edition** 166 | 167 | --- 168 | 169 | *This Code of Conduct is a living document and will be updated as needed to ensure it continues to serve our community effectively. We welcome feedback and suggestions for improvement.* 170 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Collaborative Code Editor - GSSoC 2025 2 | 3 | Welcome to the Collaborative Code Editor project! We're excited to have you as part of GSSoC 2025. This document will guide you through the contribution process and help you get started. 4 | 5 | ## 📑 Table of Contents 6 | - [🚀 Getting Started](#-getting-started) 7 | - [Prerequisites](#prerequisites) 8 | - [Quick Start](#quick-start) 9 | - [📋 GSSoC 2025 Guidelines](#-gssoc-2025-guidelines) 10 | - [Project Structure](#project-structure) 11 | - [Available Issues](#available-issues) 12 | - [Contribution Areas](#contribution-areas) 13 | - [🛠️ Development Setup](#-development-setup) 14 | - [Backend Setup](#backend-setup) 15 | - [Frontend Setup](#frontend-setup) 16 | - [Environment Variables](#environment-variables) 17 | - [📝 Code Standards](#-code-standards) 18 | - [JavaScript/React](#javascriptreact) 19 | - [Git Commit Messages](#git-commit-messages) 20 | - [Pull Request Guidelines](#pull-request-guidelines) 21 | - [🧪 Testing](#-testing) 22 | - [Frontend Testing](#frontend-testing) 23 | - [Backend Testing](#backend-testing) 24 | - [Manual Testing](#manual-testing) 25 | - [📚 Learning Resources](#-learning-resources) 26 | - [🤝 Communication](#-communication) 27 | - [Getting Help](#getting-help) 28 | - [Code Reviews](#code-reviews) 29 | - [🏆 GSSoC 2025 Evaluation](#-gssoc-2025-evaluation) 30 | - [🎯 Milestone Goals](#-milestone-goals) 31 | - [Phase 1: Onboarding](#phase-1-onboarding) 32 | - [Phase 2: Active Development](#phase-2-active-development) 33 | - [Phase 3: Project Completion](#phase-3-project-completion) 34 | - [🚨 Important Notes](#-important-notes) 35 | - [Code of Conduct](#code-of-conduct) 36 | - [Intellectual Property](#intellectual-property) 37 | - [Security](#security) 38 | - [📞 Contact Information](#-contact-information) 39 | - [🎉 Welcome to GSSoC 2025!](#-welcome-to-gssoc-2025) 40 | 41 | ## 🚀 Getting Started 42 | 43 | ### Prerequisites 44 | - Basic knowledge of JavaScript/React 45 | - Familiarity with Git and GitHub 46 | - Enthusiasm to learn and contribute! 47 | 48 | ### Quick Start 49 | 1. **Fork** this repository 50 | 2. **Clone** your fork locally 51 | 3. **Create** a new branch for your work 52 | 4. **Make** your changes 53 | 5. **Test** your changes 54 | 6. **Commit** and **push** to your fork 55 | 7. **Create** a Pull Request 56 | 57 | ## 📋 GSSoC 2025 Guidelines 58 | 59 | ### Project Structure 60 | ``` 61 | codeeditor/ 62 | ├── backend/ # Node.js + Socket.IO backend 63 | ├── frontend/ # React + Vite frontend 64 | ├── .github/ # GitHub workflows and templates 65 | └── docs/ # Documentation 66 | ``` 67 | 68 | ### Available Issues 69 | - Look for issues labeled with: 70 | - `gssoc2025` - GSSoC 2025 specific issues 71 | - `good first issue` - Beginner friendly 72 | - `help wanted` - Need assistance 73 | - `bug` - Bug fixes 74 | - `enhancement` - New features 75 | 76 | ### Contribution Areas 77 | 78 | #### Frontend (React + Vite) 79 | - UI/UX improvements 80 | - New features 81 | - Bug fixes 82 | - Performance optimizations 83 | - Accessibility improvements 84 | 85 | #### Backend (Node.js + Socket.IO) 86 | - API enhancements 87 | - Real-time features 88 | - Security improvements 89 | - Performance optimizations 90 | - Testing 91 | 92 | #### Documentation 93 | - README updates 94 | - API documentation 95 | - User guides 96 | - Code comments 97 | 98 | ## 🛠️ Development Setup 99 | 100 | ### Backend Setup 101 | ```bash 102 | cd backend 103 | npm install 104 | npm run dev 105 | ``` 106 | 107 | ### Frontend Setup 108 | ```bash 109 | cd frontend/vite-project 110 | npm install 111 | npm run dev 112 | ``` 113 | 114 | ### Environment Variables 115 | Create `.env` files as needed (see `.env.example` files) 116 | 117 | ## 📝 Code Standards 118 | 119 | ### JavaScript/React 120 | - Use ES6+ features 121 | - Follow React best practices 122 | - Use meaningful variable names 123 | - Add comments for complex logic 124 | - Follow the existing code style 125 | 126 | ### Git Commit Messages 127 | Use conventional commit format: 128 | ``` 129 | type(scope): description 130 | 131 | feat(frontend): add dark mode toggle 132 | fix(backend): resolve socket connection issue 133 | docs(readme): update installation instructions 134 | ``` 135 | 136 | ### Pull Request Guidelines 137 | - **Title**: Clear and descriptive 138 | - **Description**: Explain what and why, not how 139 | - **Screenshots**: Include for UI changes 140 | - **Testing**: Describe how you tested 141 | - **Related Issues**: Link to relevant issues 142 | 143 | ## 🧪 Testing 144 | 145 | ### Frontend Testing 146 | ```bash 147 | cd frontend/vite-project 148 | npm run test 149 | ``` 150 | 151 | ### Backend Testing 152 | ```bash 153 | cd backend 154 | npm test 155 | ``` 156 | 157 | ### Manual Testing 158 | - Test on different browsers 159 | - Test responsive design 160 | - Test real-time features 161 | - Test edge cases 162 | 163 | ## 📚 Learning Resources 164 | 165 | ### React & Vite 166 | - [React Documentation](https://react.dev/) 167 | - [Vite Documentation](https://vitejs.dev/) 168 | - [Socket.IO Documentation](https://socket.io/docs/) 169 | 170 | ### GSSoC Resources 171 | - [GSSoC 2025 Official Site](https://gssoc.girlscript.tech/) 172 | - [GSSoC Community](https://discord.gg/gssoc) 173 | - [GSSoC Guidelines](https://gssoc.girlscript.tech/guidelines) 174 | 175 | ## 🤝 Communication 176 | 177 | ### Getting Help 178 | - **GitHub Issues**: For bugs and feature requests 179 | - **GitHub Discussions**: For questions and discussions 180 | - **Discord**: Join the GSSoC community 181 | 182 | ### Code Reviews 183 | - Be respectful and constructive 184 | - Focus on the code, not the person 185 | - Ask questions if something is unclear 186 | - Suggest improvements politely 187 | 188 | ## 🏆 GSSoC 2025 Evaluation 189 | 190 | Your contributions will be evaluated based on: 191 | - **Code Quality**: Clean, readable, and maintainable code 192 | - **Documentation**: Clear documentation and comments 193 | - **Testing**: Proper testing and edge case handling 194 | - **Communication**: Professional and helpful communication 195 | - **Innovation**: Creative solutions and improvements 196 | 197 | ## 🎯 Milestone Goals 198 | 199 | ### Phase 1: Onboarding 200 | - [ ] Set up development environment 201 | - [ ] Understand project structure 202 | - [ ] Make first small contribution 203 | - [ ] Familiarize with codebase 204 | 205 | ### Phase 2: Active Development 206 | - [ ] Work on assigned issues 207 | - [ ] Collaborate with mentors 208 | - [ ] Learn advanced concepts 209 | - [ ] Help other participants 210 | 211 | ### Phase 3: Project Completion 212 | - [ ] Complete major features 213 | - [ ] Write comprehensive tests 214 | - [ ] Update documentation 215 | - [ ] Prepare final submission 216 | 217 | ## 🚨 Important Notes 218 | 219 | ### Code of Conduct 220 | - Follow our [Code of Conduct](CODE_OF_CONDUCT.md) 221 | - Be respectful and inclusive 222 | - Report any violations immediately 223 | 224 | ### Intellectual Property 225 | - All contributions become part of the project 226 | - Ensure you have rights to contribute 227 | - Don't include proprietary code 228 | 229 | ### Security 230 | - Don't commit sensitive information 231 | - Report security vulnerabilities privately 232 | - Follow security best practices 233 | 234 | ## 📞 Contact Information 235 | 236 | **Project Maintainers:** 237 | - [Maintainer Name] - [kushikaagarwalg1080@gmail.com] 238 | 239 | 240 | ## 🎉 Welcome to GSSoC 2025! 241 | 242 | We're excited to see your contributions and help you grow as a developer. Remember: 243 | - **Ask questions** - There are no stupid questions! 244 | - **Be patient** - Learning takes time 245 | - **Have fun** - Open source should be enjoyable 246 | - **Help others** - We're all learning together 247 | 248 | Good luck with your contributions! 🚀✨ 249 | 250 | **GSSoC 2025 Edition** 251 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Kushika Agarwal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Backend - Collaborative Code Editor 2 | 3 | This backend is structured using the **MVC (Model-View-Controller)** architecture pattern for better organization, maintainability, and scalability. 4 | 5 | --- 6 | 7 | ## ⚙️ Requirements 8 | - **Node.js v18+** 9 | - **npm v8+** 10 | - A modern browser (Chrome / Firefox / Edge) 11 | 12 | --- 13 | 14 | ## 📦 Installation 15 | 16 | ### 1️⃣ Clone the Repository 17 | ```bash 18 | git clone https://github.com/Kushika-Agarwal/Collaborative-code-editor.git 19 | cd Collaborative-code-editor/backend 20 | 21 | 2️⃣ Install Dependencies 22 | npm install 23 | 24 | 3️⃣ Start the Server 25 | # Development mode with auto-restart 26 | npm run dev 27 | 28 | # Production mode 29 | npm start 30 | 31 | 32 | Server will start on 👉 http://localhost:3000 33 | 34 | 🖼️ Demo 35 | ## 📸 Screenshots 36 | 37 | ### 🏠 Home Page 38 | ![Home Page](./screenshots/Home_Page.png) 39 | 40 | ### 🚀 Features Page 41 | ![Features Page](./screenshots/Feature_Page.png) 42 | 43 | 44 | 45 | 🏗️ Architecture Overview 46 | backend/ 47 | ├── config/ # Configuration files 48 | ├── controllers/ # Business logic handlers 49 | ├── middleware/ # Express middleware 50 | ├── models/ # Data models 51 | ├── routes/ # Route definitions 52 | ├── services/ # Business logic services 53 | └── server.js # Main server file 54 | 55 | 📁 Directory Structure 56 | config/ 57 | 58 | database.js - Database configuration and in-memory storage management 59 | 60 | server.js - Server configuration (CORS, ports, etc.) 61 | 62 | controllers/ 63 | 64 | RoomController.js - Handles room-related socket events and business logic 65 | 66 | VideoCallController.js - Handles video call-related socket events 67 | 68 | middleware/ 69 | 70 | errorHandler.js - Global error handling middleware 71 | 72 | logger.js - Request and socket logging middleware 73 | 74 | models/ 75 | 76 | Room.js - Room data model with methods for room operations 77 | 78 | User.js - User data model with methods for user operations 79 | 80 | routes/ 81 | 82 | httpRoutes.js - HTTP REST API endpoints 83 | 84 | socketRoutes.js - Socket.IO event handlers 85 | 86 | services/ 87 | 88 | RoomService.js - Business logic for room operations 89 | 90 | UserService.js - Business logic for user operations 91 | 92 | 🔄 Data Flow 93 | 94 | Socket Events → routes/socketRoutes.js → controllers/ → services/ → models/ 95 | 96 | HTTP Requests → routes/httpRoutes.js → controllers/ → services/ → models/ 97 | 98 | 🚀 Features 99 | Real-time Collaboration 100 | 101 | Multiple users can join the same room 102 | 103 | Live code synchronization 104 | 105 | Typing indicators 106 | 107 | Language selection 108 | 109 | Chat System 110 | 111 | Real-time messaging within rooms 112 | 113 | Message history (last 50 messages) 114 | 115 | User identification 116 | 117 | Video Calling 118 | 119 | WebRTC-based video calls 120 | 121 | Camera and microphone controls 122 | 123 | Call room management 124 | 125 | Signaling for peer connections 126 | 127 | API Endpoints 128 | 129 | Health check: GET /health 130 | 131 | Room statistics: GET /api/rooms/stats 132 | 133 | Call statistics: GET /api/calls/stats 134 | 135 | Room info: GET /api/rooms/:roomId 136 | 137 | API docs: GET /api/docs 138 | 139 | 140 | 🛠️ Development 141 | Starting the Server 142 | # Development mode with auto-restart 143 | npm run dev 144 | 145 | # Production mode 146 | npm start 147 | 148 | API Testing 149 | # Health check 150 | curl http://localhost:3000/health 151 | 152 | # Room statistics 153 | curl http://localhost:3000/api/rooms/stats 154 | 155 | # Call statistics 156 | curl http://localhost:3000/api/calls/stats 157 | 158 | 159 | 📊 Monitoring 160 | 161 | The application includes built-in logging and monitoring: 162 | 163 | Request/response logging 164 | 165 | Socket connection tracking 166 | 167 | Error handling and reporting 168 | 169 | Room and user statistics 170 | 171 | 172 | 🔧 Configuration 173 | 174 | Server configuration can be modified in config/server.js: 175 | 176 | Port settings 177 | 178 | CORS configuration 179 | 180 | Static file serving 181 | 182 | Socket.IO settings 183 | 184 | 185 | 🚨 Error Handling 186 | 187 | Global error handler middleware 188 | 189 | Socket error logging 190 | 191 | Graceful shutdown handling 192 | 193 | 404 route handling 194 | 195 | 196 | 📈 Scalability 197 | 198 | The current structure supports: 199 | 200 | Easy addition of new features 201 | 202 | Database integration (replace in-memory storage) 203 | 204 | Microservices architecture 205 | 206 | Load balancing 207 | 208 | Horizontal scaling 209 | 210 | 211 | 🔒 Security Considerations 212 | 213 | CORS configuration for production 214 | 215 | Input validation (to be implemented) 216 | 217 | Rate limiting (to be implemented) 218 | 219 | Authentication (to be implemented) 220 | 221 | 222 | 🤝 Contributing 223 | 224 | Fork the repo 225 | 226 | Create a new branch (git checkout -b readme-improvements) 227 | 228 | Commit your changes (git commit -m "docs: improve backend README") 229 | 230 | Push to the branch (git push origin readme-improvements) 231 | 232 | Open a Pull Request 🎉 233 | 234 | 📜 License 235 | 236 | MIT License © 2025 Kushika Agarwal & Contributors 237 | -------------------------------------------------------------------------------- /backend/config/database.js: -------------------------------------------------------------------------------- 1 | // Database configuration for managing room data 2 | // In a production environment, this would connect to a real database 3 | // For now, we're using in-memory storage with Map 4 | 5 | class RoomDatabase { 6 | constructor() { 7 | // In-memory storage for rooms and their participants 8 | this.rooms = new Map(); 9 | } 10 | 11 | // Create a new room 12 | createRoom(roomId) { 13 | if (!this.rooms.has(roomId)) { 14 | this.rooms.set(roomId, new Set()); 15 | return true; 16 | } 17 | return false; 18 | } 19 | 20 | // Add user to room 21 | addUserToRoom(roomId, userName) { 22 | if (!this.rooms.has(roomId)) { 23 | this.createRoom(roomId); 24 | } 25 | this.rooms.get(roomId).add(userName); 26 | return true; 27 | } 28 | 29 | // Remove user from room 30 | removeUserFromRoom(roomId, userName) { 31 | if (this.rooms.has(roomId)) { 32 | this.rooms.get(roomId).delete(userName); 33 | return true; 34 | } 35 | return false; 36 | } 37 | 38 | // Get all users in a room 39 | getRoomUsers(roomId) { 40 | if (this.rooms.has(roomId)) { 41 | return Array.from(this.rooms.get(roomId)); 42 | } 43 | return []; 44 | } 45 | 46 | // Check if room exists 47 | roomExists(roomId) { 48 | return this.rooms.has(roomId); 49 | } 50 | 51 | // Get all rooms (for debugging/admin purposes) 52 | getAllRooms() { 53 | return Array.from(this.rooms.keys()); 54 | } 55 | } 56 | 57 | export default new RoomDatabase(); -------------------------------------------------------------------------------- /backend/config/server.js: -------------------------------------------------------------------------------- 1 | // Server configuration settings 2 | export const serverConfig = { 3 | port: process.env.PORT || 3000, 4 | cors: { 5 | origin: "*", // In production, specify exact origins 6 | methods: ["GET", "POST"], 7 | credentials: true 8 | }, 9 | socket: { 10 | cors: { 11 | origin: "*", 12 | methods: ["GET", "POST"] 13 | } 14 | } 15 | }; 16 | 17 | // Static file serving configuration 18 | export const staticConfig = { 19 | path: "../frontend/vite-project/dist", 20 | options: { 21 | index: "index.html" 22 | } 23 | }; -------------------------------------------------------------------------------- /backend/controllers/RoomController.js: -------------------------------------------------------------------------------- 1 | // Room controller - handles room-related socket events and business logic 2 | import roomService from '../services/RoomService.js'; 3 | import userService from '../services/UserService.js'; 4 | 5 | class RoomController { 6 | // File management methods 7 | 8 | // Handle creating a new file 9 | handleCreateFile(socket, { roomId, filename, createdBy, code = '', language = 'javascript' }) { 10 | try { 11 | const result = roomService.createFileInRoom(roomId, filename, createdBy, code, language); 12 | if (result.success) { 13 | // Broadcast new file to all users in the room 14 | this.io.in(roomId).emit("fileCreated", { 15 | file: result.file, 16 | createdBy 17 | }); 18 | return { success: true, file: result.file }; 19 | } 20 | return result; 21 | } catch (error) { 22 | console.error('Error in handleCreateFile:', error); 23 | return { success: false, error: error.message }; 24 | } 25 | } 26 | 27 | // Handle deleting a file 28 | handleDeleteFile(socket, { roomId, filename, deletedBy }) { 29 | try { 30 | const result = roomService.deleteFileFromRoom(roomId, filename); 31 | if (result.success) { 32 | // Broadcast file deletion to all users in the room 33 | this.io.in(roomId).emit("fileDeleted", { 34 | filename, 35 | deletedBy, 36 | newActiveFile: roomService.getRoomInfo(roomId).activeFile 37 | }); 38 | return { success: true }; 39 | } 40 | return result; 41 | } catch (error) { 42 | console.error('Error in handleDeleteFile:', error); 43 | return { success: false, error: error.message }; 44 | } 45 | } 46 | 47 | // Handle renaming a file 48 | handleRenameFile(socket, { roomId, oldFilename, newFilename, renamedBy }) { 49 | try { 50 | const result = roomService.renameFileInRoom(roomId, oldFilename, newFilename); 51 | if (result.success) { 52 | // Broadcast file rename to all users in the room 53 | this.io.in(roomId).emit("fileRenamed", { 54 | oldFilename, 55 | newFilename, 56 | renamedBy 57 | }); 58 | return { success: true }; 59 | } 60 | return result; 61 | } catch (error) { 62 | console.error('Error in handleRenameFile:', error); 63 | return { success: false, error: error.message }; 64 | } 65 | } 66 | 67 | // Handle switching active file 68 | handleSwitchFile(socket, { roomId, filename, switchedBy }) { 69 | try { 70 | const result = roomService.setActiveFileInRoom(roomId, filename); 71 | if (result.success) { 72 | const file = roomService.getRoomFile(roomId, filename); 73 | // Broadcast active file change to all users in the room 74 | this.io.in(roomId).emit("activeFileChanged", { 75 | filename, 76 | file, 77 | switchedBy 78 | }); 79 | return { success: true, file }; 80 | } 81 | return result; 82 | } catch (error) { 83 | console.error('Error in handleSwitchFile:', error); 84 | return { success: false, error: error.message }; 85 | } 86 | } 87 | 88 | // Handle getting all files in room 89 | handleGetFiles(socket, { roomId }) { 90 | try { 91 | const files = roomService.getRoomFiles(roomId); 92 | const activeFile = roomService.getRoomInfo(roomId)?.activeFile; 93 | socket.emit("filesUpdated", { files, activeFile }); 94 | return { success: true, files }; 95 | } catch (error) { 96 | console.error('Error in handleGetFiles:', error); 97 | return { success: false, error: error.message }; 98 | } 99 | } 100 | 101 | // Handle updating file code 102 | handleFileCodeChange(socket, { roomId, filename, code }) { 103 | try { 104 | const result = roomService.updateFileCodeInRoom(roomId, filename, code); 105 | if (result.success) { 106 | // Broadcast file code change to all users in the room except sender 107 | socket.to(roomId).emit("fileCodeUpdated", { filename, code }); 108 | return { success: true }; 109 | } 110 | return result; 111 | } catch (error) { 112 | console.error('Error in handleFileCodeChange:', error); 113 | return { success: false, error: error.message }; 114 | } 115 | } 116 | 117 | // Handle updating file language 118 | handleFileLanguageChange(socket, { roomId, filename, language }) { 119 | try { 120 | const result = roomService.updateFileLanguageInRoom(roomId, filename, language); 121 | if (result.success) { 122 | // Broadcast file language change to all users in the room except sender 123 | socket.to(roomId).emit("fileLanguageUpdated", { filename, language }); 124 | return { success: true }; 125 | } 126 | return result; 127 | } catch (error) { 128 | console.error('Error in handleFileLanguageChange:', error); 129 | return { success: false, error: error.message }; 130 | } 131 | } 132 | 133 | // Handle filename changes in room (legacy method, updated to work with new file system) 134 | handleFilenameChange(socket, { roomId, oldFilename, newFilename, userName }) { 135 | try { 136 | // Use the new rename file method 137 | const result = roomService.renameFileInRoom(roomId, oldFilename, newFilename); 138 | if (result.success) { 139 | // Broadcast filename change to all users in the room 140 | this.io.in(roomId).emit("filenameChanged", { oldFilename, newFilename, userName }); 141 | // Broadcast system chat message 142 | const systemMessage = `Filename changed from "${oldFilename}" to "${newFilename}" by ${userName}`; 143 | this.io.in(roomId).emit("chatMessage", { userName: "System", message: systemMessage }); 144 | console.log(systemMessage); 145 | return { success: true }; 146 | } 147 | return result; 148 | } catch (error) { 149 | console.error('Error in handleFilenameChange:', error); 150 | return { success: false, error: error.message }; 151 | } 152 | } 153 | constructor(io) { 154 | this.io = io; // Store io instance for broadcasting 155 | } 156 | 157 | // Handle remote cursor movement 158 | handleCursorMove(socket, { roomId, filename, position }) { 159 | try { 160 | const user = userService.getUser(socket.id); 161 | if (user) { 162 | // Broadcast to all users in the room except sender 163 | socket.to(roomId).emit("cursorPosition", { 164 | roomId, 165 | filename, 166 | position, 167 | userId: socket.id, 168 | userName: user.userName, 169 | }); 170 | } 171 | return { success: true }; 172 | } catch (error) { 173 | console.error('Error in handleCursorMove:', error); 174 | return { success: false, error: error.message }; 175 | } 176 | } 177 | 178 | // Handle clearing of a user's cursor (e.g., on leave/disconnect) 179 | handleCursorClear(socket, { roomId, userId }) { 180 | try { 181 | // Broadcast to all users in the room except sender 182 | socket.to(roomId).emit("cursorCleared", { userId }); 183 | return { success: true }; 184 | } catch (error) { 185 | console.error('Error in handleCursorClear:', error); 186 | return { success: false, error: error.message }; 187 | } 188 | } 189 | // Handle user joining a room 190 | handleJoinRoom(socket, { roomId, userName }) { 191 | try { 192 | // Create user if doesn't exist 193 | let user = userService.getUser(socket.id); 194 | if (!user) { 195 | user = userService.createUser(socket.id, userName); 196 | } else { 197 | // Update user name if changed 198 | userService.updateUserName(socket.id, userName); 199 | } 200 | 201 | // Leave previous room if any 202 | const previousRoom = user.getCurrentRoom(); 203 | if (previousRoom) { 204 | socket.leave(previousRoom); 205 | const { users } = roomService.removeUserFromRoom(previousRoom, user.userName); 206 | this.io.in(previousRoom).emit("userJoined", users); 207 | } 208 | 209 | // Join new room 210 | socket.join(roomId); 211 | userService.updateUserRoom(socket.id, roomId); 212 | const { users } = roomService.addUserToRoom(roomId, userName); 213 | 214 | // Notify all users in the room 215 | this.io.in(roomId).emit("userJoined", users); 216 | 217 | // Send current files and active file to the newly joined user 218 | const roomInfo = roomService.getRoomInfo(roomId); 219 | if (roomInfo) { 220 | socket.emit("filesUpdated", { 221 | files: roomInfo.files, 222 | activeFile: roomInfo.activeFile 223 | }); 224 | 225 | // Send current active file content 226 | const activeFileData = roomService.getRoomFile(roomId, roomInfo.activeFile); 227 | if (activeFileData) { 228 | socket.emit("codeUpdated", activeFileData.code); 229 | socket.emit("languageUpdated", activeFileData.language); 230 | } 231 | } 232 | 233 | console.log(`User ${userName} joined room ${roomId}`); 234 | return { success: true, users }; 235 | } catch (error) { 236 | console.error('Error in handleJoinRoom:', error); 237 | return { success: false, error: error.message }; 238 | } 239 | } 240 | 241 | // Handle code changes in room 242 | handleCodeChange(socket, { roomId, code }) { 243 | try { 244 | const updatedCode = roomService.updateRoomCode(roomId, code); 245 | if (updatedCode !== null) { 246 | // Broadcast to all users in the room, including sender 247 | this.io.in(roomId).emit("codeUpdated", code); 248 | return { success: true }; 249 | } 250 | return { success: false, error: 'Room not found' }; 251 | } catch (error) { 252 | console.error('Error in handleCodeChange:', error); 253 | return { success: false, error: error.message }; 254 | } 255 | } 256 | 257 | // Handle language changes in room 258 | handleLanguageChange(socket, { roomId, language }) { 259 | try { 260 | const updatedLanguage = roomService.updateRoomLanguage(roomId, language); 261 | if (updatedLanguage !== null) { 262 | socket.to(roomId).emit("languageUpdated", language); 263 | return { success: true }; 264 | } 265 | return { success: false, error: 'Room not found' }; 266 | } catch (error) { 267 | console.error('Error in handleLanguageChange:', error); 268 | return { success: false, error: error.message }; 269 | } 270 | } 271 | 272 | // Handle user leaving room 273 | handleLeaveRoom(socket) { 274 | try { 275 | const user = userService.getUser(socket.id); 276 | if (user && user.getCurrentRoom()) { 277 | const roomId = user.getCurrentRoom(); 278 | const userName = user.userName; 279 | 280 | socket.leave(roomId); 281 | userService.removeUserFromRoom(socket.id); 282 | const { users } = roomService.removeUserFromRoom(roomId, userName); 283 | 284 | socket.to(roomId).emit("userJoined", users); 285 | console.log(`User ${userName} left room ${roomId}`); 286 | return { success: true }; 287 | } 288 | return { success: false, error: 'User not in room' }; 289 | } catch (error) { 290 | console.error('Error in handleLeaveRoom:', error); 291 | return { success: false, error: error.message }; 292 | } 293 | } 294 | 295 | // Handle typing indicator 296 | handleTyping(socket, { roomId, userName }) { 297 | try { 298 | userService.updateUserTyping(socket.id, true); 299 | socket.to(roomId).emit("userTyping", userName); 300 | return { success: true }; 301 | } catch (error) { 302 | console.error('Error in handleTyping:', error); 303 | return { success: false, error: error.message }; 304 | } 305 | } 306 | 307 | // Handle chat messages 308 | handleChatMessage(socket, { roomId, userName, message }) { 309 | try { 310 | const messageObj = roomService.addMessageToRoom(roomId, userName, message); 311 | if (messageObj) { 312 | // Send to all users in the room including sender 313 | this.io.to(roomId).emit("chatMessage", { userName, message }); 314 | return { success: true, message: messageObj }; 315 | } 316 | return { success: false, error: 'Room not found' }; 317 | } catch (error) { 318 | console.error('Error in handleChatMessage:', error); 319 | return { success: false, error: error.message }; 320 | } 321 | } 322 | 323 | // Handle user disconnect 324 | handleDisconnect(socket) { 325 | try { 326 | const user = userService.getUser(socket.id); 327 | if (user) { 328 | const roomId = user.getCurrentRoom(); 329 | const userName = user.userName; 330 | 331 | if (roomId) { 332 | const { users } = roomService.removeUserFromRoom(roomId, userName); 333 | socket.to(roomId).emit("userJoined", users); 334 | } 335 | 336 | userService.removeUser(socket.id); 337 | console.log(`User ${userName} disconnected`); 338 | } 339 | return { success: true }; 340 | } catch (error) { 341 | console.error('Error in handleDisconnect:', error); 342 | return { success: false, error: error.message }; 343 | } 344 | } 345 | 346 | // Get room info 347 | getRoomInfo(roomId) { 348 | try { 349 | return roomService.getRoomInfo(roomId); 350 | } catch (error) { 351 | console.error('Error in getRoomInfo:', error); 352 | return null; 353 | } 354 | } 355 | 356 | // Get room statistics 357 | getRoomStats() { 358 | try { 359 | return roomService.getRoomStats(); 360 | } catch (error) { 361 | console.error('Error in getRoomStats:', error); 362 | return null; 363 | } 364 | } 365 | } 366 | 367 | export default RoomController; 368 | -------------------------------------------------------------------------------- /backend/controllers/VideoCallController.js: -------------------------------------------------------------------------------- 1 | // Video call controller - handles video call-related socket events 2 | import userService from '../services/UserService.js'; 3 | 4 | class VideoCallController { 5 | // Handle user joining video call 6 | handleJoinCall(socket, { roomId, userName }) { 7 | try { 8 | const user = userService.getUser(socket.id); 9 | if (user) { 10 | // Join the call room (separate from main room) 11 | const callRoomId = roomId + "-call"; 12 | socket.join(callRoomId); 13 | 14 | // Update user call status 15 | userService.updateUserCallStatus(socket.id, true); 16 | 17 | // Notify other users in the call 18 | socket.to(callRoomId).emit("user-joined-call", { 19 | userName, 20 | socketId: socket.id 21 | }); 22 | 23 | console.log(`User ${userName} joined video call in room ${roomId}`); 24 | return { success: true }; 25 | } 26 | return { success: false, error: 'User not found' }; 27 | } catch (error) { 28 | console.error('Error in handleJoinCall:', error); 29 | return { success: false, error: error.message }; 30 | } 31 | } 32 | 33 | // Handle WebRTC signaling 34 | handleSignal(socket, { roomId, signal, to }) { 35 | try { 36 | // Forward the signal to the target user 37 | socket.to(to).emit("signal", { 38 | signal, 39 | from: socket.id 40 | }); 41 | return { success: true }; 42 | } catch (error) { 43 | console.error('Error in handleSignal:', error); 44 | return { success: false, error: error.message }; 45 | } 46 | } 47 | 48 | // Handle user leaving video call 49 | handleLeaveCall(socket, { roomId }) { 50 | try { 51 | const user = userService.getUser(socket.id); 52 | if (user) { 53 | const callRoomId = roomId + "-call"; 54 | socket.leave(callRoomId); 55 | 56 | // Update user call status 57 | userService.updateUserCallStatus(socket.id, false); 58 | 59 | // Notify other users in the call 60 | socket.to(callRoomId).emit("user-left-call", { 61 | socketId: socket.id 62 | }); 63 | 64 | console.log(`User ${user.userName} left video call in room ${roomId}`); 65 | return { success: true }; 66 | } 67 | return { success: false, error: 'User not found' }; 68 | } catch (error) { 69 | console.error('Error in handleLeaveCall:', error); 70 | return { success: false, error: error.message }; 71 | } 72 | } 73 | 74 | // Handle camera toggle 75 | handleToggleCamera(socket) { 76 | try { 77 | const user = userService.getUser(socket.id); 78 | if (user) { 79 | const cameraOn = userService.toggleUserCamera(socket.id); 80 | const roomId = user.getCurrentRoom(); 81 | 82 | if (roomId) { 83 | const callRoomId = roomId + "-call"; 84 | socket.to(callRoomId).emit("camera-toggled", { 85 | socketId: socket.id, 86 | cameraOn 87 | }); 88 | } 89 | 90 | return { success: true, cameraOn }; 91 | } 92 | return { success: false, error: 'User not found' }; 93 | } catch (error) { 94 | console.error('Error in handleToggleCamera:', error); 95 | return { success: false, error: error.message }; 96 | } 97 | } 98 | 99 | // Handle microphone toggle 100 | handleToggleMicrophone(socket) { 101 | try { 102 | const user = userService.getUser(socket.id); 103 | if (user) { 104 | const micOn = userService.toggleUserMicrophone(socket.id); 105 | const roomId = user.getCurrentRoom(); 106 | 107 | if (roomId) { 108 | const callRoomId = roomId + "-call"; 109 | socket.to(callRoomId).emit("microphone-toggled", { 110 | socketId: socket.id, 111 | micOn 112 | }); 113 | } 114 | 115 | return { success: true, micOn }; 116 | } 117 | return { success: false, error: 'User not found' }; 118 | } catch (error) { 119 | console.error('Error in handleToggleMicrophone:', error); 120 | return { success: false, error: error.message }; 121 | } 122 | } 123 | 124 | // Get call participants 125 | getCallParticipants(roomId) { 126 | try { 127 | const callRoomId = roomId + "-call"; 128 | const participants = userService.getUsersInRoom(roomId).filter(user => user.isInCall); 129 | return participants; 130 | } catch (error) { 131 | console.error('Error in getCallParticipants:', error); 132 | return []; 133 | } 134 | } 135 | 136 | // Handle call statistics 137 | getCallStats() { 138 | try { 139 | const stats = userService.getUserStats(); 140 | return { 141 | totalUsers: stats.totalUsers, 142 | usersInCalls: stats.usersInCalls, 143 | callRooms: stats.usersInCalls > 0 ? Math.ceil(stats.usersInCalls / 2) : 0 144 | }; 145 | } catch (error) { 146 | console.error('Error in getCallStats:', error); 147 | return null; 148 | } 149 | } 150 | } 151 | 152 | export default VideoCallController; -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import http from "http"; 3 | import { Server } from "socket.io"; 4 | import path, { dirname, join } from "path"; 5 | import { fileURLToPath } from "url"; 6 | 7 | const app = express(); 8 | const server = http.createServer(app); 9 | 10 | const io = new Server(server, { 11 | cors: { 12 | origin: "*", 13 | }, 14 | }); 15 | 16 | const rooms = new Map(); 17 | io.on("connection", (socket) => { 18 | console.log("A user connected", socket.id); 19 | 20 | let currentRoom = null; 21 | let currentUser = null; 22 | 23 | socket.on("join_room", ({ roomId, userName }) => { 24 | if (currentRoom) { 25 | socket.leave(currentRoom); 26 | rooms.get(currentRoom).delete(socket.id); 27 | io.to(currentRoom).emit("userJoined", Array.from(rooms.get(currentRoom))); 28 | } 29 | 30 | currentRoom = roomId; 31 | currentUser = userName; 32 | 33 | socket.join(roomId); 34 | 35 | if (!rooms.has(roomId)) { 36 | rooms.set(roomId, new Set()); 37 | } 38 | 39 | rooms.get(roomId).add(userName); 40 | io.to(roomId).emit("userJoined", Array.from(rooms.get(currentRoom))); 41 | }); 42 | 43 | socket.on("codeChange", ({ roomId, code }) => { 44 | socket.to(roomId).emit("codeUpdated", code); 45 | }); 46 | 47 | socket.on("leaveRoom", () => { 48 | if (currentRoom && currentUser) { 49 | rooms.get(currentRoom).delete(currentUser); 50 | io.to(currentRoom).emit("userJoined", Array.from(rooms.get(currentRoom))); 51 | socket.leave(currentRoom); 52 | currentRoom = null; 53 | currentUser = null; 54 | } 55 | }); 56 | 57 | socket.on("typing", ({ roomId, userName }) => { 58 | socket.to(roomId).emit("userTyping", userName); 59 | }); 60 | 61 | socket.on("languageChange", ({ roomId, language }) => { 62 | socket.to(roomId).emit("languageUpdated", language); 63 | }); 64 | 65 | // Chat message event 66 | socket.on("chatMessage", ({ roomId, userName, message }) => { 67 | io.to(roomId).emit("chatMessage", { userName, message }); 68 | }); 69 | 70 | // Video call signaling events 71 | socket.on("join-call", ({ roomId, userName }) => { 72 | socket.join(roomId + "-call"); 73 | socket.to(roomId + "-call").emit("user-joined-call", { userName, socketId: socket.id }); 74 | }); 75 | 76 | socket.on("signal", ({ roomId, signal, to }) => { 77 | io.to(to).emit("signal", { signal, from: socket.id }); 78 | }); 79 | 80 | socket.on("leave-call", ({ roomId }) => { 81 | socket.leave(roomId + "-call"); 82 | socket.to(roomId + "-call").emit("user-left-call", { socketId: socket.id }); 83 | }); 84 | 85 | socket.on("disconnect", () => { 86 | if (currentRoom && currentUser) { 87 | rooms.get(currentRoom).delete(currentUser); 88 | io.to(currentRoom).emit("userJoined", Array.from(rooms.get(currentRoom))); 89 | } 90 | console.log("A user disconnected", socket.id); 91 | }); 92 | }); 93 | 94 | const port = 3000; 95 | const __filename = fileURLToPath(import.meta.url); 96 | const __dirname = dirname(__filename); 97 | app.use(express.static(path.join(__dirname, "../frontend/vite-project/dist"))); 98 | 99 | app.get("*", (req, res) => { 100 | res.sendFile(path.join(__dirname, "../frontend/vite-project/dist/index.html")); 101 | }); 102 | 103 | server.listen(port, () => { 104 | console.log(`Server is running on port ${port}`); 105 | }); 106 | -------------------------------------------------------------------------------- /backend/middleware/errorHandler.js: -------------------------------------------------------------------------------- 1 | // Error handling middleware 2 | export const errorHandler = (err, req, res, next) => { 3 | console.error('Error:', err); 4 | 5 | // Default error 6 | let error = { 7 | message: err.message || 'Internal Server Error', 8 | status: err.status || 500 9 | }; 10 | 11 | // Handle specific error types 12 | if (err.name === 'ValidationError') { 13 | error.status = 400; 14 | error.message = 'Validation Error'; 15 | } 16 | 17 | if (err.name === 'CastError') { 18 | error.status = 400; 19 | error.message = 'Invalid ID format'; 20 | } 21 | 22 | // Send error response 23 | res.status(error.status).json({ 24 | success: false, 25 | error: error.message, 26 | ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) 27 | }); 28 | }; 29 | 30 | // 404 handler for undefined routes 31 | export const notFoundHandler = (req, res) => { 32 | res.status(404).json({ 33 | success: false, 34 | error: 'Route not found', 35 | path: req.path 36 | }); 37 | }; -------------------------------------------------------------------------------- /backend/middleware/logger.js: -------------------------------------------------------------------------------- 1 | // Logging middleware for request tracking 2 | export const requestLogger = (req, res, next) => { 3 | const start = Date.now(); 4 | 5 | // Log request details 6 | console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); 7 | 8 | // Log response details after request completes 9 | res.on('finish', () => { 10 | const duration = Date.now() - start; 11 | console.log(`${new Date().toISOString()} - ${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`); 12 | }); 13 | 14 | next(); 15 | }; 16 | 17 | // Socket connection logger 18 | export const socketLogger = (socket, next) => { 19 | console.log(`${new Date().toISOString()} - Socket connected: ${socket.id}`); 20 | next(); 21 | }; 22 | 23 | // Error logger for socket events 24 | export const socketErrorLogger = (error) => { 25 | console.error(`${new Date().toISOString()} - Socket error:`, error); 26 | }; -------------------------------------------------------------------------------- /backend/models/Room.js: -------------------------------------------------------------------------------- 1 | // Room model - handles room data and operations 2 | import roomDatabase from '../config/database.js'; 3 | 4 | class Room { 5 | constructor(roomId) { 6 | this.roomId = roomId; 7 | this.users = new Set(); 8 | this.files = new Map(); // Map of filename -> { code, language, lastModified } 9 | this.activeFile = 'untitled.js'; // Currently active file 10 | this.messages = []; 11 | 12 | // Initialize with default file 13 | this.files.set('untitled.js', { 14 | code: '', 15 | language: 'javascript', 16 | lastModified: new Date().toISOString(), 17 | createdBy: 'system' 18 | }); 19 | } 20 | // File management methods 21 | 22 | // Create a new file 23 | createFile(filename, createdBy, code = '', language = 'javascript') { 24 | if (this.files.has(filename)) { 25 | return { success: false, error: 'File already exists' }; 26 | } 27 | 28 | this.files.set(filename, { 29 | code, 30 | language, 31 | lastModified: new Date().toISOString(), 32 | createdBy 33 | }); 34 | 35 | return { success: true, file: this.getFile(filename) }; 36 | } 37 | 38 | // Delete a file 39 | deleteFile(filename) { 40 | if (!this.files.has(filename)) { 41 | return { success: false, error: 'File not found' }; 42 | } 43 | 44 | if (this.files.size <= 1) { 45 | return { success: false, error: 'Cannot delete the last file in room' }; 46 | } 47 | 48 | this.files.delete(filename); 49 | 50 | // If deleted file was active, switch to first available file 51 | if (this.activeFile === filename) { 52 | this.activeFile = this.files.keys().next().value; 53 | } 54 | 55 | return { success: true }; 56 | } 57 | 58 | // Rename a file 59 | renameFile(oldFilename, newFilename) { 60 | if (!this.files.has(oldFilename)) { 61 | return { success: false, error: 'File not found' }; 62 | } 63 | 64 | if (this.files.has(newFilename)) { 65 | return { success: false, error: 'File with new name already exists' }; 66 | } 67 | 68 | const fileData = this.files.get(oldFilename); 69 | this.files.delete(oldFilename); 70 | this.files.set(newFilename, { 71 | ...fileData, 72 | lastModified: new Date().toISOString() 73 | }); 74 | 75 | // Update active file if necessary 76 | if (this.activeFile === oldFilename) { 77 | this.activeFile = newFilename; 78 | } 79 | 80 | return { success: true }; 81 | } 82 | 83 | // Set active file 84 | setActiveFile(filename) { 85 | if (!this.files.has(filename)) { 86 | return { success: false, error: 'File not found' }; 87 | } 88 | 89 | this.activeFile = filename; 90 | return { success: true }; 91 | } 92 | 93 | // Get active file 94 | getActiveFile() { 95 | return this.activeFile; 96 | } 97 | 98 | // Get specific file 99 | getFile(filename) { 100 | const fileData = this.files.get(filename); 101 | if (!fileData) return null; 102 | 103 | return { 104 | filename, 105 | ...fileData, 106 | isActive: filename === this.activeFile 107 | }; 108 | } 109 | 110 | // Get all files 111 | getAllFiles() { 112 | return Array.from(this.files.entries()) 113 | .map(([filename, fileData]) => ({ 114 | filename, 115 | ...fileData, 116 | isActive: filename === this.activeFile 117 | })) 118 | .sort((a, b) => a.filename.localeCompare(b.filename)); 119 | } 120 | 121 | 122 | // Update file content 123 | updateFileCode(filename, code) { 124 | if (!this.files.has(filename)) { 125 | return { success: false, error: 'File not found' }; 126 | } 127 | 128 | const fileData = this.files.get(filename); 129 | this.files.set(filename, { 130 | ...fileData, 131 | code, 132 | lastModified: new Date().toISOString() 133 | }); 134 | 135 | return { success: true }; 136 | } 137 | 138 | // Update file language 139 | updateFileLanguage(filename, language) { 140 | if (!this.files.has(filename)) { 141 | return { success: false, error: 'File not found' }; 142 | } 143 | 144 | const fileData = this.files.get(filename); 145 | this.files.set(filename, { 146 | ...fileData, 147 | language, 148 | lastModified: new Date().toISOString() 149 | }); 150 | 151 | return { success: true }; 152 | } 153 | 154 | // Legacy methods for backward compatibility 155 | updateFilename(newFilename) { 156 | const result = this.renameFile(this.activeFile, newFilename); 157 | return result.success ? newFilename : null; 158 | } 159 | 160 | getFilename() { 161 | return this.activeFile; 162 | } 163 | 164 | // Add user to room 165 | addUser(userName) { 166 | this.users.add(userName); 167 | roomDatabase.addUserToRoom(this.roomId, userName); 168 | return this.getUsers(); 169 | } 170 | 171 | // Remove user from room 172 | removeUser(userName) { 173 | this.users.delete(userName); 174 | roomDatabase.removeUserFromRoom(this.roomId, userName); 175 | return this.getUsers(); 176 | } 177 | 178 | // Get all users in room 179 | getUsers() { 180 | return Array.from(this.users); 181 | } 182 | 183 | // Update code in room (for active file) 184 | updateCode(code) { 185 | return this.updateFileCode(this.activeFile, code).success ? code : null; 186 | } 187 | 188 | // Get current code (from active file) 189 | getCode() { 190 | const file = this.getFile(this.activeFile); 191 | return file ? file.code : ''; 192 | } 193 | 194 | // Update language (for active file) 195 | updateLanguage(language) { 196 | return this.updateFileLanguage(this.activeFile, language).success ? language : null; 197 | } 198 | 199 | // Get current language (from active file) 200 | getLanguage() { 201 | const file = this.getFile(this.activeFile); 202 | return file ? file.language : 'javascript'; 203 | } 204 | 205 | // Add chat message 206 | addMessage(userName, message) { 207 | const messageObj = { 208 | userName, 209 | message, 210 | timestamp: new Date().toISOString() 211 | }; 212 | this.messages.push(messageObj); 213 | return messageObj; 214 | } 215 | 216 | // Get recent messages (last 50) 217 | getMessages() { 218 | return this.messages.slice(-50); 219 | } 220 | 221 | // Check if user is in room 222 | hasUser(userName) { 223 | return this.users.has(userName); 224 | } 225 | 226 | // Get room info 227 | getInfo() { 228 | return { 229 | roomId: this.roomId, 230 | users: this.getUsers(), 231 | userCount: this.users.size, 232 | language: this.getLanguage(), 233 | activeFile: this.activeFile, 234 | files: this.getAllFiles() 235 | }; 236 | } 237 | } 238 | 239 | export default Room; -------------------------------------------------------------------------------- /backend/models/User.js: -------------------------------------------------------------------------------- 1 | // User model - optimized version 2 | class User { 3 | constructor(socketId, userName) { 4 | this.socketId = socketId; 5 | this.userName = userName; 6 | this.currentRoom = null; 7 | this._isTyping = false; 8 | this._isInCall = false; 9 | this._cameraOn = false; 10 | this._micOn = false; 11 | this.connectedAt = new Date(); 12 | } 13 | 14 | // ----- Room Management ----- 15 | joinRoom(roomId) { 16 | this.currentRoom = roomId; 17 | return this; // allow chaining 18 | } 19 | 20 | leaveRoom() { 21 | const leftRoom = this.currentRoom; 22 | this.currentRoom = null; 23 | return leftRoom; 24 | } 25 | 26 | // ----- Typing Status ----- 27 | set typing(status) { 28 | this._isTyping = Boolean(status); 29 | } 30 | 31 | get typing() { 32 | return this._isTyping; 33 | } 34 | 35 | // ----- Call Management ----- 36 | joinCall() { 37 | this._isInCall = true; 38 | return this; 39 | } 40 | 41 | leaveCall() { 42 | this._isInCall = false; 43 | this._cameraOn = false; 44 | this._micOn = false; 45 | return this; 46 | } 47 | 48 | toggleCamera() { 49 | this._cameraOn = !this._cameraOn; 50 | return this._cameraOn; 51 | } 52 | 53 | toggleMicrophone() { 54 | this._micOn = !this._micOn; 55 | return this._micOn; 56 | } 57 | 58 | // ----- User Info ----- 59 | get info() { 60 | return { 61 | socketId: this.socketId, 62 | userName: this.userName, 63 | currentRoom: this.currentRoom, 64 | isTyping: this._isTyping, 65 | isInCall: this._isInCall, 66 | cameraOn: this._cameraOn, 67 | micOn: this._micOn, 68 | connectedAt: this.connectedAt 69 | }; 70 | } 71 | 72 | updateName(newName) { 73 | this.userName = newName.trim(); 74 | return this.userName; 75 | } 76 | } 77 | 78 | export default User; 79 | -------------------------------------------------------------------------------- /backend/routes/httpRoutes.js: -------------------------------------------------------------------------------- 1 | // HTTP routes - handles REST API endpoints 2 | import express from 'express'; 3 | import RoomController from '../controllers/RoomController.js'; 4 | import VideoCallController from '../controllers/VideoCallController.js'; 5 | 6 | // Create controller instances (io will be null for HTTP routes) 7 | const roomController = new RoomController(null); 8 | const videoCallController = new VideoCallController(null); 9 | 10 | const router = express.Router(); 11 | 12 | // Health check endpoint 13 | router.get('/health', (req, res) => { 14 | res.json({ 15 | status: 'OK', 16 | timestamp: new Date().toISOString(), 17 | message: 'Collaborative Code Editor API is running' 18 | }); 19 | }); 20 | 21 | // Get room statistics 22 | router.get('/api/rooms/stats', (req, res) => { 23 | try { 24 | const stats = roomController.getRoomStats(); 25 | res.json(stats); 26 | } catch (error) { 27 | res.status(500).json({ error: 'Failed to get room statistics' }); 28 | } 29 | }); 30 | 31 | // Get call statistics 32 | router.get('/api/calls/stats', (req, res) => { 33 | try { 34 | const stats = videoCallController.getCallStats(); 35 | res.json(stats); 36 | } catch (error) { 37 | res.status(500).json({ error: 'Failed to get call statistics' }); 38 | } 39 | }); 40 | 41 | // Get room info by ID 42 | router.get('/api/rooms/:roomId', (req, res) => { 43 | try { 44 | const { roomId } = req.params; 45 | const roomInfo = roomController.getRoomInfo(roomId); 46 | 47 | if (roomInfo) { 48 | res.json(roomInfo); 49 | } else { 50 | res.status(404).json({ error: 'Room not found' }); 51 | } 52 | } catch (error) { 53 | res.status(500).json({ error: 'Failed to get room info' }); 54 | } 55 | }); 56 | 57 | // Get all active rooms 58 | router.get('/api/rooms', (req, res) => { 59 | try { 60 | const rooms = roomController.getRoomStats(); 61 | res.json(rooms); 62 | } catch (error) { 63 | res.status(500).json({ error: 'Failed to get rooms' }); 64 | } 65 | }); 66 | 67 | // API documentation endpoint 68 | router.get('/api/docs', (req, res) => { 69 | res.json({ 70 | name: 'Collaborative Code Editor API', 71 | version: '1.0.0', 72 | endpoints: { 73 | 'GET /health': 'Health check endpoint', 74 | 'GET /api/rooms/stats': 'Get room statistics', 75 | 'GET /api/calls/stats': 'Get call statistics', 76 | 'GET /api/rooms/:roomId': 'Get specific room info', 77 | 'GET /api/rooms': 'Get all active rooms' 78 | }, 79 | description: 'Real-time collaborative code editor with video calling capabilities' 80 | }); 81 | }); 82 | 83 | export default router; -------------------------------------------------------------------------------- /backend/routes/socketRoutes.js: -------------------------------------------------------------------------------- 1 | // Socket routes - handles all socket events and delegates to controllers 2 | import RoomController from "../controllers/RoomController.js"; 3 | import VideoCallController from "../controllers/VideoCallController.js"; 4 | 5 | class SocketRoutes { 6 | constructor() { 7 | this.roomController = null; 8 | this.videoCallController = null; 9 | } 10 | // Initialize socket event handlers 11 | initializeSocketHandlers(io) { 12 | // Initialize controllers with io instance 13 | this.roomController = new RoomController(io); 14 | this.videoCallController = new VideoCallController(io); 15 | 16 | io.on("connection", (socket) => { 17 | console.log("A user connected", socket.id); 18 | 19 | // File management events 20 | socket.on("createFile", (data) => { 21 | const result = this.roomController.handleCreateFile(socket, data); 22 | if (!result.success) { 23 | console.error("Create file error:", result.error); 24 | socket.emit("error", { type: "createFile", message: result.error }); 25 | } 26 | }); 27 | 28 | socket.on("deleteFile", (data) => { 29 | const result = this.roomController.handleDeleteFile(socket, data); 30 | if (!result.success) { 31 | console.error("Delete file error:", result.error); 32 | socket.emit("error", { type: "deleteFile", message: result.error }); 33 | } 34 | }); 35 | 36 | socket.on("renameFile", (data) => { 37 | const result = this.roomController.handleRenameFile(socket, data); 38 | if (!result.success) { 39 | console.error("Rename file error:", result.error); 40 | socket.emit("error", { type: "renameFile", message: result.error }); 41 | } 42 | }); 43 | 44 | socket.on("switchFile", (data) => { 45 | const result = this.roomController.handleSwitchFile(socket, data); 46 | if (!result.success) { 47 | console.error("Switch file error:", result.error); 48 | socket.emit("error", { type: "switchFile", message: result.error }); 49 | } 50 | }); 51 | 52 | socket.on("getFiles", (data) => { 53 | const result = this.roomController.handleGetFiles(socket, data); 54 | if (!result.success) { 55 | console.error("Get files error:", result.error); 56 | } 57 | }); 58 | 59 | socket.on("fileCodeChange", (data) => { 60 | const result = this.roomController.handleFileCodeChange(socket, data); 61 | if (!result.success) { 62 | console.error("File code change error:", result.error); 63 | } 64 | }); 65 | 66 | socket.on("fileLanguageChange", (data) => { 67 | const result = this.roomController.handleFileLanguageChange( 68 | socket, 69 | data 70 | ); 71 | if (!result.success) { 72 | console.error("File language change error:", result.error); 73 | } 74 | }); 75 | 76 | // Legacy filename change event 77 | socket.on("filenameChange", (data) => { 78 | const result = this.roomController.handleFilenameChange(socket, data); 79 | if (!result.success) { 80 | console.error("Filename change error:", result.error); 81 | } 82 | }); 83 | 84 | // Room-related events 85 | socket.on("join_room", (data) => { 86 | const result = this.roomController.handleJoinRoom(socket, data); 87 | if (!result.success) { 88 | console.error("Join room error:", result.error); 89 | } 90 | }); 91 | 92 | socket.on("codeChange", (data) => { 93 | const result = this.roomController.handleCodeChange(socket, data); 94 | if (!result.success) { 95 | console.error("Code change error:", result.error); 96 | } 97 | }); 98 | 99 | socket.on("languageChange", (data) => { 100 | const result = this.roomController.handleLanguageChange(socket, data); 101 | if (!result.success) { 102 | console.error("Language change error:", result.error); 103 | } 104 | }); 105 | 106 | socket.on("leaveRoom", () => { 107 | const result = this.roomController.handleLeaveRoom(socket); 108 | if (!result.success) { 109 | console.error("Leave room error:", result.error); 110 | } 111 | }); 112 | 113 | socket.on("typing", (data) => { 114 | const result = this.roomController.handleTyping(socket, data); 115 | if (!result.success) { 116 | console.error("Typing error:", result.error); 117 | } 118 | }); 119 | 120 | socket.on("chatMessage", (data) => { 121 | const result = this.roomController.handleChatMessage(socket, data); 122 | if (!result.success) { 123 | console.error("Chat message error:", result.error); 124 | } 125 | }); 126 | // it shows who is typing!! 127 | socket.on("typing", ({ roomId, user }) => { 128 | socket.to(roomId).emit("userTyping", { user }); 129 | }); 130 | 131 | socket.on("stopTyping", ({ roomId, user }) => { 132 | socket.to(roomId).emit("userStopTyping", { user }); 133 | }); 134 | 135 | // Video call events 136 | socket.on("join-call", (data) => { 137 | const result = this.videoCallController.handleJoinCall(socket, data); 138 | if (!result.success) { 139 | console.error("Join call error:", result.error); 140 | } 141 | }); 142 | 143 | socket.on("signal", (data) => { 144 | const result = this.videoCallController.handleSignal(socket, data); 145 | if (!result.success) { 146 | console.error("Signal error:", result.error); 147 | } 148 | }); 149 | 150 | socket.on("leave-call", (data) => { 151 | const result = this.videoCallController.handleLeaveCall(socket, data); 152 | if (!result.success) { 153 | console.error("Leave call error:", result.error); 154 | } 155 | }); 156 | 157 | socket.on("toggle-camera", () => { 158 | const result = this.videoCallController.handleToggleCamera(socket); 159 | if (!result.success) { 160 | console.error("Toggle camera error:", result.error); 161 | } 162 | }); 163 | 164 | socket.on("toggle-microphone", () => { 165 | const result = this.videoCallController.handleToggleMicrophone(socket); 166 | if (!result.success) { 167 | console.error("Toggle microphone error:", result.error); 168 | } 169 | }); 170 | 171 | // Disconnect event 172 | socket.on("disconnect", () => { 173 | const result = this.roomController.handleDisconnect(socket); 174 | if (!result.success) { 175 | console.error("Disconnect error:", result.error); 176 | } 177 | console.log("A user disconnected", socket.id); 178 | }); 179 | }); 180 | } 181 | 182 | // Get room statistics (for admin/analytics) 183 | getRoomStats() { 184 | return this.roomController ? this.roomController.getRoomStats() : null; 185 | } 186 | 187 | // Get call statistics (for admin/analytics) 188 | getCallStats() { 189 | return this.videoCallController 190 | ? this.videoCallController.getCallStats() 191 | : null; 192 | } 193 | } 194 | 195 | export default new SocketRoutes(); 196 | -------------------------------------------------------------------------------- /backend/screenshots/Feature_Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kushika-Agarwal/Collaborative-code-editor/4a392b968477422bcdf5bdf0c86c10d39297a355/backend/screenshots/Feature_Page.png -------------------------------------------------------------------------------- /backend/screenshots/Home_Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kushika-Agarwal/Collaborative-code-editor/4a392b968477422bcdf5bdf0c86c10d39297a355/backend/screenshots/Home_Page.png -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | // Main server file - Collaborative Code Editor Backend 2 | // MVC Architecture with Socket.IO for real-time collaboration 3 | 4 | import express from "express"; 5 | import http from "http"; 6 | import { Server } from "socket.io"; 7 | import path, { dirname, join } from "path"; 8 | import { fileURLToPath } from "url"; 9 | import cors from "cors"; 10 | 11 | // Import configurations 12 | import { serverConfig, staticConfig } from './config/server.js'; 13 | 14 | // Import routes 15 | import httpRoutes from './routes/httpRoutes.js'; 16 | import socketRoutes from './routes/socketRoutes.js'; 17 | 18 | // Import middleware 19 | import { requestLogger } from './middleware/logger.js'; 20 | import { errorHandler, notFoundHandler } from './middleware/errorHandler.js'; 21 | 22 | // Initialize Express app 23 | const app = express(); 24 | const server = http.createServer(app); 25 | 26 | // Initialize Socket.IO with CORS configuration 27 | const io = new Server(server, { 28 | cors: serverConfig.socket.cors 29 | }); 30 | 31 | // Middleware setup 32 | app.use(cors(serverConfig.cors)); 33 | app.use(express.json()); 34 | app.use(express.urlencoded({ extended: true })); 35 | app.use(requestLogger); 36 | 37 | // API routes 38 | app.use('/', httpRoutes); 39 | 40 | // Serve static files from frontend build 41 | const __filename = fileURLToPath(import.meta.url); 42 | const __dirname = dirname(__filename); 43 | app.use(express.static(join(__dirname, staticConfig.path))); 44 | 45 | // Socket.IO event handlers 46 | socketRoutes.initializeSocketHandlers(io); 47 | 48 | // Catch-all route for SPA (Single Page Application) 49 | app.get("/*", (req, res) => { 50 | res.sendFile(join(__dirname, staticConfig.path, staticConfig.options.index)); 51 | }); 52 | 53 | // Error handling middleware (must be last) 54 | app.use(notFoundHandler); 55 | app.use(errorHandler); 56 | 57 | // Start server 58 | const port = serverConfig.port; 59 | server.listen(port, () => { 60 | console.log(`🚀 Collaborative Code Editor Server is running on port ${port}`); 61 | console.log(`📊 API Documentation: http://localhost:${port}/api/docs`); 62 | console.log(`🏥 Health Check: http://localhost:${port}/health`); 63 | console.log(`📈 Room Stats: http://localhost:${port}/api/rooms/stats`); 64 | console.log(`📞 Call Stats: http://localhost:${port}/api/calls/stats`); 65 | }); 66 | 67 | // Graceful shutdown 68 | process.on('SIGTERM', () => { 69 | console.log('SIGTERM received, shutting down gracefully'); 70 | server.close(() => { 71 | console.log('Server closed'); 72 | process.exit(0); 73 | }); 74 | }); 75 | 76 | process.on('SIGINT', () => { 77 | console.log('SIGINT received, shutting down gracefully'); 78 | server.close(() => { 79 | console.log('Server closed'); 80 | process.exit(0); 81 | }); 82 | }); 83 | 84 | export default app; -------------------------------------------------------------------------------- /backend/services/RoomService.js: -------------------------------------------------------------------------------- 1 | // Room service - handles business logic for room operations 2 | import Room from '../models/Room.js'; 3 | 4 | class RoomService { 5 | // File management methods 6 | 7 | // Create new file in room 8 | createFileInRoom(roomId, filename, createdBy, code = '', language = 'javascript') { 9 | const room = this.rooms.get(roomId); 10 | if (room) { 11 | return room.createFile(filename, createdBy, code, language); 12 | } 13 | return { success: false, error: 'Room not found' }; 14 | } 15 | 16 | // Delete file from room 17 | deleteFileFromRoom(roomId, filename) { 18 | const room = this.rooms.get(roomId); 19 | if (room) { 20 | return room.deleteFile(filename); 21 | } 22 | return { success: false, error: 'Room not found' }; 23 | } 24 | 25 | // Rename file in room 26 | renameFileInRoom(roomId, oldFilename, newFilename) { 27 | const room = this.rooms.get(roomId); 28 | if (room) { 29 | return room.renameFile(oldFilename, newFilename); 30 | } 31 | return { success: false, error: 'Room not found' }; 32 | } 33 | 34 | // Set active file in room 35 | setActiveFileInRoom(roomId, filename) { 36 | const room = this.rooms.get(roomId); 37 | if (room) { 38 | return room.setActiveFile(filename); 39 | } 40 | return { success: false, error: 'Room not found' }; 41 | } 42 | 43 | // Get all files in room 44 | getRoomFiles(roomId) { 45 | const room = this.rooms.get(roomId); 46 | return room ? room.getAllFiles() : []; 47 | } 48 | 49 | // Get specific file in room 50 | getRoomFile(roomId, filename) { 51 | const room = this.rooms.get(roomId); 52 | return room ? room.getFile(filename) : null; 53 | } 54 | 55 | // Update file code in room 56 | updateFileCodeInRoom(roomId, filename, code) { 57 | const room = this.rooms.get(roomId); 58 | if (room) { 59 | return room.updateFileCode(filename, code); 60 | } 61 | return { success: false, error: 'Room not found' }; 62 | } 63 | 64 | // Update file language in room 65 | updateFileLanguageInRoom(roomId, filename, language) { 66 | const room = this.rooms.get(roomId); 67 | if (room) { 68 | return room.updateFileLanguage(filename, language); 69 | } 70 | return { success: false, error: 'Room not found' }; 71 | } 72 | 73 | // Update filename in room (legacy method for backward compatibility) 74 | updateRoomFilename(roomId, newFilename) { 75 | const room = this.rooms.get(roomId); 76 | if (room) { 77 | return room.updateFilename(newFilename); 78 | } 79 | return null; 80 | } 81 | constructor() { 82 | this.rooms = new Map(); // In-memory room storage 83 | } 84 | 85 | // Get or create a room 86 | getOrCreateRoom(roomId) { 87 | if (!this.rooms.has(roomId)) { 88 | this.rooms.set(roomId, new Room(roomId)); 89 | } 90 | return this.rooms.get(roomId); 91 | } 92 | 93 | // Add user to room 94 | addUserToRoom(roomId, userName) { 95 | const room = this.getOrCreateRoom(roomId); 96 | const users = room.addUser(userName); 97 | return { room, users }; 98 | } 99 | 100 | // Remove user from room 101 | removeUserFromRoom(roomId, userName) { 102 | const room = this.rooms.get(roomId); 103 | if (room) { 104 | const users = room.removeUser(userName); 105 | 106 | // Clean up empty rooms 107 | if (users.length === 0) { 108 | this.rooms.delete(roomId); 109 | } 110 | 111 | return { room, users }; 112 | } 113 | return { room: null, users: [] }; 114 | } 115 | 116 | // Update code in room 117 | updateRoomCode(roomId, code) { 118 | const room = this.rooms.get(roomId); 119 | if (room) { 120 | return room.updateCode(code); 121 | } 122 | return null; 123 | } 124 | 125 | // Update language in room 126 | updateRoomLanguage(roomId, language) { 127 | const room = this.rooms.get(roomId); 128 | if (room) { 129 | return room.updateLanguage(language); 130 | } 131 | return null; 132 | } 133 | 134 | // Add message to room 135 | addMessageToRoom(roomId, userName, message) { 136 | const room = this.rooms.get(roomId); 137 | if (room) { 138 | return room.addMessage(userName, message); 139 | } 140 | return null; 141 | } 142 | 143 | // Get room users 144 | getRoomUsers(roomId) { 145 | const room = this.rooms.get(roomId); 146 | return room ? room.getUsers() : []; 147 | } 148 | 149 | // Get room info 150 | getRoomInfo(roomId) { 151 | const room = this.rooms.get(roomId); 152 | return room ? room.getInfo() : null; 153 | } 154 | 155 | // Check if room exists 156 | roomExists(roomId) { 157 | return this.rooms.has(roomId); 158 | } 159 | 160 | // Get all rooms (for admin purposes) 161 | getAllRooms() { 162 | return Array.from(this.rooms.keys()); 163 | } 164 | 165 | // Get room statistics 166 | getRoomStats() { 167 | const stats = { 168 | totalRooms: this.rooms.size, 169 | rooms: [] 170 | }; 171 | 172 | for (const [roomId, room] of this.rooms) { 173 | stats.rooms.push({ 174 | roomId, 175 | userCount: room.getUsers().length, 176 | language: room.getLanguage() 177 | }); 178 | } 179 | 180 | return stats; 181 | } 182 | } 183 | 184 | export default new RoomService(); -------------------------------------------------------------------------------- /backend/services/UserService.js: -------------------------------------------------------------------------------- 1 | // User service - handles business logic for user operations 2 | import User from '../models/User.js'; 3 | 4 | class UserService { 5 | constructor() { 6 | this.users = new Map(); // In-memory user storage by socket ID 7 | } 8 | 9 | // Create a new user 10 | createUser(socketId, userName) { 11 | const user = new User(socketId, userName); 12 | this.users.set(socketId, user); 13 | return user; 14 | } 15 | 16 | // Get user by socket ID 17 | getUser(socketId) { 18 | return this.users.get(socketId); 19 | } 20 | 21 | // Remove user 22 | removeUser(socketId) { 23 | const user = this.users.get(socketId); 24 | if (user) { 25 | this.users.delete(socketId); 26 | return user; 27 | } 28 | return null; 29 | } 30 | 31 | // Update user's current room 32 | updateUserRoom(socketId, roomId) { 33 | const user = this.getUser(socketId); 34 | if (user) { 35 | return user.joinRoom(roomId); 36 | } 37 | return null; 38 | } 39 | 40 | // Remove user from current room 41 | removeUserFromRoom(socketId) { 42 | const user = this.getUser(socketId); 43 | if (user) { 44 | return user.leaveRoom(); 45 | } 46 | return null; 47 | } 48 | 49 | // Update user's typing status 50 | updateUserTyping(socketId, isTyping) { 51 | const user = this.getUser(socketId); 52 | if (user) { 53 | return user.setTyping(isTyping); 54 | } 55 | return false; 56 | } 57 | 58 | // Update user's call status 59 | updateUserCallStatus(socketId, isInCall) { 60 | const user = this.getUser(socketId); 61 | if (user) { 62 | if (isInCall) { 63 | return user.joinCall(); 64 | } else { 65 | return user.leaveCall(); 66 | } 67 | } 68 | return false; 69 | } 70 | 71 | // Toggle user's camera 72 | toggleUserCamera(socketId) { 73 | const user = this.getUser(socketId); 74 | if (user) { 75 | return user.toggleCamera(); 76 | } 77 | return false; 78 | } 79 | 80 | // Toggle user's microphone 81 | toggleUserMicrophone(socketId) { 82 | const user = this.getUser(socketId); 83 | if (user) { 84 | return user.toggleMicrophone(); 85 | } 86 | return false; 87 | } 88 | 89 | // Get user info 90 | getUserInfo(socketId) { 91 | const user = this.getUser(socketId); 92 | return user ? user.getInfo() : null; 93 | } 94 | 95 | // Update user name 96 | updateUserName(socketId, newName) { 97 | const user = this.getUser(socketId); 98 | if (user) { 99 | return user.updateName(newName); 100 | } 101 | return null; 102 | } 103 | 104 | // Get all users in a room 105 | getUsersInRoom(roomId) { 106 | const usersInRoom = []; 107 | for (const [socketId, user] of this.users) { 108 | if (user.getCurrentRoom() === roomId) { 109 | usersInRoom.push(user.getInfo()); 110 | } 111 | } 112 | return usersInRoom; 113 | } 114 | 115 | // Get all connected users 116 | getAllUsers() { 117 | const allUsers = []; 118 | for (const [socketId, user] of this.users) { 119 | allUsers.push(user.getInfo()); 120 | } 121 | return allUsers; 122 | } 123 | 124 | // Get user statistics 125 | getUserStats() { 126 | const stats = { 127 | totalUsers: this.users.size, 128 | usersInRooms: 0, 129 | usersInCalls: 0 130 | }; 131 | 132 | for (const [socketId, user] of this.users) { 133 | if (user.getCurrentRoom()) { 134 | stats.usersInRooms++; 135 | } 136 | if (user.isInCall) { 137 | stats.usersInCalls++; 138 | } 139 | } 140 | 141 | return stats; 142 | } 143 | } 144 | 145 | export default new UserService(); -------------------------------------------------------------------------------- /frontend/vite-project/.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 | -------------------------------------------------------------------------------- /frontend/vite-project/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. 13 | -------------------------------------------------------------------------------- /frontend/vite-project/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import { defineConfig, globalIgnores } from 'eslint/config' 6 | 7 | export default defineConfig([ 8 | globalIgnores(['dist']), 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | extends: [ 12 | js.configs.recommended, 13 | reactHooks.configs['recommended-latest'], 14 | reactRefresh.configs.vite, 15 | ], 16 | languageOptions: { 17 | ecmaVersion: 2020, 18 | globals: globals.browser, 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | ecmaFeatures: { jsx: true }, 22 | sourceType: 'module', 23 | }, 24 | }, 25 | rules: { 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | }, 28 | }, 29 | ]) 30 | -------------------------------------------------------------------------------- /frontend/vite-project/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | Vite + React 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/vite-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-project", 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 | "test": "echo \"No tests specified\" && exit 0" 12 | }, 13 | "dependencies": { 14 | "@hookform/resolvers": "^3.10.0", 15 | "@jridgewell/trace-mapping": "^0.3.25", 16 | "@monaco-editor/react": "^4.7.0", 17 | "@neondatabase/serverless": "^0.10.4", 18 | "@radix-ui/react-accordion": "^1.2.4", 19 | "@radix-ui/react-alert-dialog": "^1.1.7", 20 | "@radix-ui/react-aspect-ratio": "^1.1.3", 21 | "@radix-ui/react-avatar": "^1.1.4", 22 | "@radix-ui/react-checkbox": "^1.1.5", 23 | "@radix-ui/react-collapsible": "^1.1.4", 24 | "@radix-ui/react-context-menu": "^2.2.7", 25 | "@radix-ui/react-dialog": "^1.1.7", 26 | "@radix-ui/react-dropdown-menu": "^2.1.7", 27 | "@radix-ui/react-hover-card": "^1.1.7", 28 | "@radix-ui/react-label": "^2.1.3", 29 | "@radix-ui/react-menubar": "^1.1.7", 30 | "@radix-ui/react-navigation-menu": "^1.2.6", 31 | "@radix-ui/react-popover": "^1.1.7", 32 | "@radix-ui/react-progress": "^1.1.3", 33 | "@radix-ui/react-radio-group": "^1.2.4", 34 | "@radix-ui/react-scroll-area": "^1.2.4", 35 | "@radix-ui/react-select": "^2.1.7", 36 | "@radix-ui/react-separator": "^1.1.3", 37 | "@radix-ui/react-slider": "^1.2.4", 38 | "@radix-ui/react-slot": "^1.2.0", 39 | "@radix-ui/react-switch": "^1.1.4", 40 | "@radix-ui/react-tabs": "^1.1.4", 41 | "@radix-ui/react-toast": "^1.2.7", 42 | "@radix-ui/react-toggle": "^1.1.3", 43 | "@radix-ui/react-toggle-group": "^1.1.3", 44 | "@radix-ui/react-tooltip": "^1.2.0", 45 | "@tanstack/react-query": "^5.60.5", 46 | "buffer": "^6.0.3", 47 | "class-variance-authority": "^0.7.1", 48 | "clsx": "^2.1.1", 49 | "cmdk": "^1.1.1", 50 | "connect-pg-simple": "^10.0.0", 51 | "date-fns": "^3.6.0", 52 | "drizzle-orm": "^0.39.1", 53 | "drizzle-zod": "^0.7.0", 54 | "embla-carousel-react": "^8.6.0", 55 | "events": "^3.3.0", 56 | "express": "^4.21.2", 57 | "express-session": "^1.18.1", 58 | "framer-motion": "^11.13.1", 59 | "global": "^4.4.0", 60 | "input-otp": "^1.4.2", 61 | "jszip": "^3.10.1", 62 | "lucide-react": "^0.453.0", 63 | "memorystore": "^1.6.7", 64 | "monaco-editor": "^0.52.2", 65 | "next-themes": "^0.4.6", 66 | "passport": "^0.7.0", 67 | "passport-local": "^1.0.0", 68 | "process": "^0.11.10", 69 | "react": "^18.3.1", 70 | "react-day-picker": "^8.10.1", 71 | "react-dom": "^18.3.1", 72 | "react-hook-form": "^7.55.0", 73 | "react-icons": "^5.4.0", 74 | "react-resizable-panels": "^2.1.7", 75 | "recharts": "^2.15.2", 76 | "simple-peer": "^9.11.1", 77 | "socket.io-client": "^4.8.1", 78 | "tailwind-merge": "^2.6.0", 79 | "tailwindcss-animate": "^1.0.7", 80 | "tw-animate-css": "^1.2.5", 81 | "util": "^0.12.5", 82 | "uuid": "^11.1.0", 83 | "vaul": "^1.1.2", 84 | "wouter": "^3.3.5", 85 | "ws": "^8.18.0", 86 | "zod": "^3.24.2", 87 | "zod-validation-error": "^3.4.0" 88 | }, 89 | "devDependencies": { 90 | "@eslint/js": "^9.30.1", 91 | "@tailwindcss/postcss": "^4.1.12", 92 | "@tailwindcss/typography": "^0.5.15", 93 | "@tailwindcss/vite": "^4.1.3", 94 | "@types/connect-pg-simple": "^7.0.3", 95 | "@types/express": "4.17.21", 96 | "@types/express-session": "^1.18.0", 97 | "@types/node": "^22.12.0", 98 | "@types/passport": "^1.0.16", 99 | "@types/passport-local": "^1.0.38", 100 | "@types/react": "^18.3.11", 101 | "@types/react-dom": "^18.3.1", 102 | "@types/ws": "^8.5.13", 103 | "@vitejs/plugin-react": "^4.3.2", 104 | "autoprefixer": "^10.4.21", 105 | "drizzle-kit": "^0.30.4", 106 | "esbuild": "^0.25.0", 107 | "eslint": "^9.30.1", 108 | "eslint-plugin-react-hooks": "^5.2.0", 109 | "eslint-plugin-react-refresh": "^0.4.20", 110 | "globals": "^16.3.0", 111 | "postcss": "^8.5.6", 112 | "tailwindcss": "^3.4.17", 113 | "vite": "^7.0.4" 114 | }, 115 | "optionalDependencies": { 116 | "bufferutil": "^4.0.8" 117 | }, 118 | "description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.", 119 | "main": "eslint.config.js", 120 | "keywords": [], 121 | "author": "", 122 | "license": "ISC" 123 | } 124 | -------------------------------------------------------------------------------- /frontend/vite-project/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/vite-project/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/vite-project/src/VersionHistory.css: -------------------------------------------------------------------------------- 1 | /* Version History Modal Styles */ 2 | .version-history-modal { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | background-color: rgba(0, 0, 0, 0.5); 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | z-index: 1000; 13 | } 14 | 15 | .version-history-content { 16 | background: white; 17 | border-radius: 8px; 18 | width: 90%; 19 | max-width: 600px; 20 | height: 80%; 21 | max-height: 700px; 22 | display: flex; 23 | flex-direction: column; 24 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 25 | } 26 | 27 | .version-history-header { 28 | display: flex; 29 | justify-content: space-between; 30 | align-items: center; 31 | padding: 20px; 32 | border-bottom: 1px solid #e1e5e9; 33 | background: #f8f9fa; 34 | border-radius: 8px 8px 0 0; 35 | } 36 | 37 | .version-history-header h3 { 38 | margin: 0; 39 | color: #2c3e50; 40 | font-size: 1.2rem; 41 | } 42 | 43 | .close-button { 44 | background: none; 45 | border: none; 46 | font-size: 24px; 47 | cursor: pointer; 48 | color: #6c757d; 49 | padding: 0; 50 | width: 30px; 51 | height: 30px; 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | border-radius: 50%; 56 | transition: background-color 0.2s; 57 | } 58 | 59 | .close-button:hover { 60 | background-color: #e9ecef; 61 | color: #495057; 62 | } 63 | 64 | /* Undo/Redo Controls */ 65 | .undo-redo-controls { 66 | display: flex; 67 | gap: 10px; 68 | padding: 15px 20px; 69 | border-bottom: 1px solid #e1e5e9; 70 | background: #f8f9fa; 71 | align-items: center; 72 | } 73 | .version-buttons{ 74 | flex-wrap: wrap; 75 | } 76 | 77 | .undo-button, 78 | .redo-button { 79 | padding: 8px 16px; 80 | border: 1px solid #dee2e6; 81 | border-radius: 4px; 82 | background: white; 83 | cursor: pointer; 84 | font-size: 14px; 85 | transition: all 0.2s; 86 | display: flex; 87 | align-items: center; 88 | gap: 5px; 89 | } 90 | 91 | .undo-button:hover:not(.disabled), 92 | .redo-button:hover:not(.disabled) { 93 | background: #e9ecef; 94 | border-color: #adb5bd; 95 | } 96 | 97 | .undo-button.disabled, 98 | .redo-button.disabled { 99 | opacity: 0.5; 100 | cursor: not-allowed; 101 | background: #f8f9fa; 102 | } 103 | 104 | .version-info { 105 | margin-left: auto; 106 | font-size: 14px; 107 | color: #6c757d; 108 | font-weight: 500; 109 | } 110 | 111 | /* Version List */ 112 | .version-list { 113 | flex: 1; 114 | overflow-y: auto; 115 | padding: 10px; 116 | } 117 | 118 | .version-item { 119 | border: 1px solid #e1e5e9; 120 | border-radius: 6px; 121 | margin-bottom: 10px; 122 | padding: 15px; 123 | background: white; 124 | transition: box-shadow 0.2s; 125 | } 126 | 127 | .version-item:hover { 128 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 129 | } 130 | 131 | .version-header { 132 | display: flex; 133 | align-items: flex-start; 134 | gap: 12px; 135 | margin-bottom: 10px; 136 | } 137 | 138 | .version-icon { 139 | font-size: 20px; 140 | min-width: 24px; 141 | } 142 | 143 | .version-info { 144 | flex: 1; 145 | } 146 | 147 | .version-meta { 148 | display: flex; 149 | gap: 10px; 150 | align-items: center; 151 | margin-bottom: 4px; 152 | } 153 | 154 | .version-user { 155 | font-weight: 600; 156 | color: #2c3e50; 157 | font-size: 14px; 158 | } 159 | 160 | .version-type { 161 | background: #e9ecef; 162 | padding: 2px 8px; 163 | border-radius: 12px; 164 | font-size: 12px; 165 | color: #495057; 166 | } 167 | 168 | .version-timestamp { 169 | font-size: 12px; 170 | color: #6c757d; 171 | } 172 | 173 | .version-details { 174 | display: flex; 175 | gap: 15px; 176 | margin-bottom: 10px; 177 | font-size: 13px; 178 | color: #6c757d; 179 | } 180 | 181 | .code-length::before { 182 | content: "📄 "; 183 | } 184 | 185 | .language::before { 186 | content: "🔧 "; 187 | } 188 | 189 | .version-actions { 190 | display: flex; 191 | gap: 8px; 192 | } 193 | 194 | .view-button, 195 | .revert-button { 196 | padding: 6px 12px; 197 | border: 1px solid #dee2e6; 198 | border-radius: 4px; 199 | background: white; 200 | cursor: pointer; 201 | font-size: 12px; 202 | transition: all 0.2s; 203 | } 204 | 205 | .view-button:hover { 206 | background: #e3f2fd; 207 | border-color: #2196f3; 208 | color: #1976d2; 209 | } 210 | 211 | .revert-button:hover { 212 | background: #fff3e0; 213 | border-color: #ff9800; 214 | color: #f57c00; 215 | } 216 | 217 | /* Loading and Empty States */ 218 | .loading, 219 | .no-versions { 220 | text-align: center; 221 | padding: 40px 20px; 222 | color: #6c757d; 223 | font-style: italic; 224 | } 225 | 226 | /* Footer */ 227 | .version-history-footer { 228 | padding: 15px 20px; 229 | border-top: 1px solid #e1e5e9; 230 | background: #f8f9fa; 231 | display: flex; 232 | justify-content: center; 233 | } 234 | 235 | .refresh-button { 236 | padding: 8px 16px; 237 | border: 1px solid var(--accent-color); 238 | border-radius: 4px; 239 | background: var(--accent-color); 240 | color: white; 241 | cursor: pointer; 242 | font-size: 14px; 243 | transition: all 0.2s; 244 | display: flex; 245 | align-items: center; 246 | gap: 5px; 247 | } 248 | 249 | .refresh-button:hover { 250 | background: var(--accent-hover); 251 | border-color: var(--accent-hover); 252 | } 253 | 254 | /* Version Details Modal */ 255 | .version-details-modal { 256 | position: fixed; 257 | top: 0; 258 | left: 0; 259 | width: 100%; 260 | height: 100%; 261 | background-color: rgba(0, 0, 0, 0.7); 262 | display: flex; 263 | justify-content: center; 264 | align-items: center; 265 | z-index: 1001; 266 | } 267 | 268 | .version-details-content { 269 | background: white; 270 | border-radius: 8px; 271 | width: 90%; 272 | max-width: 500px; 273 | max-height: 80%; 274 | overflow-y: auto; 275 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); 276 | } 277 | 278 | .version-details-header { 279 | display: flex; 280 | justify-content: space-between; 281 | align-items: center; 282 | padding: 20px; 283 | border-bottom: 1px solid #e1e5e9; 284 | background: #f8f9fa; 285 | border-radius: 8px 8px 0 0; 286 | } 287 | 288 | .version-details-header h4 { 289 | margin: 0; 290 | color: #2c3e50; 291 | } 292 | 293 | .close-details-button { 294 | background: none; 295 | border: none; 296 | font-size: 20px; 297 | cursor: pointer; 298 | color: #6c757d; 299 | padding: 0; 300 | width: 25px; 301 | height: 25px; 302 | display: flex; 303 | align-items: center; 304 | justify-content: center; 305 | border-radius: 50%; 306 | transition: background-color 0.2s; 307 | } 308 | 309 | .close-details-button:hover { 310 | background-color: #e9ecef; 311 | } 312 | 313 | .version-details-body { 314 | padding: 20px; 315 | } 316 | 317 | .detail-row { 318 | margin-bottom: 15px; 319 | line-height: 1.5; 320 | } 321 | 322 | .detail-row strong { 323 | color: #2c3e50; 324 | display: inline-block; 325 | min-width: 80px; 326 | } 327 | 328 | .code-preview { 329 | margin-top: 20px; 330 | } 331 | 332 | .code-content { 333 | background: #f8f9fa; 334 | border: 1px solid #e1e5e9; 335 | border-radius: 4px; 336 | padding: 15px; 337 | margin-top: 10px; 338 | white-space: pre-wrap; 339 | font-family: "Monaco", "Consolas", "Courier New", monospace; 340 | font-size: 12px; 341 | line-height: 1.4; 342 | max-height: 200px; 343 | overflow-y: auto; 344 | } 345 | 346 | /* Responsive Design */ 347 | @media (max-width: 768px) { 348 | .version-history-content { 349 | width: 95%; 350 | height: 90%; 351 | } 352 | 353 | .version-header { 354 | flex-direction: column; 355 | gap: 8px; 356 | } 357 | 358 | .version-meta { 359 | flex-direction: column; 360 | gap: 4px; 361 | align-items: flex-start; 362 | } 363 | 364 | .version-details { 365 | flex-direction: column; 366 | gap: 8px; 367 | } 368 | 369 | .version-actions { 370 | justify-content: flex-end; 371 | } 372 | 373 | .undo-redo-controls { 374 | flex-wrap: wrap; 375 | gap: 8px; 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /frontend/vite-project/src/VersionHistory.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from "react"; 2 | import "./VersionHistory.css"; 3 | 4 | const VersionHistory = ({ socket, roomId, isOpen, onClose }) => { 5 | const [versions, setVersions] = useState([]); 6 | const [loading, setLoading] = useState(false); 7 | const [selectedVersion, setSelectedVersion] = useState(null); 8 | const [undoRedoState, setUndoRedoState] = useState({ 9 | canUndo: false, 10 | canRedo: false, 11 | currentVersionIndex: -1, 12 | totalVersions: 0, 13 | }); 14 | 15 | // Memoized fetch functions 16 | const fetchVersionHistory = useCallback(() => { 17 | if (socket && roomId) { 18 | setLoading(true); 19 | socket.emit("getVersionHistory", { roomId, limit: 50 }); 20 | } 21 | }, [socket, roomId]); 22 | 23 | const fetchUndoRedoState = useCallback(() => { 24 | if (socket && roomId) { 25 | socket.emit("getUndoRedoState", { roomId }); 26 | } 27 | }, [socket, roomId]); 28 | 29 | useEffect(() => { 30 | if (!socket || !roomId || !isOpen) return; 31 | 32 | fetchVersionHistory(); 33 | fetchUndoRedoState(); 34 | }, [isOpen, fetchVersionHistory, fetchUndoRedoState, roomId, socket]); 35 | 36 | useEffect(() => { 37 | if (!socket) return; 38 | 39 | const handleVersionHistoryResponse = (response) => { 40 | setLoading(false); 41 | if (response.success) { 42 | setVersions(response.versions); 43 | setUndoRedoState(response.undoRedoState); 44 | } else { 45 | console.error("Failed to fetch version history:", response.error); 46 | } 47 | }; 48 | 49 | const handleUndoRedoStateResponse = (response) => { 50 | if (response.success) { 51 | setUndoRedoState(response.undoRedoState); 52 | } 53 | }; 54 | 55 | const handleVersionAdded = (data) => { 56 | setUndoRedoState(data.undoRedoState); 57 | if (isOpen) fetchVersionHistory(); 58 | }; 59 | 60 | const handleCodeReverted = (data) => { 61 | setUndoRedoState(data.undoRedoState); 62 | if (isOpen) fetchVersionHistory(); 63 | }; 64 | 65 | const handleVersionDetailsResponse = (response) => { 66 | if (response.success) { 67 | setSelectedVersion(response.version); 68 | } 69 | }; 70 | 71 | socket.on("versionHistoryResponse", handleVersionHistoryResponse); 72 | socket.on("undoRedoStateResponse", handleUndoRedoStateResponse); 73 | socket.on("versionAdded", handleVersionAdded); 74 | socket.on("codeReverted", handleCodeReverted); 75 | socket.on("versionDetailsResponse", handleVersionDetailsResponse); 76 | 77 | return () => { 78 | socket.off("versionHistoryResponse", handleVersionHistoryResponse); 79 | socket.off("undoRedoStateResponse", handleUndoRedoStateResponse); 80 | socket.off("versionAdded", handleVersionAdded); 81 | socket.off("codeReverted", handleCodeReverted); 82 | socket.off("versionDetailsResponse", handleVersionDetailsResponse); 83 | }; 84 | }, [socket, isOpen, fetchVersionHistory]); 85 | 86 | const handleUndo = () => { 87 | if (socket && undoRedoState.canUndo) socket.emit("undo", { roomId }); 88 | }; 89 | 90 | const handleRedo = () => { 91 | if (socket && undoRedoState.canRedo) socket.emit("redo", { roomId }); 92 | }; 93 | 94 | const handleRevertToVersion = (versionId) => { 95 | if (socket && versionId) { 96 | const confirmRevert = window.confirm( 97 | "Are you sure you want to revert to this version? This will create a new version." 98 | ); 99 | if (confirmRevert) socket.emit("revertToVersion", { roomId, versionId }); 100 | } 101 | }; 102 | 103 | const handleViewVersion = (versionId) => { 104 | if (socket) socket.emit("getVersionDetails", { roomId, versionId }); 105 | }; 106 | 107 | const formatTimestamp = (timestamp) => 108 | new Date(timestamp).toLocaleString(); 109 | 110 | const getChangeTypeIcon = (changeType) => { 111 | switch (changeType) { 112 | case "initial": return "🎯"; 113 | case "code_change": return "📝"; 114 | case "language_change": return "🔧"; 115 | case "revert": return "↶"; 116 | case "checkpoint": return "📌"; 117 | default: return "📄"; 118 | } 119 | }; 120 | 121 | const getChangeTypeLabel = (changeType) => { 122 | switch (changeType) { 123 | case "initial": return "Initial"; 124 | case "code_change": return "Code Change"; 125 | case "language_change": return "Language Change"; 126 | case "revert": return "Reverted"; 127 | case "checkpoint": return "Checkpoint"; 128 | default: return "Unknown"; 129 | } 130 | }; 131 | 132 | if (!isOpen) return null; 133 | 134 | return ( 135 |
136 |
137 |
138 |

Version History

139 | 142 |
143 | 144 | {/* Undo/Redo Controls */} 145 |
146 | 156 | 166 | 167 | {undoRedoState.currentVersionIndex + 1} /{" "} 168 | {undoRedoState.totalVersions} 169 | 170 |
171 | 172 | {/* Version List */} 173 |
174 | {loading ? ( 175 |
Loading version history...
176 | ) : versions.length > 0 ? ( 177 | versions.map((version) => ( 178 |
179 |
180 | 181 | {getChangeTypeIcon(version.changeType)} 182 | 183 |
184 |
185 | {version.userName} 186 | 187 | {getChangeTypeLabel(version.changeType)} 188 | 189 |
190 |
191 | {formatTimestamp(version.timestamp)} 192 |
193 |
194 |
195 |
196 | 197 | {version.codeLength} characters 198 | 199 | {version.language} 200 |
201 |
202 | 208 | 214 |
215 |
216 | )) 217 | ) : ( 218 |
No version history available
219 | )} 220 |
221 | 222 | {/* Refresh Button */} 223 |
224 | 227 |
228 |
229 | 230 | {/* Version Details Modal */} 231 | {selectedVersion && ( 232 |
233 |
234 |
235 |

Version Details

236 | 242 |
243 |
244 |
245 | User: {selectedVersion.userName} 246 |
247 |
248 | Type:{" "} 249 | {getChangeTypeLabel(selectedVersion.changeType)} 250 |
251 |
252 | Time:{" "} 253 | {formatTimestamp(selectedVersion.timestamp)} 254 |
255 |
256 | Language: {selectedVersion.language} 257 |
258 |
259 | Code Preview: 260 |
261 |                   {selectedVersion.code.substring(0, 500)}
262 |                   {selectedVersion.code.length > 500 && "..."}
263 |                 
264 |
265 |
266 |
267 |
268 | )} 269 |
270 | ); 271 | }; 272 | 273 | export default VersionHistory; 274 | -------------------------------------------------------------------------------- /frontend/vite-project/src/VideoCall.css: -------------------------------------------------------------------------------- 1 | /* =============================== THEME VARIABLES =================================*/ 2 | :root { 3 | /* Dark mode (default) */ 4 | --bg-color: #161719; 5 | --text-color: #ffffff; 6 | --button-text-color: #fff; 7 | --accent-color: #6725d9; 8 | --accent-hover: #793be4; 9 | --video-bg: #6725d9; 10 | --placeholder-bg: #9568e3; 11 | --label-color: #7ed6df; 12 | --shadow-color: rgba(247, 247, 247, 0.5); 13 | --mic-bg: #222; 14 | --mic-gradient: linear-gradient(90deg, #7ed6df 0%, #00b894 100%); 15 | --border-color: gray; 16 | } 17 | 18 | /* Light mode overrides */ 19 | .light-mode { 20 | --bg-color: #bd9df6; 21 | --text-color: #fffbfb; 22 | --button-text-color: #fff; 23 | /* Keep same accent colors as dark mode */ 24 | --accent-color: #6725d9; 25 | --accent-hover: #793be4; 26 | --video-bg: #6725d9; 27 | --placeholder-bg: #ceb3fc; 28 | --label-color: #f5f5f5; 29 | --shadow-color: rgba(0, 0, 0, 0.2); 30 | --mic-bg: #ddd; 31 | --mic-gradient: linear-gradient(90deg, #4a90e2 0%, #50e3c2 100%); 32 | --border-color: #ccc; 33 | } 34 | /* =============================== STYLES =================================*/ 35 | .video-call-panel { 36 | border: 1px solid var(--border-color); 37 | position: absolute; 38 | top: 20px; 39 | right: 20px; 40 | min-width: 320px; 41 | background: var(--bg-color); 42 | color: var(--text-color); 43 | border-radius: 8px; 44 | box-shadow: 0 2px 8px var(--shadow-color); 45 | z-index: 1000; 46 | padding: 0.5rem 1rem 1rem 1rem; 47 | cursor: grab; 48 | user-select: none; 49 | } 50 | .video-call-header { 51 | cursor: grab; 52 | background: var(--accent-color); 53 | border-radius: 8px 8px 0 0; 54 | padding: 0.5rem; 55 | margin: -0.5rem -1rem 0.5rem -1rem; 56 | text-align: center; 57 | color: var(--button-text-color); 58 | } 59 | .video-call-controls { 60 | display: flex; 61 | gap: 0.5rem; 62 | margin-bottom: 0.5rem; 63 | justify-content: center; 64 | } 65 | .video-call-controls button { 66 | background: var(--accent-color); 67 | color: var(--button-text-color); 68 | border: none; 69 | border-radius: 4px; 70 | padding: 0.6rem 0.8rem; 71 | font-size: 0.85rem; 72 | cursor: pointer; 73 | transition: background 0.2s; 74 | } 75 | .video-call-controls button:hover { 76 | background: var(--accent-hover); 77 | } 78 | .video-call-videos { 79 | display: flex; 80 | flex-wrap: wrap; 81 | gap: 0.5rem; 82 | justify-content: center; 83 | } 84 | .video-tile { 85 | background: var(--video-bg); 86 | border-top-left-radius: 10rem; 87 | border-top-right-radius: 10rem; 88 | border-bottom-left-radius: 1rem; 89 | border-bottom-right-radius: 1rem; 90 | padding: 0.3rem; 91 | width: 100px; 92 | height: 100px; 93 | display: flex; 94 | flex-direction: column; 95 | align-items: center; 96 | justify-content: flex-end; 97 | position: relative; 98 | } 99 | .video-tile video { 100 | width: 90px; 101 | height: 70px; 102 | border-radius: 4px; 103 | background: #000; 104 | object-fit: cover; 105 | } 106 | .video-placeholder { 107 | width: 82px; 108 | height: 67px; 109 | background: var(--placeholder-bg); 110 | color: var(--text-color); 111 | display: flex; 112 | align-items: center; 113 | justify-content: center; 114 | border-top-left-radius: 10rem; 115 | border-top-right-radius: 10rem; 116 | border-bottom-left-radius: 1rem; 117 | border-bottom-right-radius: 1rem; 118 | font-size: 2.5rem; 119 | } 120 | .video-label { 121 | font-size: 0.85rem; 122 | margin-top: 0.2rem; 123 | text-align: center; 124 | color: var(--label-color); 125 | } 126 | .mic-volume-bar { 127 | display: inline-block; 128 | vertical-align: middle; 129 | width: 40px; 130 | height: 8px; 131 | background: var(--mic-bg); 132 | border-radius: 4px; 133 | margin-left: 6px; 134 | overflow: hidden; 135 | } 136 | .mic-volume-fill { 137 | display: block; 138 | height: 100%; 139 | background: var(--mic-gradient); 140 | border-radius: 4px; 141 | transition: width 0.1s linear; 142 | } 143 | 144 | /* New styles for improved microphone UI */ 145 | .initializing-indicator { 146 | font-size: 0.7rem; 147 | margin-left: 0.5rem; 148 | opacity: 0.8; 149 | } 150 | 151 | .media-error { 152 | background: #ff4757; 153 | color: white; 154 | padding: 0.4rem; 155 | margin: 0.5rem 0; 156 | border-radius: 4px; 157 | font-size: 0.8rem; 158 | text-align: center; 159 | } 160 | 161 | .video-call-controls button.active { 162 | background: #00b894; 163 | } 164 | 165 | .video-call-controls button.active:hover { 166 | background: #00a085; 167 | } 168 | 169 | .video-call-controls button:disabled { 170 | background: #666; 171 | cursor: not-allowed; 172 | opacity: 0.6; 173 | } 174 | 175 | .video-call-controls button:disabled:hover { 176 | background: #666; 177 | } 178 | -------------------------------------------------------------------------------- /frontend/vite-project/src/assets/gssoc logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kushika-Agarwal/Collaborative-code-editor/4a392b968477422bcdf5bdf0c86c10d39297a355/frontend/vite-project/src/assets/gssoc logo.png -------------------------------------------------------------------------------- /frontend/vite-project/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ChatWindow.css: -------------------------------------------------------------------------------- 1 | /* ChatWindow styles are embedded in the component for the detached window */ 2 | .chat-window-placeholder { 3 | display: none; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ChatWindow.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import './ChatWindow.css'; 3 | 4 | const ChatWindow = ({ 5 | chatMessages, 6 | chatInput, 7 | setChatInput, 8 | sendChatMessage, 9 | onClose 10 | }) => { 11 | const windowRef = useRef(null); 12 | 13 | useEffect(() => { 14 | // Open new window for detached chat 15 | const newWindow = window.open( 16 | '', 17 | 'ChatWindow', 18 | 'width=400,height=500,scrollbars=yes,resizable=yes,status=no,location=no,toolbar=no,menubar=no' 19 | ); 20 | 21 | if (newWindow) { 22 | windowRef.current = newWindow; 23 | 24 | // Set up the detached window content 25 | newWindow.document.title = 'Collaborative Code Editor - Chat'; 26 | newWindow.document.head.innerHTML = ` 27 | 28 | 29 | Chat Window 30 | 148 | `; 149 | 150 | // Handle window close 151 | newWindow.addEventListener('beforeunload', () => { 152 | onClose(); 153 | }); 154 | 155 | // Initial render 156 | renderChatContent(newWindow); 157 | } 158 | 159 | return () => { 160 | if (windowRef.current && !windowRef.current.closed) { 161 | windowRef.current.close(); 162 | } 163 | }; 164 | }, []); 165 | 166 | const renderChatContent = (targetWindow) => { 167 | if (!targetWindow || targetWindow.closed) return; 168 | 169 | targetWindow.document.body.innerHTML = ` 170 |
171 |
172 |
💬 Chat
173 | 174 |
175 | 176 |
177 | ${chatMessages.length === 0 178 | ? '
No messages yet. Start the conversation!
' 179 | : chatMessages.map(msg => ` 180 |
181 | ${msg.userName.slice(0, 8)}: 182 | ${msg.message} 183 |
184 | `).join('') 185 | } 186 |
187 | 188 |
189 | 197 | 198 |
199 |
200 | `; 201 | 202 | // Add event listeners 203 | const form = targetWindow.document.getElementById('detached-chat-form'); 204 | const input = targetWindow.document.getElementById('detached-input'); 205 | 206 | // Set up input value 207 | if (input) { 208 | input.value = chatInput || ""; 209 | input.setSelectionRange(input.value.length, input.value.length); 210 | input.addEventListener('input', (e) => { 211 | setChatInput(e.target.value); 212 | }); 213 | input.focus(); 214 | } 215 | 216 | if (form && input) { 217 | form.addEventListener('submit', (e) => { 218 | e.preventDefault(); 219 | if (input.value.trim()) { 220 | sendChatMessage(e); 221 | input.value = ''; 222 | } 223 | }); 224 | 225 | input.addEventListener('input', (e) => { 226 | setChatInput(e.target.value); 227 | }); 228 | 229 | // Focus input 230 | input.focus(); 231 | } 232 | 233 | // Auto-scroll to bottom 234 | const messagesContainer = targetWindow.document.getElementById('detached-messages'); 235 | if (messagesContainer) { 236 | messagesContainer.scrollTop = messagesContainer.scrollHeight; 237 | } 238 | }; 239 | 240 | // Re-render when messages change 241 | useEffect(() => { 242 | if (windowRef.current && !windowRef.current.closed) { 243 | renderChatContent(windowRef.current); 244 | } 245 | }, [chatMessages, chatInput]); 246 | 247 | // This component doesn't render anything in the main window 248 | return null; 249 | }; 250 | 251 | export default ChatWindow; 252 | -------------------------------------------------------------------------------- /frontend/vite-project/src/components/FileExplorer.css: -------------------------------------------------------------------------------- 1 | .file-explorer { 2 | background: var(--explorer-bg, #252526); 3 | border: 1px solid var(--explorer-border, #404040); 4 | border-radius: 4px; 5 | margin-bottom: 20px; 6 | } 7 | 8 | .file-explorer-header { 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | padding: 8px 12px; 13 | background: var(--explorer-header-bg, #2d2d2d); 14 | border-bottom: 1px solid var(--explorer-border, #404040); 15 | border-radius: 4px 4px 0 0; 16 | } 17 | 18 | /* File Actions Toolbar */ 19 | .file-actions-toolbar { 20 | display: flex; 21 | flex-wrap: wrap; 22 | gap: 8px; 23 | padding: 8px 12px; 24 | background: var(--explorer-bg, #252526); 25 | border-bottom: 1px solid var(--explorer-border, #404040); 26 | } 27 | 28 | .file-actions-toolbar .import-actions, 29 | .file-actions-toolbar .export-actions { 30 | display: flex; 31 | flex-wrap:wrap; 32 | gap: 8px; 33 | width:100%; 34 | } 35 | 36 | .file-actions-toolbar button { 37 | flex: 1; 38 | min-width: 120px; 39 | justify-content: center; 40 | white-space: nowrap; 41 | display: flex; 42 | align-items: center; 43 | gap: 6px; 44 | padding: 8px 12px; 45 | border-radius: 4px; 46 | border: 1px solid var(--btn-border, #555); 47 | background: var(--btn-bg, #3a3d41); 48 | color: var(--btn-text, #e0e0e0); 49 | font-size: 13px; 50 | cursor: pointer; 51 | transition: all 0.2s ease; 52 | } 53 | 54 | .file-actions-toolbar button:hover { 55 | background: var(--btn-hover-bg, #4a4e54); 56 | border-color: var(--btn-hover-border, #666); 57 | } 58 | 59 | .file-actions-toolbar .import-files-btn { 60 | background: var(--import-bg, #2c5b8e); 61 | border-color: var(--import-border, #3a7cbf); 62 | } 63 | 64 | .file-actions-toolbar .import-folder-btn { 65 | background: var(--import-bg, #2c5b8e); 66 | border-color: var(--import-border, #3a7cbf); 67 | } 68 | 69 | .file-actions-toolbar .export-current-btn { 70 | background: var(--export-bg, #2e7d32); 71 | border-color: var(--export-border, #3d8b40); 72 | } 73 | 74 | .file-actions-toolbar .export-all-btn { 75 | background: var(--export-bg, #2e7d32); 76 | border-color: var(--export-border, #3d8b40); 77 | } 78 | 79 | .file-actions-toolbar .icon { 80 | font-size: 14px; 81 | flex-shrink:0; 82 | } 83 | 84 | /* Light mode adjustments */ 85 | .light-mode .file-actions-toolbar { 86 | background: var(--explorer-bg, #f5f5f5); 87 | border-bottom-color: var(--explorer-border, #ddd); 88 | } 89 | 90 | .light-mode .file-actions-toolbar button { 91 | background: var(--btn-bg, #e0e0e0); 92 | color: var(--btn-text, #333); 93 | border-color: var(--btn-border, #ccc); 94 | } 95 | 96 | .light-mode .file-actions-toolbar button:hover { 97 | background: var(--btn-hover-bg, #d0d0d0); 98 | border-color: var(--btn-hover-border, #bbb); 99 | } 100 | 101 | .file-explorer-header h3 { 102 | margin: 0; 103 | font-size: 14px; 104 | font-weight: 600; 105 | color: var(--explorer-text, #cccccc); 106 | } 107 | 108 | .file-actions { 109 | display: flex; 110 | gap: 6px; 111 | } 112 | 113 | .file-actions button { 114 | background: var(--btn-secondary-bg, #3a3d41); 115 | color: #fff; 116 | border: 1px solid var(--explorer-border, #404040); 117 | border-radius: 3px; 118 | padding: 4px 8px; 119 | font-size: 12px; 120 | cursor: pointer; 121 | transition: background-color 0.2s ease, border-color 0.2s ease; 122 | } 123 | 124 | .file-actions button:hover { 125 | background: var(--btn-secondary-hover, #4a4e54); 126 | border-color: var(--input-focus-border, #007acc); 127 | } 128 | 129 | .light-mode .file-actions button { 130 | background: var(--btn-secondary-bg, #e9ecef); 131 | color: #333; 132 | border-color: var(--explorer-border, #d0d0d0); 133 | } 134 | 135 | .light-mode .file-actions button:hover { 136 | background: var(--btn-secondary-hover, #dee2e6); 137 | border-color: var(--input-focus-border, #007acc); 138 | } 139 | 140 | .new-file-btn { 141 | background: var(--btn-primary-bg, #007acc); 142 | border: none; 143 | color: white; 144 | width: 24px; 145 | height: 24px; 146 | border-radius: 3px; 147 | cursor: pointer; 148 | font-size: 16px; 149 | font-weight: bold; 150 | display: flex; 151 | align-items: center; 152 | justify-content: center; 153 | transition: background-color 0.2s ease; 154 | } 155 | 156 | .new-file-btn:hover { 157 | background: var(--btn-primary-hover, #0086d1); 158 | } 159 | 160 | .new-file-form-container { 161 | padding: 12px; 162 | background: var(--form-bg, #1e1e1e); 163 | border-bottom: 1px solid var(--explorer-border, #404040); 164 | } 165 | 166 | .new-file-form { 167 | display: flex; 168 | flex-direction: column; 169 | gap: 8px; 170 | } 171 | 172 | .new-file-input { 173 | background: var(--input-bg, #3c3c3c); 174 | border: 1px solid var(--input-border, #555555); 175 | color: var(--input-text, #ffffff); 176 | padding: 6px 8px; 177 | border-radius: 3px; 178 | font-size: 13px; 179 | outline: none; 180 | } 181 | 182 | .new-file-input:focus { 183 | border-color: var(--input-focus-border, #007acc); 184 | } 185 | 186 | .new-file-buttons { 187 | display: flex; 188 | gap: 6px; 189 | justify-content: flex-end; 190 | } 191 | 192 | .create-btn, .cancel-btn { 193 | padding: 4px 12px; 194 | border: none; 195 | border-radius: 3px; 196 | font-size: 12px; 197 | cursor: pointer; 198 | transition: all 0.2s ease; 199 | } 200 | 201 | .create-btn { 202 | background: var(--btn-success-bg, #28a745); 203 | color: white; 204 | } 205 | 206 | .create-btn:hover { 207 | background: var(--btn-success-hover, #218838); 208 | } 209 | 210 | .cancel-btn { 211 | background: var(--btn-secondary-bg, #6c757d); 212 | color: white; 213 | } 214 | 215 | .cancel-btn:hover { 216 | background: var(--btn-secondary-hover, #5a6268); 217 | } 218 | 219 | .file-tabs-container { 220 | display: flex; 221 | flex-direction: column; 222 | gap: 2px; 223 | padding: 8px; 224 | max-height: 300px; 225 | overflow-y: auto; 226 | } 227 | 228 | .no-files-message { 229 | padding: 20px; 230 | text-align: center; 231 | color: var(--text-muted, #888888); 232 | font-style: italic; 233 | font-size: 13px; 234 | } 235 | 236 | .context-menu { 237 | background: var(--menu-bg, #2d2d2d); 238 | border: 1px solid var(--menu-border, #454545); 239 | border-radius: 4px; 240 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 241 | overflow: hidden; 242 | min-width: 120px; 243 | } 244 | 245 | .context-menu-item { 246 | display: block; 247 | width: 100%; 248 | padding: 8px 12px; 249 | background: none; 250 | border: none; 251 | color: var(--menu-text, #cccccc); 252 | font-size: 13px; 253 | text-align: left; 254 | cursor: pointer; 255 | transition: background-color 0.2s ease; 256 | } 257 | 258 | .context-menu-item:hover:not(:disabled) { 259 | background: var(--menu-hover-bg, #3d3d3d); 260 | } 261 | 262 | .context-menu-item:disabled { 263 | color: var(--menu-text-disabled, #666666); 264 | cursor: not-allowed; 265 | } 266 | 267 | /* Light mode styles */ 268 | .light-mode .file-explorer { 269 | --explorer-bg: #ffffff; 270 | --explorer-border: #d0d0d0; 271 | --explorer-header-bg: #f8f9fa; 272 | --explorer-text: #333333; 273 | --btn-primary-bg: #007acc; 274 | --btn-primary-hover: #0086d1; 275 | --form-bg: #f8f9fa; 276 | --input-bg: #ffffff; 277 | --input-border: #ced4da; 278 | --input-text: #333333; 279 | --input-focus-border: #007acc; 280 | --btn-success-bg: #28a745; 281 | --btn-success-hover: #218838; 282 | --btn-secondary-bg: #6c757d; 283 | --btn-secondary-hover: #5a6268; 284 | --text-muted: #666666; 285 | --menu-bg: #ffffff; 286 | --menu-border: #d0d0d0; 287 | --menu-text: #333333; 288 | --menu-hover-bg: #f8f9fa; 289 | --menu-text-disabled: #999999; 290 | } 291 | 292 | /* Scrollbar styling */ 293 | .file-tabs-container::-webkit-scrollbar { 294 | width: 6px; 295 | } 296 | 297 | .file-tabs-container::-webkit-scrollbar-track { 298 | background: var(--scrollbar-track, #1e1e1e); 299 | } 300 | 301 | .file-tabs-container::-webkit-scrollbar-thumb { 302 | background: var(--scrollbar-thumb, #404040); 303 | border-radius: 3px; 304 | } 305 | 306 | .file-tabs-container::-webkit-scrollbar-thumb:hover { 307 | background: var(--scrollbar-thumb-hover, #555555); 308 | } 309 | 310 | .light-mode .file-tabs-container::-webkit-scrollbar-track { 311 | background: var(--scrollbar-track, #f1f1f1); 312 | } 313 | 314 | .light-mode .file-tabs-container::-webkit-scrollbar-thumb { 315 | background: var(--scrollbar-thumb, #c1c1c1); 316 | } 317 | 318 | .light-mode .file-tabs-container::-webkit-scrollbar-thumb:hover { 319 | background: var(--scrollbar-thumb-hover, #a8a8a8); 320 | } -------------------------------------------------------------------------------- /frontend/vite-project/src/components/FileExplorer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import FileTab from './FileTab'; 3 | import './FileExplorer.css'; 4 | import { detectFileType } from '../utils/fileTypeDetection'; 5 | 6 | const FileExplorer = ({ 7 | files = [], 8 | activeFile, 9 | onFileCreate, 10 | onFileDelete, 11 | onFileRename, 12 | onFileSwitch, 13 | userName 14 | }) => { 15 | const [showNewFileForm, setShowNewFileForm] = useState(false); 16 | const [newFileName, setNewFileName] = useState(''); 17 | const [showContextMenu, setShowContextMenu] = useState(null); 18 | const fileInputRef = React.useRef(null); 19 | const folderInputRef = React.useRef(null); 20 | 21 | const handleCreateFile = (e) => { 22 | e.preventDefault(); 23 | if (newFileName.trim()) { 24 | const filename = newFileName.trim(); 25 | const language = detectFileType(filename) || 'javascript'; 26 | onFileCreate(filename, userName, '', language); 27 | setNewFileName(''); 28 | setShowNewFileForm(false); 29 | } 30 | }; 31 | 32 | const handleFileDelete = (filename) => { 33 | if (files.length <= 1) { 34 | alert('Cannot delete the last file in the room'); 35 | return; 36 | } 37 | 38 | if (confirm(`Are you sure you want to delete "${filename}"?`)) { 39 | onFileDelete(filename, userName); 40 | } 41 | setShowContextMenu(null); 42 | }; 43 | 44 | const handleContextMenu = (e, filename) => { 45 | e.preventDefault(); 46 | setShowContextMenu({ filename, x: e.clientX, y: e.clientY }); 47 | }; 48 | 49 | const closeContextMenu = () => { 50 | setShowContextMenu(null); 51 | }; 52 | 53 | // Close context menu when clicking outside 54 | React.useEffect(() => { 55 | const handleClick = () => closeContextMenu(); 56 | document.addEventListener('click', handleClick); 57 | return () => document.removeEventListener('click', handleClick); 58 | }, []); 59 | 60 | // Import: read selected FileList and create files in room 61 | const importFileList = async (fileList) => { 62 | if (!fileList || fileList.length === 0) return; 63 | const readFile = (f) => new Promise((resolve, reject) => { 64 | const reader = new FileReader(); 65 | reader.onload = () => resolve(reader.result ?? ''); 66 | reader.onerror = reject; 67 | reader.readAsText(f); 68 | }); 69 | for (const f of fileList) { 70 | try { 71 | // Prefer webkitRelativePath to preserve folders; fallback to name 72 | const name = (f.webkitRelativePath && f.webkitRelativePath.length > 0) ? f.webkitRelativePath : f.name; 73 | const code = await readFile(f); 74 | const language = detectFileType(name) || 'javascript'; 75 | onFileCreate(name, userName, code, language); 76 | } catch (err) { 77 | console.error('Failed to import file:', f.name, err); 78 | } 79 | } 80 | }; 81 | 82 | const triggerImportFiles = () => fileInputRef.current?.click(); 83 | const triggerImportFolder = () => folderInputRef.current?.click(); 84 | 85 | const onFilesSelected = async (e) => { 86 | const list = e.target.files; 87 | await importFileList(list); 88 | e.target.value = ''; 89 | }; 90 | 91 | const onFolderSelected = async (e) => { 92 | const list = e.target.files; 93 | await importFileList(list); 94 | e.target.value = ''; 95 | }; 96 | 97 | // Export helpers 98 | const downloadBlob = (content, filename, type = 'text/plain;charset=utf-8') => { 99 | const blob = new Blob([content], { type }); 100 | const url = URL.createObjectURL(blob); 101 | const a = document.createElement('a'); 102 | a.href = url; 103 | a.download = filename; 104 | document.body.appendChild(a); 105 | a.click(); 106 | a.remove(); 107 | URL.revokeObjectURL(url); 108 | }; 109 | 110 | const handleExportCurrent = () => { 111 | const file = files.find(f => f.filename === activeFile); 112 | if (!file) { 113 | alert('No active file to export'); 114 | return; 115 | } 116 | downloadBlob(file.code ?? '', file.filename); 117 | }; 118 | 119 | const handleExportAll = async () => { 120 | if (!files || files.length === 0) { 121 | alert('No files to export'); 122 | return; 123 | } 124 | try { 125 | const JSZip = (await import('jszip')).default; 126 | const zip = new JSZip(); 127 | files.forEach(f => { 128 | const path = f.filename || 'untitled.txt'; 129 | zip.file(path, f.code ?? ''); 130 | }); 131 | const blob = await zip.generateAsync({ type: 'blob' }); 132 | const url = URL.createObjectURL(blob); 133 | const a = document.createElement('a'); 134 | a.href = url; 135 | a.download = 'project.zip'; 136 | document.body.appendChild(a); 137 | a.click(); 138 | a.remove(); 139 | URL.revokeObjectURL(url); 140 | } catch (err) { 141 | console.error('Export all failed:', err); 142 | alert('Failed to export all files'); 143 | } 144 | }; 145 | 146 | return ( 147 |
148 |
149 |

Files

150 | 157 |
158 | 159 | {/* File Actions Toolbar */} 160 |
161 |
162 | 165 | 168 |
169 |
170 | 173 | 176 |
177 | {/* Hidden inputs for file/folder selection */} 178 | 185 | 193 |
194 | 195 | {showNewFileForm && ( 196 |
197 |
198 | setNewFileName(e.target.value)} 202 | placeholder="Enter filename (e.g., main.js)" 203 | className="new-file-input" 204 | autoFocus 205 | /> 206 |
207 | 208 | 218 |
219 |
220 |
221 | )} 222 | 223 |
224 | {files.map((file) => ( 225 |
handleContextMenu(e, file.filename)} 228 | > 229 | onFileSwitch(file.filename, userName)} 233 | onClose={files.length > 1 ? (filename) => handleFileDelete(filename) : null} 234 | onRename={onFileRename} 235 | isRenamable={true} 236 | /> 237 |
238 | ))} 239 |
240 | 241 | {files.length === 0 && ( 242 |
243 | No files in this room. Create a new file to get started. 244 |
245 | )} 246 | 247 | {/* Context Menu */} 248 | {showContextMenu && ( 249 |
258 | 265 | 274 |
275 | )} 276 |
277 | ); 278 | }; 279 | 280 | export default FileExplorer; 281 | -------------------------------------------------------------------------------- /frontend/vite-project/src/components/FileTab.css: -------------------------------------------------------------------------------- 1 | .file-tab { 2 | display: flex; 3 | align-items: center; 4 | padding: 8px 12px; 5 | background: var(--tab-bg, #2d2d2d); 6 | border: 1px solid var(--tab-border, #404040); 7 | border-bottom: none; 8 | border-radius: 4px 4px 0 0; 9 | cursor: pointer; 10 | position: relative; 11 | min-width: 120px; 12 | max-width: 200px; 13 | transition: all 0.2s ease; 14 | user-select: none; 15 | gap: 6px; 16 | } 17 | .typing-indicator { 18 | font-size: 0.9rem; 19 | color: #888; 20 | margin-top: 4px; 21 | padding-left: 8px; 22 | font-style: italic; 23 | } 24 | 25 | .file-tab:hover { 26 | background: var(--tab-hover-bg, #3d3d3d); 27 | } 28 | 29 | .file-tab.active { 30 | background: var(--tab-active-bg, #1e1e1e); 31 | border-color: var(--tab-active-border, #007acc); 32 | border-bottom: 1px solid var(--tab-active-bg, #1e1e1e); 33 | } 34 | 35 | .file-icon { 36 | font-size: 14px; 37 | flex-shrink: 0; 38 | } 39 | 40 | .file-name { 41 | flex: 1; 42 | font-size: 13px; 43 | color: var(--tab-text, #cccccc); 44 | white-space: nowrap; 45 | overflow: hidden; 46 | text-overflow: ellipsis; 47 | } 48 | 49 | .file-tab.active .file-name { 50 | color: var(--tab-active-text, #ffffff); 51 | } 52 | 53 | .file-close-btn { 54 | background: none; 55 | border: none; 56 | color: var(--tab-close, #999999); 57 | font-size: 16px; 58 | font-weight: bold; 59 | cursor: pointer; 60 | padding: 0; 61 | width: 16px; 62 | height: 16px; 63 | display: flex; 64 | align-items: center; 65 | justify-content: center; 66 | border-radius: 2px; 67 | flex-shrink: 0; 68 | opacity: 0; 69 | transition: all 0.2s ease; 70 | } 71 | 72 | .file-tab:hover .file-close-btn { 73 | opacity: 1; 74 | } 75 | 76 | .file-close-btn:hover { 77 | background: var(--tab-close-hover-bg, #ff6b6b); 78 | color: white; 79 | } 80 | 81 | .file-modified-indicator { 82 | color: var(--tab-modified, #ffa500); 83 | font-size: 12px; 84 | flex-shrink: 0; 85 | } 86 | 87 | .file-rename-form { 88 | flex: 1; 89 | margin: 0; 90 | } 91 | 92 | .file-rename-input { 93 | width: 100%; 94 | background: var(--input-bg, #404040); 95 | border: 1px solid var(--input-border, #007acc); 96 | color: var(--input-text, #ffffff); 97 | padding: 2px 4px; 98 | font-size: 13px; 99 | border-radius: 2px; 100 | outline: none; 101 | } 102 | 103 | /* Light mode styles */ 104 | .light-mode .file-tab { 105 | --tab-bg: #f3f3f3; 106 | --tab-border: #d0d0d0; 107 | --tab-hover-bg: #e8e8e8; 108 | --tab-active-bg: #ffffff; 109 | --tab-active-border: #007acc; 110 | --tab-text: #333333; 111 | --tab-active-text: #000000; 112 | --tab-close: #666666; 113 | --tab-close-hover-bg: #ff6b6b; 114 | --tab-modified: #ff8c00; 115 | --input-bg: #ffffff; 116 | --input-border: #007acc; 117 | --input-text: #000000; 118 | } 119 | 120 | /* Animation for new tabs */ 121 | .file-tab { 122 | animation: slideInTab 0.2s ease-out; 123 | } 124 | 125 | @keyframes slideInTab { 126 | from { 127 | opacity: 0; 128 | transform: translateY(-10px); 129 | } 130 | to { 131 | opacity: 1; 132 | transform: translateY(0); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /frontend/vite-project/src/components/FileTab.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import './FileTab.css'; 3 | import { io } from 'socket.io-client'; 4 | 5 | // Replace with your backend port 6 | const socket = io('http://localhost:3000'); 7 | 8 | const FileTab = ({ 9 | file, 10 | isActive, 11 | onClick, 12 | onClose, 13 | onRename, 14 | isRenamable = true, 15 | roomId, 16 | currentUser 17 | }) => { 18 | const [isEditing, setIsEditing] = useState(false); 19 | const [newName, setNewName] = useState(file.filename); 20 | const [typingUsers, setTypingUsers] = useState([]); 21 | const typingTimeout = useRef(null); 22 | 23 | useEffect(() => { 24 | socket.on('userTyping', ({ user }) => { 25 | setTypingUsers((prev) => 26 | prev.includes(user) ? prev : [...prev, user] 27 | ); 28 | }); 29 | 30 | socket.on('userStopTyping', ({ user }) => { 31 | setTypingUsers((prev) => prev.filter((u) => u !== user)); 32 | }); 33 | 34 | return () => { 35 | socket.off('userTyping'); 36 | socket.off('userStopTyping'); 37 | }; 38 | }, []); 39 | 40 | const handleDoubleClick = () => { 41 | if (isRenamable) { 42 | setIsEditing(true); 43 | setNewName(file.filename); 44 | } 45 | }; 46 | 47 | const handleSubmit = (e) => { 48 | e.preventDefault(); 49 | if (newName.trim() && newName !== file.filename) { 50 | onRename(file.filename, newName.trim()); 51 | } 52 | setIsEditing(false); 53 | socket.emit('stopTyping', { roomId, user: currentUser }); 54 | }; 55 | 56 | const handleKeyDown = (e) => { 57 | if (e.key === 'Escape') { 58 | setIsEditing(false); 59 | setNewName(file.filename); 60 | socket.emit('stopTyping', { roomId, user: currentUser }); 61 | } 62 | }; 63 | 64 | const handleCloseClick = (e) => { 65 | e.stopPropagation(); 66 | onClose(file.filename); 67 | }; 68 | 69 | const handleInputChange = (e) => { 70 | setNewName(e.target.value); 71 | socket.emit('typing', { roomId, user: currentUser }); 72 | 73 | clearTimeout(typingTimeout.current); 74 | typingTimeout.current = setTimeout(() => { 75 | socket.emit('stopTyping', { roomId, user: currentUser }); 76 | }, 1000); 77 | }; 78 | 79 | const getFileIcon = (filename) => { 80 | const ext = filename.split('.').pop()?.toLowerCase(); 81 | switch (ext) { 82 | case 'js': 83 | case 'jsx': 84 | return '📄'; 85 | case 'ts': 86 | case 'tsx': 87 | return '📘'; 88 | case 'py': 89 | return '🐍'; 90 | case 'html': 91 | return '🌐'; 92 | case 'css': 93 | return '🎨'; 94 | case 'json': 95 | return '📋'; 96 | case 'md': 97 | return '📝'; 98 | case 'txt': 99 | return '📄'; 100 | default: 101 | return '📄'; 102 | } 103 | }; 104 | 105 | return ( 106 |
112 | {getFileIcon(file.filename)} 113 | 114 | {isEditing ? ( 115 |
116 | { 122 | setIsEditing(false); 123 | socket.emit('stopTyping', { roomId, user: currentUser }); 124 | }} 125 | className="file-rename-input" 126 | autoFocus 127 | /> 128 |
129 | ) : ( 130 | {file.filename} 131 | )} 132 | 133 | {onClose && ( 134 | 141 | )} 142 | 143 | {file.lastModified && !isActive && ( 144 | 145 | • 146 | 147 | )} 148 | 149 | 150 |
151 | ); 152 | }; 153 | 154 | function TypingIndicator({ typingUsers, currentUser }) { 155 | const othersTyping = typingUsers.filter((u) => u !== currentUser); 156 | if (othersTyping.length === 0) return null; 157 | 158 | return ( 159 |
160 | {othersTyping.join(', ')} {othersTyping.length === 1 ? 'is' : 'are'} typing... 161 |
162 | ); 163 | } 164 | 165 | export default FileTab; -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ResizableLayout.css: -------------------------------------------------------------------------------- 1 | /* Resizable Layout Styles */ 2 | .resizable-layout { 3 | display: flex; 4 | height: 100vh; 5 | width: 100%; 6 | position: relative; 7 | overflow: hidden; 8 | } 9 | 10 | /* Sidebar */ 11 | .resizable-sidebar { 12 | min-width: 200px; 13 | max-width: 400px; 14 | height: 100vh; 15 | overflow-y: auto; 16 | flex-shrink: 0; 17 | background: var(--sidebar-bg); 18 | position: relative; 19 | z-index: 1; 20 | } 21 | 22 | /* Editor */ 23 | .resizable-editor { 24 | flex: 1; 25 | height: 100vh; 26 | overflow: hidden; 27 | position: relative; 28 | background-color: var(--panel-bg); 29 | } 30 | 31 | /* Chat Panel */ 32 | .resizable-chat { 33 | min-width: 250px; 34 | max-width: 500px; 35 | height: 100vh; 36 | background-color: var(--panel-bg); 37 | border-left: 1px solid var(--border-color); 38 | display: flex; 39 | flex-direction: column; 40 | position: relative; 41 | z-index: 1; 42 | transition: width 0.3s ease; 43 | } 44 | 45 | .resizable-chat.minimized { 46 | min-width: 50px; 47 | max-width: 50px; 48 | width: 50px !important; 49 | } 50 | 51 | /* Resize Handles */ 52 | .resize-handle { 53 | position: relative; 54 | background-color: transparent; 55 | cursor: col-resize; 56 | user-select: none; 57 | z-index: 10; 58 | transition: background-color 0.2s ease; 59 | } 60 | 61 | .resize-handle:hover { 62 | background-color: var(--accent-color); 63 | } 64 | 65 | .resize-handle:active { 66 | background-color: var(--accent-hover); 67 | } 68 | 69 | .sidebar-resize { 70 | width: 4px; 71 | height: 100vh; 72 | flex-shrink: 0; 73 | } 74 | 75 | .chat-resize { 76 | width: 4px; 77 | height: 100vh; 78 | flex-shrink: 0; 79 | } 80 | 81 | /* Chat Header */ 82 | .chat-header { 83 | display: flex; 84 | align-items: center; 85 | justify-content: space-between; 86 | padding: 0.5rem 1rem; 87 | background: var(--accent-color); 88 | border-bottom: 1px solid var(--border-color); 89 | min-height: 50px; 90 | flex-shrink: 0; 91 | } 92 | 93 | .chat-header h3 { 94 | margin: 0; 95 | color: var(--text-color); 96 | font-size: 1rem; 97 | } 98 | 99 | .chat-controls { 100 | display: flex; 101 | gap: 0.5rem; 102 | align-items: center; 103 | } 104 | 105 | .chat-control-btn { 106 | background: rgba(255, 255, 255, 0.1); 107 | border: 1px solid rgba(255, 255, 255, 0.2); 108 | color: var(--text-color); 109 | padding: 0.3rem 0.6rem; 110 | border-radius: 4px; 111 | cursor: pointer; 112 | font-size: 0.9rem; 113 | transition: all 0.2s ease; 114 | min-width: 30px; 115 | height: 30px; 116 | display: flex; 117 | align-items: center; 118 | justify-content: center; 119 | } 120 | 121 | .chat-control-btn:hover { 122 | background: rgba(255, 255, 255, 0.2); 123 | border-color: rgba(255, 255, 255, 0.3); 124 | transform: translateY(-1px); 125 | } 126 | 127 | .chat-control-btn:active { 128 | transform: translateY(0); 129 | } 130 | 131 | /* Chat Content */ 132 | .chat-content { 133 | flex: 1; 134 | overflow: hidden; 135 | display: flex; 136 | flex-direction: column; 137 | } 138 | 139 | /* Floating Chat */ 140 | .floating-chat { 141 | position: fixed; 142 | z-index: 1000; 143 | background-color: var(--panel-bg); 144 | border: 2px solid var(--accent-color); 145 | border-radius: 8px; 146 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); 147 | min-width: 300px; 148 | max-width: 500px; 149 | min-height: 400px; 150 | max-height: 600px; 151 | display: flex; 152 | flex-direction: column; 153 | overflow: hidden; 154 | resize: both; 155 | } 156 | 157 | .floating-chat-header { 158 | background: var(--accent-color); 159 | color: var(--text-color); 160 | padding: 0.75rem 1rem; 161 | cursor: move; 162 | display: flex; 163 | align-items: center; 164 | justify-content: space-between; 165 | border-bottom: 1px solid var(--border-color); 166 | user-select: none; 167 | flex-shrink: 0; 168 | } 169 | 170 | .floating-chat-header:hover { 171 | background: var(--accent-hover); 172 | } 173 | 174 | .floating-chat-controls { 175 | display: flex; 176 | gap: 0.5rem; 177 | } 178 | 179 | .floating-chat-content { 180 | flex: 1; 181 | overflow: hidden; 182 | display: flex; 183 | flex-direction: column; 184 | } 185 | 186 | /* Chat panel when minimized */ 187 | .resizable-chat.minimized .chat-header { 188 | writing-mode: vertical-lr; 189 | text-orientation: mixed; 190 | padding: 1rem 0.5rem; 191 | height: 100vh; 192 | justify-content: flex-start; 193 | flex-direction: column; 194 | gap: 1rem; 195 | } 196 | 197 | .resizable-chat.minimized .chat-controls { 198 | flex-direction: column; 199 | gap: 0.5rem; 200 | } 201 | 202 | .resizable-chat.minimized .chat-control-btn { 203 | writing-mode: horizontal-tb; 204 | text-orientation: upright; 205 | min-width: 35px; 206 | height: 35px; 207 | } 208 | 209 | /* Responsive adjustments */ 210 | @media (max-width: 768px) { 211 | .resizable-layout { 212 | flex-direction: column; 213 | } 214 | 215 | .resizable-sidebar { 216 | height: auto; 217 | max-height: 30vh; 218 | width: 100% !important; 219 | min-width: unset; 220 | max-width: unset; 221 | } 222 | 223 | .resizable-editor { 224 | height: auto; 225 | flex: 1; 226 | } 227 | 228 | .resizable-chat { 229 | height: 40vh; 230 | width: 100% !important; 231 | min-width: unset; 232 | max-width: unset; 233 | border-left: none; 234 | border-top: 1px solid var(--border-color); 235 | } 236 | 237 | .resize-handle { 238 | display: none; 239 | } 240 | 241 | .floating-chat { 242 | width: 90vw !important; 243 | height: 70vh !important; 244 | max-width: unset; 245 | max-height: unset; 246 | left: 5vw !important; 247 | top: 15vh !important; 248 | } 249 | } 250 | 251 | /* Dragging states */ 252 | .resizable-layout.dragging * { 253 | user-select: none; 254 | pointer-events: none; 255 | } 256 | 257 | .resizable-layout.dragging .resize-handle { 258 | pointer-events: all; 259 | } 260 | 261 | /* Custom scrollbar for chat areas */ 262 | .chat-content::-webkit-scrollbar, 263 | .floating-chat-content::-webkit-scrollbar { 264 | width: 6px; 265 | } 266 | 267 | .chat-content::-webkit-scrollbar-track, 268 | .floating-chat-content::-webkit-scrollbar-track { 269 | background: var(--panel-bg); 270 | } 271 | 272 | .chat-content::-webkit-scrollbar-thumb, 273 | .floating-chat-content::-webkit-scrollbar-thumb { 274 | background: var(--accent-color); 275 | border-radius: 3px; 276 | } 277 | 278 | .chat-content::-webkit-scrollbar-thumb:hover, 279 | .floating-chat-content::-webkit-scrollbar-thumb:hover { 280 | background: var(--accent-hover); 281 | } 282 | 283 | /* Animation for smooth transitions */ 284 | .resizable-chat:not(.minimized) { 285 | transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); 286 | } 287 | 288 | .chat-control-btn { 289 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); 290 | } 291 | 292 | .floating-chat { 293 | transition: box-shadow 0.2s ease; 294 | } 295 | 296 | .floating-chat:hover { 297 | box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); 298 | } 299 | 300 | /* Focus states for accessibility */ 301 | .chat-control-btn:focus { 302 | outline: 2px solid var(--accent-color); 303 | outline-offset: 2px; 304 | } 305 | 306 | .resize-handle:focus { 307 | outline: 2px solid var(--accent-color); 308 | outline-offset: -1px; 309 | } 310 | 311 | /* Resizer Styles */ 312 | .resizer { 313 | width: 8px; 314 | background-color: var(--border-color); 315 | cursor: col-resize; 316 | position: relative; 317 | flex-shrink: 0; 318 | transition: background-color 0.2s ease; 319 | } 320 | 321 | .resizer:hover { 322 | background-color: var(--accent-color); 323 | } 324 | 325 | .resizer-handle { 326 | position: absolute; 327 | top: 50%; 328 | left: 50%; 329 | transform: translate(-50%, -50%); 330 | width: 3px; 331 | height: 30px; 332 | background-color: var(--text-color); 333 | border-radius: 2px; 334 | opacity: 0; 335 | transition: opacity 0.2s ease; 336 | } 337 | 338 | .resizer:hover .resizer-handle { 339 | opacity: 0.7; 340 | } 341 | 342 | .sidebar-resizer { 343 | order: 2; 344 | } 345 | 346 | .chat-resizer { 347 | order: 4; 348 | } 349 | 350 | /* Chat Header Styles */ 351 | .chat-header { 352 | display: flex; 353 | align-items: center; 354 | padding: 0.5rem 1rem; 355 | background-color: var(--accent-color); 356 | color: var(--text-color); 357 | border-bottom: 1px solid var(--border-color); 358 | min-height: 40px; 359 | } 360 | 361 | .chat-drag-handle { 362 | cursor: move; 363 | padding: 0.25rem; 364 | margin-right: 0.5rem; 365 | font-size: 1.2rem; 366 | line-height: 1; 367 | user-select: none; 368 | color: var(--text-color); 369 | opacity: 0.7; 370 | transition: opacity 0.2s ease; 371 | } 372 | 373 | .chat-drag-handle:hover { 374 | opacity: 1; 375 | } 376 | 377 | .chat-title { 378 | flex-grow: 1; 379 | font-weight: 600; 380 | font-size: 0.9rem; 381 | } 382 | 383 | .chat-controls { 384 | display: flex; 385 | gap: 0.25rem; 386 | } 387 | 388 | .chat-control-btn { 389 | background: transparent; 390 | border: 1px solid rgba(255, 255, 255, 0.3); 391 | color: var(--text-color); 392 | padding: 0.25rem 0.5rem; 393 | border-radius: 3px; 394 | cursor: pointer; 395 | font-size: 0.8rem; 396 | transition: all 0.2s ease; 397 | min-width: 24px; 398 | height: 24px; 399 | display: flex; 400 | align-items: center; 401 | justify-content: center; 402 | } 403 | 404 | .chat-control-btn:hover { 405 | background-color: rgba(255, 255, 255, 0.1); 406 | border-color: rgba(255, 255, 255, 0.5); 407 | } 408 | 409 | .minimize-btn:hover { 410 | background-color: rgba(255, 193, 7, 0.2); 411 | border-color: #ffc107; 412 | } 413 | 414 | .detach-btn:hover { 415 | background-color: rgba(0, 123, 255, 0.2); 416 | border-color: #007bff; 417 | } 418 | 419 | .chat-content { 420 | flex: 1; 421 | display: flex; 422 | flex-direction: column; 423 | overflow: hidden; 424 | } 425 | 426 | /* Floating Chat Indicator */ 427 | .floating-chat-indicator { 428 | position: fixed; 429 | bottom: 20px; 430 | right: 20px; 431 | z-index: 1001; 432 | } 433 | 434 | .floating-chat-btn { 435 | background-color: var(--accent-color); 436 | color: var(--text-color); 437 | border: none; 438 | padding: 0.75rem 1rem; 439 | border-radius: 25px; 440 | cursor: pointer; 441 | font-size: 0.9rem; 442 | box-shadow: 0 4px 12px rgba(0,0,0,0.3); 443 | transition: all 0.3s ease; 444 | animation: pulse 2s infinite; 445 | } 446 | 447 | .floating-chat-btn:hover { 448 | background-color: var(--accent-hover); 449 | transform: translateY(-2px); 450 | box-shadow: 0 6px 16px rgba(0,0,0,0.4); 451 | } 452 | 453 | @keyframes pulse { 454 | 0% { 455 | box-shadow: 0 4px 12px rgba(0,0,0,0.3), 0 0 0 0 rgba(103, 37, 217, 0.7); 456 | } 457 | 70% { 458 | box-shadow: 0 4px 12px rgba(0,0,0,0.3), 0 0 0 10px rgba(103, 37, 217, 0); 459 | } 460 | 100% { 461 | box-shadow: 0 4px 12px rgba(0,0,0,0.3), 0 0 0 0 rgba(103, 37, 217, 0); 462 | } 463 | } 464 | 465 | /* Dragging States */ 466 | .resizable-layout.dragging-sidebar { 467 | cursor: col-resize; 468 | } 469 | 470 | .resizable-layout.dragging-chat { 471 | cursor: col-resize; 472 | } 473 | 474 | .resizable-layout.dragging-sidebar * { 475 | user-select: none; 476 | pointer-events: none; 477 | } 478 | 479 | .resizable-layout.dragging-chat * { 480 | user-select: none; 481 | pointer-events: none; 482 | } 483 | 484 | /* Responsive Design */ 485 | @media (max-width: 768px) { 486 | .resizable-sidebar { 487 | min-width: 150px; 488 | max-width: 250px; 489 | } 490 | 491 | .resizable-chat { 492 | min-width: 200px; 493 | max-width: 300px; 494 | } 495 | 496 | .resizer { 497 | width: 12px; 498 | } 499 | 500 | .chat-header { 501 | padding: 0.4rem 0.8rem; 502 | } 503 | 504 | .chat-drag-handle { 505 | font-size: 1rem; 506 | } 507 | 508 | .floating-chat-btn { 509 | bottom: 15px; 510 | right: 15px; 511 | padding: 0.6rem 0.8rem; 512 | font-size: 0.8rem; 513 | } 514 | } 515 | 516 | @media (max-width: 480px) { 517 | .resizable-layout { 518 | flex-direction: column; 519 | } 520 | 521 | .resizable-sidebar { 522 | width: 100% !important; 523 | max-height: 30vh; 524 | min-width: auto; 525 | max-width: none; 526 | } 527 | 528 | .resizable-editor { 529 | width: 100% !important; 530 | min-width: auto; 531 | } 532 | 533 | .resizable-chat { 534 | width: 100% !important; 535 | min-width: auto; 536 | max-width: none; 537 | border-left: none; 538 | border-top: 1px solid var(--border-color); 539 | } 540 | 541 | .resizer { 542 | display: none; 543 | } 544 | } 545 | 546 | /* Dark/Light Mode Compatibility */ 547 | .light-mode .resizer { 548 | background-color: #ddd; 549 | } 550 | 551 | .light-mode .resizer:hover { 552 | background-color: var(--accent-color); 553 | } 554 | 555 | .light-mode .resizer-handle { 556 | background-color: #666; 557 | } 558 | 559 | .light-mode .chat-control-btn { 560 | border-color: rgba(0, 0, 0, 0.2); 561 | color: var(--text-color); 562 | } 563 | 564 | .light-mode .chat-control-btn:hover { 565 | background-color: rgba(0, 0, 0, 0.1); 566 | border-color: rgba(0, 0, 0, 0.3); 567 | } 568 | 569 | .detached-chat-input { 570 | direction: ltr; 571 | unicode-bidi: normal; 572 | text-align: left; 573 | } 574 | -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ui/BackToTop.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useState, useEffect } from 'react'; 3 | import "../../styles/BackToTop.css" 4 | 5 | const BackToTop = () => { 6 | const [isVisible, setIsVisible] = useState(false); 7 | 8 | const handleScroll = () => { 9 | if (window.scrollY > 100) { 10 | setIsVisible(true); 11 | } else { 12 | setIsVisible(false); 13 | } 14 | }; 15 | 16 | useEffect(() => { 17 | window.addEventListener('scroll', handleScroll); 18 | return () => { 19 | window.removeEventListener('scroll', handleScroll); 20 | }; 21 | }, []); 22 | 23 | const backToTop = () => { 24 | window.scrollTo({ 25 | top: 0, 26 | behavior: 'smooth', 27 | }); 28 | }; 29 | 30 | 31 | return ( 32 |
36 | 37 |
38 | ) 39 | } 40 | 41 | export default BackToTop -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ui/aspect-ratio.jsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 2 | 3 | const AspectRatio = AspectRatioPrimitive.Root 4 | 5 | export { AspectRatio } -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ui/badge.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | function Badge({ className, variant, ...props }) { 27 | return ( 28 |
29 | ) 30 | } 31 | 32 | export { Badge, badgeVariants } -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ui/button.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | const Button = React.forwardRef( 37 | ({ className, variant, size, asChild = false, ...props }, ref) => { 38 | const Comp = asChild ? Slot : "button" 39 | return ( 40 | 45 | ) 46 | } 47 | ) 48 | Button.displayName = "Button" 49 | 50 | export { Button, buttonVariants } -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ui/card.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "..//../lib/utils" 4 | 5 | const Card = React.forwardRef(({ className, ...props }, ref) => ( 6 |
14 | )) 15 | Card.displayName = "Card" 16 | 17 | const CardHeader = React.forwardRef(({ className, ...props }, ref) => ( 18 |
23 | )) 24 | CardHeader.displayName = "CardHeader" 25 | 26 | const CardTitle = React.forwardRef(({ className, ...props }, ref) => ( 27 |
35 | )) 36 | CardTitle.displayName = "CardTitle" 37 | 38 | const CardDescription = React.forwardRef(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | CardDescription.displayName = "CardDescription" 46 | 47 | const CardContent = React.forwardRef(({ className, ...props }, ref) => ( 48 |
49 | )) 50 | CardContent.displayName = "CardContent" 51 | 52 | const CardFooter = React.forwardRef(({ className, ...props }, ref) => ( 53 |
58 | )) 59 | CardFooter.displayName = "CardFooter" 60 | 61 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ui/input.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ui/label.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef(({ className, ...props }, ref) => ( 12 | 17 | )) 18 | Label.displayName = LabelPrimitive.Root.displayName 19 | 20 | export { Label } -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ui/progress.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ProgressPrimitive from "@radix-ui/react-progress" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Progress = React.forwardRef(({ className, value, ...props }, ref) => ( 9 | 17 | 21 | 22 | )) 23 | Progress.displayName = ProgressPrimitive.Root.displayName 24 | 25 | export { Progress } -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ui/separator.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Separator = React.forwardRef( 7 | ( 8 | { className, orientation = "horizontal", decorative = true, ...props }, 9 | ref 10 | ) => ( 11 | 22 | ) 23 | ) 24 | Separator.displayName = SeparatorPrimitive.Root.displayName 25 | 26 | export { Separator } -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ui/skeleton.jsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }) { 4 | return ( 5 |
9 | ) 10 | } 11 | 12 | export { Skeleton } -------------------------------------------------------------------------------- /frontend/vite-project/src/components/ui/textarea.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Textarea = React.forwardRef(({ className, ...props }, ref) => { 6 | return ( 7 |