├── .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 | 
39 |
40 | ### 🚀 Features Page
41 | 
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 |
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 |
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 |
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 |
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 |
15 | )
16 | })
17 | Textarea.displayName = "Textarea"
18 |
19 | export { Textarea }
--------------------------------------------------------------------------------
/frontend/vite-project/src/components/ui/toast.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "../../lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
11 |
19 | ))
20 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
21 |
22 | const toastVariants = cva(
23 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
24 | {
25 | variants: {
26 | variant: {
27 | default: "border bg-background text-foreground",
28 | destructive:
29 | "destructive group border-destructive bg-destructive text-destructive-foreground",
30 | },
31 | },
32 | defaultVariants: {
33 | variant: "default",
34 | },
35 | }
36 | )
37 |
38 | const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
39 | return (
40 |
45 | )
46 | })
47 | Toast.displayName = ToastPrimitives.Root.displayName
48 |
49 | const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
50 |
58 | ))
59 | ToastAction.displayName = ToastPrimitives.Action.displayName
60 |
61 | const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
62 |
71 |
72 |
73 | ))
74 | ToastClose.displayName = ToastPrimitives.Close.displayName
75 |
76 | const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
77 |
82 | ))
83 | ToastTitle.displayName = ToastPrimitives.Title.displayName
84 |
85 | const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
86 |
91 | ))
92 | ToastDescription.displayName = ToastPrimitives.Description.displayName
93 |
94 | export {
95 | ToastProvider,
96 | ToastViewport,
97 | Toast,
98 | ToastTitle,
99 | ToastDescription,
100 | ToastClose,
101 | ToastAction,
102 | }
--------------------------------------------------------------------------------
/frontend/vite-project/src/components/ui/toaster.jsx:
--------------------------------------------------------------------------------
1 | import { useToast } from "../../hooks/use-toast"
2 | import {
3 | Toast,
4 | ToastClose,
5 | ToastDescription,
6 | ToastProvider,
7 | ToastTitle,
8 | ToastViewport,
9 | } from "../ui/toast"
10 |
11 | export function Toaster() {
12 | const { toasts } = useToast()
13 |
14 | return (
15 |
16 | {toasts.map(function ({ id, title, description, action, ...props }) {
17 | return (
18 |
19 |
20 | {title && {title}}
21 | {description && (
22 | {description}
23 | )}
24 |
25 | {action}
26 |
27 |
28 | )
29 | })}
30 |
31 |
32 | )
33 | }
--------------------------------------------------------------------------------
/frontend/vite-project/src/components/ui/tooltip.jsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "../../lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef(
15 | ({ className, sideOffset = 4, ...props }, ref) => (
16 |
25 | )
26 | )
27 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
28 |
29 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
--------------------------------------------------------------------------------
/frontend/vite-project/src/hooks/use-mobile.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
--------------------------------------------------------------------------------
/frontend/vite-project/src/hooks/use-toast.js:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const TOAST_LIMIT = 1
4 | const TOAST_REMOVE_DELAY = 1000000
5 |
6 | const actionTypes = {
7 | ADD_TOAST: "ADD_TOAST",
8 | UPDATE_TOAST: "UPDATE_TOAST",
9 | DISMISS_TOAST: "DISMISS_TOAST",
10 | REMOVE_TOAST: "REMOVE_TOAST",
11 | }
12 |
13 | let count = 0
14 |
15 | function genId() {
16 | count = (count + 1) % Number.MAX_SAFE_INTEGER
17 | return count.toString()
18 | }
19 |
20 | const toastTimeouts = new Map()
21 |
22 | const addToRemoveQueue = (toastId) => {
23 | if (toastTimeouts.has(toastId)) {
24 | return
25 | }
26 |
27 | const timeout = setTimeout(() => {
28 | toastTimeouts.delete(toastId)
29 | dispatch({
30 | type: "REMOVE_TOAST",
31 | toastId: toastId,
32 | })
33 | }, TOAST_REMOVE_DELAY)
34 |
35 | toastTimeouts.set(toastId, timeout)
36 | }
37 |
38 | export const reducer = (state, action) => {
39 | switch (action.type) {
40 | case "ADD_TOAST":
41 | return {
42 | ...state,
43 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
44 | }
45 |
46 | case "UPDATE_TOAST":
47 | return {
48 | ...state,
49 | toasts: state.toasts.map((t) =>
50 | t.id === action.toast.id ? { ...t, ...action.toast } : t
51 | ),
52 | }
53 |
54 | case "DISMISS_TOAST": {
55 | const { toastId } = action
56 |
57 | // ! Side effects ! - This could be extracted into a dismissToast() action,
58 | // but I'll keep it here for simplicity
59 | if (toastId) {
60 | addToRemoveQueue(toastId)
61 | } else {
62 | state.toasts.forEach((toast) => {
63 | addToRemoveQueue(toast.id)
64 | })
65 | }
66 |
67 | return {
68 | ...state,
69 | toasts: state.toasts.map((t) =>
70 | t.id === toastId || toastId === undefined
71 | ? {
72 | ...t,
73 | open: false,
74 | }
75 | : t
76 | ),
77 | }
78 | }
79 | case "REMOVE_TOAST":
80 | if (action.toastId === undefined) {
81 | return {
82 | ...state,
83 | toasts: [],
84 | }
85 | }
86 | return {
87 | ...state,
88 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
89 | }
90 | }
91 | }
92 |
93 | const listeners = []
94 |
95 | let memoryState = { toasts: [] }
96 |
97 | function dispatch(action) {
98 | memoryState = reducer(memoryState, action)
99 | listeners.forEach((listener) => {
100 | listener(memoryState)
101 | })
102 | }
103 |
104 | function toast({ ...props }) {
105 | const id = genId()
106 |
107 | const update = (props) =>
108 | dispatch({
109 | type: "UPDATE_TOAST",
110 | toast: { ...props, id },
111 | })
112 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
113 |
114 | dispatch({
115 | type: "ADD_TOAST",
116 | toast: {
117 | ...props,
118 | id,
119 | open: true,
120 | onOpenChange: (open) => {
121 | if (!open) dismiss()
122 | },
123 | },
124 | })
125 |
126 | return {
127 | id: id,
128 | dismiss,
129 | update,
130 | }
131 | }
132 |
133 | function useToast() {
134 | const [state, setState] = React.useState(memoryState)
135 |
136 | React.useEffect(() => {
137 | listeners.push(setState)
138 | return () => {
139 | const index = listeners.indexOf(setState)
140 | if (index > -1) {
141 | listeners.splice(index, 1)
142 | }
143 | }
144 | }, [state])
145 |
146 | return {
147 | ...state,
148 | toast,
149 | dismiss: (toastId) => dispatch({ type: "DISMISS_TOAST", toastId }),
150 | }
151 | }
152 |
153 | export { useToast, toast }
--------------------------------------------------------------------------------
/frontend/vite-project/src/lib/queryClient.js:
--------------------------------------------------------------------------------
1 | import { QueryClient } from "@tanstack/react-query";
2 |
3 | async function throwIfResNotOk(res) {
4 | if (!res.ok) {
5 | const text = (await res.text()) || res.statusText;
6 | throw new Error(`${res.status}: ${text}`);
7 | }
8 | }
9 |
10 | export async function apiRequest(method, url, data) {
11 | const res = await fetch(url, {
12 | method,
13 | headers: data ? { "Content-Type": "application/json" } : {},
14 | body: data ? JSON.stringify(data) : undefined,
15 | credentials: "include",
16 | });
17 |
18 | await throwIfResNotOk(res);
19 | return res;
20 | }
21 |
22 | export const getQueryFn = ({ on401: unauthorizedBehavior }) =>
23 | async ({ queryKey }) => {
24 | const res = await fetch(queryKey.join("/"), {
25 | credentials: "include",
26 | });
27 |
28 | if (unauthorizedBehavior === "returnNull" && res.status === 401) {
29 | return null;
30 | }
31 |
32 | await throwIfResNotOk(res);
33 | return await res.json();
34 | };
35 |
36 | export const queryClient = new QueryClient({
37 | defaultOptions: {
38 | queries: {
39 | queryFn: getQueryFn({ on401: "throw" }),
40 | refetchInterval: false,
41 | refetchOnWindowFocus: false,
42 | staleTime: Infinity,
43 | retry: false,
44 | },
45 | mutations: {
46 | retry: false,
47 | },
48 | },
49 | });
--------------------------------------------------------------------------------
/frontend/vite-project/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs) {
5 | return twMerge(clsx(inputs));
6 | }
--------------------------------------------------------------------------------
/frontend/vite-project/src/main.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 | @reference "tailwindcss";
6 |
7 | :root {
8 | --background: hsl(0, 0%, 100%);
9 | --foreground: hsl(210, 25%, 7.8431%);
10 | --card: hsl(180, 6.6667%, 97.0588%);
11 | --card-foreground: hsl(210, 25%, 7.8431%);
12 | --popover: hsl(0, 0%, 100%);
13 | --popover-foreground: hsl(210, 25%, 7.8431%);
14 | --primary: hsl(203.8863, 88.2845%, 53.1373%);
15 | --primary-foreground: hsl(0, 0%, 100%);
16 | --secondary: hsl(210, 25%, 7.8431%);
17 | --secondary-foreground: hsl(0, 0%, 100%);
18 | --muted: hsl(240, 1.9608%, 90%);
19 | --muted-foreground: hsl(210, 25%, 7.8431%);
20 | --accent: hsl(211.5789, 51.3514%, 92.7451%);
21 | --accent-foreground: hsl(203.8863, 88.2845%, 53.1373%);
22 | --destructive: hsl(356.3033, 90.5579%, 54.3137%);
23 | --destructive-foreground: hsl(0, 0%, 100%);
24 | --border: hsl(201.4286, 30.4348%, 90.9804%);
25 | --input: hsl(200, 23.0769%, 97.4510%);
26 | --ring: hsl(202.8169, 89.1213%, 53.1373%);
27 | --chart-1: hsl(203.8863, 88.2845%, 53.1373%);
28 | --chart-2: hsl(159.7826, 100%, 36.0784%);
29 | --chart-3: hsl(42.0290, 92.8251%, 56.2745%);
30 | --chart-4: hsl(147.1429, 78.5047%, 41.9608%);
31 | --chart-5: hsl(341.4894, 75.2000%, 50.9804%);
32 | --sidebar: hsl(180, 6.6667%, 97.0588%);
33 | --sidebar-foreground: hsl(210, 25%, 7.8431%);
34 | --sidebar-primary: hsl(203.8863, 88.2845%, 53.1373%);
35 | --sidebar-primary-foreground: hsl(0, 0%, 100%);
36 | --sidebar-accent: hsl(211.5789, 51.3514%, 92.7451%);
37 | --sidebar-accent-foreground: hsl(203.8863, 88.2845%, 53.1373%);
38 | --sidebar-border: hsl(205.0000, 25.0000%, 90.5882%);
39 | --sidebar-ring: hsl(202.8169, 89.1213%, 53.1373%);
40 | --font-sans: 'Inter', sans-serif;
41 | --font-serif: Georgia, serif;
42 | --font-mono: 'Courier New', monospace;
43 | --radius: 1.3rem;
44 | --shadow-2xs: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
45 | --shadow-xs: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
46 | --shadow-sm: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00), 0px 1px 2px -1px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
47 | --shadow: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00), 0px 1px 2px -1px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
48 | --shadow-md: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00), 0px 2px 4px -1px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
49 | --shadow-lg: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00), 0px 4px 6px -1px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
50 | --shadow-xl: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00), 0px 8px 10px -1px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
51 | --shadow-2xl: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
52 | --tracking-normal: 0em;
53 | --spacing: 0.25rem;
54 | }
55 |
56 | .dark {
57 | --background: hsl(0, 0%, 0%);
58 | --foreground: hsl(200, 6.6667%, 91.1765%);
59 | --card: hsl(228, 9.8039%, 10%);
60 | --card-foreground: hsl(0, 0%, 85.0980%);
61 | --popover: hsl(0, 0%, 0%);
62 | --popover-foreground: hsl(200, 6.6667%, 91.1765%);
63 | --primary: hsl(203.7736, 87.6033%, 52.5490%);
64 | --primary-foreground: hsl(0, 0%, 100%);
65 | --secondary: hsl(195.0000, 15.3846%, 94.9020%);
66 | --secondary-foreground: hsl(210, 25%, 7.8431%);
67 | --muted: hsl(0, 0%, 9.4118%);
68 | --muted-foreground: hsl(210, 3.3898%, 46.2745%);
69 | --accent: hsl(205.7143, 70%, 7.8431%);
70 | --accent-foreground: hsl(203.7736, 87.6033%, 52.5490%);
71 | --destructive: hsl(356.3033, 90.5579%, 54.3137%);
72 | --destructive-foreground: hsl(0, 0%, 100%);
73 | --border: hsl(210, 5.2632%, 14.9020%);
74 | --input: hsl(207.6923, 27.6596%, 18.4314%);
75 | --ring: hsl(202.8169, 89.1213%, 53.1373%);
76 | --chart-1: hsl(203.8863, 88.2845%, 53.1373%);
77 | --chart-2: hsl(159.7826, 100%, 36.0784%);
78 | --chart-3: hsl(42.0290, 92.8251%, 56.2745%);
79 | --chart-4: hsl(147.1429, 78.5047%, 41.9608%);
80 | --chart-5: hsl(341.4894, 75.2000%, 50.9804%);
81 | --sidebar: hsl(228, 9.8039%, 10%);
82 | --sidebar-foreground: hsl(0, 0%, 85.0980%);
83 | --sidebar-primary: hsl(202.8169, 89.1213%, 53.1373%);
84 | --sidebar-primary-foreground: hsl(0, 0%, 100%);
85 | --sidebar-accent: hsl(205.7143, 70%, 7.8431%);
86 | --sidebar-accent-foreground: hsl(203.7736, 87.6033%, 52.5490%);
87 | --sidebar-border: hsl(205.7143, 15.7895%, 26.0784%);
88 | --sidebar-ring: hsl(202.8169, 89.1213%, 53.1373%);
89 | --font-sans: 'Inter', sans-serif;
90 | --font-serif: Georgia, serif;
91 | --font-mono: 'Courier New', monospace;
92 | --radius: 1.3rem;
93 | --shadow-2xs: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
94 | --shadow-xs: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
95 | --shadow-sm: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00), 0px 1px 2px -1px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
96 | --shadow: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00), 0px 1px 2px -1px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
97 | --shadow-md: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00), 0px 2px 4px -1px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
98 | --shadow-lg: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00), 0px 4px 6px -1px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
99 | --shadow-xl: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00), 0px 8px 10px -1px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
100 | --shadow-2xl: 0px 2px 0px 0px hsl(202.8169, 89.1213%, 53.1373% / 0.00);
101 | }
102 |
103 | @layer utilities {
104 | .bg-background {
105 | background-color: var(--background);
106 | }
107 |
108 | .text-foreground {
109 | color: var(--foreground);
110 | }
111 |
112 | .border-border {
113 | border-color: var(--border);
114 | }
115 | }
116 |
117 | @layer base {
118 | * {
119 | border-color: var(--border);
120 | }
121 |
122 | body {
123 | @apply font-sans antialiased bg-[var(--background)] text-[var(--foreground)];
124 | }
125 | }
126 |
127 | @layer components {
128 |
129 | /* Glass morphism effects */
130 | .glass-card {
131 | background: rgba(255, 255, 255, 0.1);
132 | backdrop-filter: blur(10px);
133 | border: 1px solid rgba(255, 255, 255, 0.2);
134 | }
135 |
136 | .glass-card-white {
137 | background: rgba(255, 255, 255, 0.95);
138 | backdrop-filter: blur(20px);
139 | border: 1px solid rgba(255, 255, 255, 0.3);
140 | }
141 |
142 | .feature-card {
143 | background: rgba(255, 255, 255, 0.1);
144 | backdrop-filter: blur(10px);
145 | border: 1px solid rgba(255, 255, 255, 0.2);
146 | transition: all 0.3s ease;
147 | }
148 |
149 | .feature-card:hover {
150 | background: rgba(255, 255, 255, 0.15);
151 | transform: translateY(-2px);
152 | }
153 |
154 | /* Background pattern */
155 | .bg-pattern {
156 | background-image:
157 | radial-gradient(circle at 25% 25%, white 2px, transparent 0),
158 | radial-gradient(circle at 75% 75%, white 2px, transparent 0);
159 | background-size: 50px 50px;
160 | }
161 |
162 | /* Spinner animation */
163 | @keyframes spin {
164 | 0% {
165 | transform: rotate(0deg);
166 | }
167 |
168 | 100% {
169 | transform: rotate(360deg);
170 | }
171 | }
172 |
173 | .spinner {
174 | animation: spin 1s linear infinite;
175 | }
176 | }
--------------------------------------------------------------------------------
/frontend/vite-project/src/main.jsx:
--------------------------------------------------------------------------------
1 | // Polyfill for simple-peer dependencies
2 | window.global = window;
3 |
4 | import { StrictMode } from "react";
5 | import { createRoot } from "react-dom/client";
6 | import './main.css';
7 | import App from "./App.jsx";
8 |
9 | createRoot(document.getElementById("root")).render(
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/frontend/vite-project/src/pages/Landing_page.css:
--------------------------------------------------------------------------------
1 | .landing-container {
2 | min-height: 100vh;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | position: relative;
7 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
8 | padding: 2rem;
9 | overflow: hidden;
10 | }
11 |
12 | .background-pattern {
13 | position: absolute;
14 | top: 0;
15 | left: 0;
16 | right: 0;
17 | bottom: 0;
18 | background-image:
19 | radial-gradient(circle at 25% 25%, rgba(255, 255, 255, 0.1) 2px, transparent 0),
20 | radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.1) 2px, transparent 0);
21 | background-size: 50px 50px;
22 | animation: float 20s ease-in-out infinite;
23 | }
24 |
25 | @keyframes float {
26 |
27 | 0%,
28 | 100% {
29 | transform: translateY(0px) rotate(0deg);
30 | }
31 |
32 | 50% {
33 | transform: translateY(-20px) rotate(180deg);
34 | }
35 | }
36 |
37 | .landing-content {
38 | max-width: 500px;
39 | width: 100%;
40 | z-index: 1;
41 | position: relative;
42 | }
43 |
44 | .landing-header {
45 | text-align: center;
46 | margin-bottom: 2rem;
47 | }
48 |
49 | .logo-container {
50 | display: flex;
51 | flex-direction: column;
52 | align-items: center;
53 | margin-bottom: 1.5rem;
54 | }
55 |
56 | .logo-icon {
57 | width: 64px;
58 | height: 64px;
59 | background: rgba(255, 255, 255, 0.2);
60 | border-radius: 16px;
61 | display: flex;
62 | align-items: center;
63 | justify-content: center;
64 | color: white;
65 | margin-bottom: 1rem;
66 | backdrop-filter: blur(10px);
67 | border: 1px solid rgba(255, 255, 255, 0.3);
68 | }
69 |
70 | .app-title {
71 | font-size: 2.5rem;
72 | font-weight: 700;
73 | color: white;
74 | margin: 0;
75 | text-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
76 | letter-spacing: -0.02em;
77 | }
78 |
79 | .datetime-display {
80 | background: rgba(255, 255, 255, 0.15);
81 | backdrop-filter: blur(10px);
82 | border-radius: 12px;
83 | padding: 0.75rem 1.5rem;
84 | border: 1px solid rgba(255, 255, 255, 0.2);
85 | display: inline-block;
86 | }
87 |
88 | .date {
89 | color: rgba(255, 255, 255, 0.9);
90 | font-size: 0.9rem;
91 | font-weight: 500;
92 | }
93 |
94 | .time {
95 | color: white;
96 | font-size: 1.1rem;
97 | font-weight: 600;
98 | font-family: 'Courier New', monospace;
99 | }
100 |
101 | .landing-card {
102 | background: rgba(255, 255, 255, 0.95);
103 | backdrop-filter: blur(20px);
104 | border-radius: 20px;
105 | padding: 2.5rem;
106 | box-shadow:
107 | 0 20px 40px rgba(0, 0, 0, 0.1),
108 | 0 0 0 1px rgba(255, 255, 255, 0.2);
109 | border: 1px solid rgba(255, 255, 255, 0.3);
110 | }
111 |
112 | .card-title {
113 | font-size: 1.75rem;
114 | font-weight: 700;
115 | color: #2d3748;
116 | margin: 0 0 0.5rem 0;
117 | text-align: center;
118 | }
119 |
120 | .card-subtitle {
121 | color: #718096;
122 | text-align: center;
123 | margin: 0 0 2rem 0;
124 | font-size: 1rem;
125 | }
126 |
127 | .form-container {
128 | margin-bottom: 2rem;
129 | }
130 |
131 | .input-group {
132 | margin-bottom: 1.5rem;
133 | }
134 |
135 | .input-group label {
136 | display: block;
137 | font-weight: 600;
138 | color: #4a5568;
139 | margin-bottom: 0.5rem;
140 | font-size: 0.9rem;
141 | }
142 |
143 | .input-group input {
144 | width: 100%;
145 | padding: 0.875rem 1rem;
146 | border: 2px solid #e2e8f0;
147 | border-radius: 12px;
148 | font-size: 1rem;
149 | transition: all 0.2s ease;
150 | background: white;
151 | box-sizing: border-box;
152 | }
153 |
154 | .input-group input:focus {
155 | outline: none;
156 | border-color: #667eea;
157 | box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
158 | }
159 |
160 | .input-group input::placeholder {
161 | color: #a0aec0;
162 | }
163 |
164 | .button-group {
165 | display: grid;
166 | grid-template-columns: 1fr 1fr;
167 | gap: 1rem;
168 | margin-top: 1.5rem;
169 | }
170 |
171 | .btn {
172 | padding: 0.875rem 1.5rem;
173 | border: none;
174 | border-radius: 12px;
175 | font-size: 1rem;
176 | font-weight: 600;
177 | cursor: pointer;
178 | transition: all 0.2s ease;
179 | display: flex;
180 | align-items: center;
181 | justify-content: center;
182 | gap: 0.5rem;
183 | position: relative;
184 | overflow: hidden;
185 | }
186 |
187 | .btn:disabled {
188 | opacity: 0.6;
189 | cursor: not-allowed;
190 | }
191 |
192 | .create-btn {
193 | background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
194 | color: white;
195 | box-shadow: 0 4px 15px rgba(79, 172, 254, 0.4);
196 | }
197 |
198 | .create-btn:hover:not(:disabled) {
199 | transform: translateY(-2px);
200 | box-shadow: 0 8px 25px rgba(79, 172, 254, 0.6);
201 | }
202 |
203 | .join-btn {
204 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
205 | color: white;
206 | box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
207 | }
208 |
209 | .join-btn:hover:not(:disabled) {
210 | transform: translateY(-2px);
211 | box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6);
212 | }
213 |
214 | .btn-content {
215 | display: flex;
216 | align-items: center;
217 | gap: 0.5rem;
218 | }
219 |
220 | .btn-loading {
221 | display: flex;
222 | align-items: center;
223 | gap: 0.75rem;
224 | }
225 |
226 | .spinner {
227 | width: 16px;
228 | height: 16px;
229 | border: 2px solid rgba(255, 255, 255, 0.3);
230 | border-top: 2px solid white;
231 | border-radius: 50%;
232 | animation: spin 1s linear infinite;
233 | }
234 |
235 | @keyframes spin {
236 | 0% {
237 | transform: rotate(0deg);
238 | }
239 |
240 | 100% {
241 | transform: rotate(360deg);
242 | }
243 | }
244 |
245 | .features-preview {
246 | display: grid;
247 | grid-template-columns: repeat(3, 1fr);
248 | gap: 1rem;
249 | margin-top: 2rem;
250 | padding-top: 2rem;
251 | border-top: 1px solid #e2e8f0;
252 | }
253 |
254 | .feature-item {
255 | display: flex;
256 | flex-direction: column;
257 | align-items: center;
258 | text-align: center;
259 | gap: 0.5rem;
260 | }
261 |
262 | .feature-icon {
263 | font-size: 1.5rem;
264 | }
265 |
266 | .feature-item span {
267 | font-size: 0.8rem;
268 | color: #718096;
269 | font-weight: 500;
270 | }
271 |
272 | .landing-footer {
273 | text-align: center;
274 | margin-top: 2rem;
275 | }
276 |
277 | .landing-footer p {
278 | color: rgba(255, 255, 255, 0.8);
279 | font-size: 0.9rem;
280 | margin: 0;
281 | }
282 |
283 | /* Responsive Design */
284 | @media (max-width: 768px) {
285 | .landing-container {
286 | padding: 1rem;
287 | }
288 |
289 | .app-title {
290 | font-size: 2rem;
291 | }
292 |
293 | .landing-card {
294 | padding: 2rem 1.5rem;
295 | }
296 |
297 | .button-group {
298 | grid-template-columns: 1fr;
299 | }
300 |
301 | .features-preview {
302 | grid-template-columns: 1fr;
303 | gap: 0.75rem;
304 | }
305 |
306 | .feature-item {
307 | flex-direction: row;
308 | justify-content: center;
309 | }
310 | }
311 |
312 | @media (max-width: 480px) {
313 | .app-title {
314 | font-size: 1.75rem;
315 | }
316 |
317 | .landing-card {
318 | padding: 1.5rem 1rem;
319 | }
320 | }
--------------------------------------------------------------------------------
/frontend/vite-project/src/pages/not-found.jsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent } from "../components/ui/card";
2 | import { AlertCircle } from "lucide-react";
3 |
4 | export default function NotFound() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
404 Page Not Found
12 |
13 |
14 |
15 | Please check after some time or contact support if the issue persists.
16 |
17 |
18 |
19 |
20 | );
21 | }
--------------------------------------------------------------------------------
/frontend/vite-project/src/styles/BackToTop.css:
--------------------------------------------------------------------------------
1 | .scroll-to-top-button {
2 | display: none;
3 | position: fixed;
4 | bottom: 20px;
5 | right: 20px;
6 | background-color: #007bff;
7 | color: #fff;
8 | border: none;
9 | border-radius: 50%;
10 | width: 75px;
11 | height: 75px;
12 | font-size: 36px;
13 | cursor: pointer;
14 | z-index: 99;
15 | transition: opacity 0.3s, visibility 0.3s;
16 | }
17 |
18 | .scroll-to-top-button.visible {
19 | display: block;
20 | }
21 |
22 |
23 | .scroll-to-top-button i {
24 | line-height: 50px;
25 | display: flex;
26 | align-items: center;
27 | justify-content: center;
28 | z-index: 99;
29 |
30 | }
31 |
32 | .scroll-to-top-button:hover {
33 | background-color: #0056b3;
34 | }
35 |
36 |
37 | .ri-arrow-up-s-line{
38 | height: 75px;
39 | }
40 |
--------------------------------------------------------------------------------
/frontend/vite-project/src/utils/fileTypeDetection.js:
--------------------------------------------------------------------------------
1 | // File Type Detection Utility for Syntax Highlighting
2 | // Automatically detects file type from extension and maps to Monaco Editor languages
3 |
4 | /**
5 | * Maps file extensions to Monaco Editor language identifiers
6 | */
7 | const FILE_TYPE_MAP = {
8 | // JavaScript & TypeScript
9 | '.js': 'javascript',
10 | '.jsx': 'javascript',
11 | '.ts': 'typescript',
12 | '.tsx': 'typescript',
13 | '.mjs': 'javascript',
14 | '.cjs': 'javascript',
15 |
16 | // Python
17 | '.py': 'python',
18 | '.pyw': 'python',
19 | '.pyi': 'python',
20 |
21 | // Java
22 | '.java': 'java',
23 | '.class': 'java',
24 |
25 | // C/C++
26 | '.c': 'c',
27 | '.cpp': 'cpp',
28 | '.cxx': 'cpp',
29 | '.cc': 'cpp',
30 | '.h': 'c',
31 | '.hpp': 'cpp',
32 | '.hxx': 'cpp',
33 |
34 | // C#
35 | '.cs': 'csharp',
36 |
37 | // Web Technologies
38 | '.html': 'html',
39 | '.htm': 'html',
40 | '.css': 'css',
41 | '.scss': 'scss',
42 | '.sass': 'sass',
43 | '.less': 'less',
44 |
45 | // PHP
46 | '.php': 'php',
47 | '.phtml': 'php',
48 |
49 | // Ruby
50 | '.rb': 'ruby',
51 | '.rbw': 'ruby',
52 |
53 | // Go
54 | '.go': 'go',
55 |
56 | // Rust
57 | '.rs': 'rust',
58 |
59 | // Shell Scripts
60 | '.sh': 'shell',
61 | '.bash': 'shell',
62 | '.zsh': 'shell',
63 |
64 | // Markup & Data
65 | '.xml': 'xml',
66 | '.json': 'json',
67 | '.yaml': 'yaml',
68 | '.yml': 'yaml',
69 | '.toml': 'toml',
70 | '.ini': 'ini',
71 | '.cfg': 'ini',
72 |
73 | // Markdown
74 | '.md': 'markdown',
75 | '.markdown': 'markdown',
76 |
77 | // SQL
78 | '.sql': 'sql',
79 |
80 | // R
81 | '.r': 'r',
82 | '.R': 'r',
83 |
84 | // Swift
85 | '.swift': 'swift',
86 |
87 | // Kotlin
88 | '.kt': 'kotlin',
89 | '.kts': 'kotlin',
90 |
91 | // Scala
92 | '.scala': 'scala',
93 | '.sc': 'scala',
94 |
95 | // Lua
96 | '.lua': 'lua',
97 |
98 | // Perl
99 | '.pl': 'perl',
100 | '.pm': 'perl',
101 |
102 | // PowerShell
103 | '.ps1': 'powershell',
104 | '.psm1': 'powershell',
105 |
106 | // Batch
107 | '.bat': 'bat',
108 | '.cmd': 'bat',
109 |
110 | // Docker
111 | 'dockerfile': 'dockerfile',
112 | '.dockerfile': 'dockerfile',
113 |
114 | // Plain text
115 | '.txt': 'plaintext',
116 | '.text': 'plaintext',
117 | };
118 |
119 | /**
120 | * Language display names for UI
121 | */
122 | const LANGUAGE_DISPLAY_NAMES = {
123 | 'javascript': 'JavaScript',
124 | 'typescript': 'TypeScript',
125 | 'python': 'Python',
126 | 'java': 'Java',
127 | 'c': 'C',
128 | 'cpp': 'C++',
129 | 'csharp': 'C#',
130 | 'html': 'HTML',
131 | 'css': 'CSS',
132 | 'scss': 'SCSS',
133 | 'sass': 'Sass',
134 | 'less': 'Less',
135 | 'php': 'PHP',
136 | 'ruby': 'Ruby',
137 | 'go': 'Go',
138 | 'rust': 'Rust',
139 | 'shell': 'Shell',
140 | 'xml': 'XML',
141 | 'json': 'JSON',
142 | 'yaml': 'YAML',
143 | 'toml': 'TOML',
144 | 'ini': 'INI',
145 | 'markdown': 'Markdown',
146 | 'sql': 'SQL',
147 | 'r': 'R',
148 | 'swift': 'Swift',
149 | 'kotlin': 'Kotlin',
150 | 'scala': 'Scala',
151 | 'lua': 'Lua',
152 | 'perl': 'Perl',
153 | 'powershell': 'PowerShell',
154 | 'bat': 'Batch',
155 | 'dockerfile': 'Dockerfile',
156 | 'plaintext': 'Plain Text',
157 | };
158 |
159 | /**
160 | * Popular languages for quick selection
161 | */
162 | const POPULAR_LANGUAGES = [
163 | 'javascript',
164 | 'typescript',
165 | 'python',
166 | 'java',
167 | 'cpp',
168 | 'csharp',
169 | 'html',
170 | 'css',
171 | 'php',
172 | 'go',
173 | 'rust',
174 | 'shell',
175 | 'json',
176 | 'markdown',
177 | 'sql'
178 | ];
179 |
180 | /**
181 | * Detects file type from filename or extension
182 | * @param {string} filename - The filename to analyze
183 | * @returns {string} Monaco Editor language identifier
184 | */
185 | export function detectFileType(filename) {
186 | if (!filename || typeof filename !== 'string') {
187 | return 'javascript'; // Default to JavaScript
188 | }
189 |
190 | // Convert to lowercase for case-insensitive matching
191 | const lowerFilename = filename.toLowerCase();
192 |
193 | // Handle special cases first
194 | if (lowerFilename === 'dockerfile' || lowerFilename.includes('dockerfile')) {
195 | return 'dockerfile';
196 | }
197 |
198 | if (lowerFilename === 'makefile' || lowerFilename.includes('makefile')) {
199 | return 'makefile';
200 | }
201 |
202 | // Extract extension
203 | const lastDotIndex = filename.lastIndexOf('.');
204 | if (lastDotIndex === -1) {
205 | return 'plaintext'; // No extension
206 | }
207 |
208 | const extension = filename.substring(lastDotIndex).toLowerCase();
209 |
210 | // Look up in our mapping
211 | return FILE_TYPE_MAP[extension] || 'plaintext';
212 | }
213 |
214 | /**
215 | * Gets the display name for a language
216 | * @param {string} language - Monaco Editor language identifier
217 | * @returns {string} Human-readable language name
218 | */
219 | export function getLanguageDisplayName(language) {
220 | return LANGUAGE_DISPLAY_NAMES[language] || language;
221 | }
222 |
223 | /**
224 | * Gets list of popular languages for quick selection
225 | * @returns {Array} Array of language objects with id and name
226 | */
227 | export function getPopularLanguages() {
228 | return POPULAR_LANGUAGES.map(lang => ({
229 | id: lang,
230 | name: getLanguageDisplayName(lang)
231 | }));
232 | }
233 |
234 | /**
235 | * Gets all supported languages
236 | * @returns {Array} Array of language objects with id and name
237 | */
238 | export function getAllSupportedLanguages() {
239 | return Object.keys(LANGUAGE_DISPLAY_NAMES).map(lang => ({
240 | id: lang,
241 | name: getLanguageDisplayName(lang)
242 | })).sort((a, b) => a.name.localeCompare(b.name));
243 | }
244 |
245 | /**
246 | * Validates if a language is supported
247 | * @param {string} language - Language to validate
248 | * @returns {boolean} Whether the language is supported
249 | */
250 | export function isLanguageSupported(language) {
251 | return Object.keys(LANGUAGE_DISPLAY_NAMES).includes(language);
252 | }
253 |
254 | /**
255 | * Gets file extension suggestions for a language
256 | * @param {string} language - Monaco Editor language identifier
257 | * @returns {Array} Array of common file extensions
258 | */
259 | export function getFileExtensionsForLanguage(language) {
260 | const extensions = [];
261 |
262 | for (const [ext, lang] of Object.entries(FILE_TYPE_MAP)) {
263 | if (lang === language) {
264 | extensions.push(ext);
265 | }
266 | }
267 |
268 | return extensions;
269 | }
270 |
--------------------------------------------------------------------------------
/frontend/vite-project/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | theme: {
4 | extend: {
5 | fontFamily: {
6 | sans: ['Inter', 'sans-serif'],
7 | },
8 | },
9 | },
10 | content: [
11 | './index.html',
12 | './src/**/*.{js,ts,jsx,tsx}',
13 | ],
14 | plugins: [],
15 | };
16 |
17 |
--------------------------------------------------------------------------------
/frontend/vite-project/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/(.*)",
5 | "destination": "/index.html"
6 | }
7 | ]
8 | }
--------------------------------------------------------------------------------
/frontend/vite-project/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | define: {
8 | global: 'globalThis',
9 | },
10 | })
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "codeeditor",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "node backend/server.js",
10 | "dev": "nodemon backend/server.js",
11 | "build": "npm install && cd frontend && cd vite-project && npm install && npm run build"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "dependencies": {
17 | "@monaco-editor/react": "^4.7.0",
18 | "cors": "^2.8.5",
19 | "express": "^4.21.2",
20 | "monaco-editor": "^0.52.2",
21 | "simple-peer": "^9.11.1",
22 | "socket.io": "^4.8.1",
23 | "socket.io-client": "^4.8.1"
24 | },
25 | "devDependencies": {
26 | "nodemon": "^3.1.10"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------