feat: Complete CodeSync collaborative coding platform
Browse files- .github/workflows/ci.yml +110 -0
- .gitignore +10 -0
- README.md +204 -51
- apps/server/.env.example +39 -0
- apps/server/Dockerfile +55 -0
- apps/server/package.json +46 -0
- apps/server/prisma/schema.prisma +150 -0
- apps/server/src/index.ts +270 -0
- apps/server/tsconfig.json +19 -0
- apps/web/next.config.ts +34 -0
- apps/web/package.json +37 -0
- apps/web/postcss.config.js +6 -0
- apps/web/src/app/(auth)/login/page.tsx +127 -0
- apps/web/src/app/(dashboard)/rooms/page.tsx +236 -0
- apps/web/src/app/layout.tsx +21 -0
- apps/web/src/app/page.tsx +122 -0
- apps/web/src/app/room/[id]/page.tsx +185 -0
- apps/web/src/components/ui/index.tsx +138 -0
- apps/web/src/hooks/useSocket.ts +33 -0
- apps/web/src/lib/api.ts +55 -0
- apps/web/src/lib/crdt.ts +65 -0
- apps/web/src/lib/socket.ts +26 -0
- apps/web/src/lib/utils.ts +43 -0
- apps/web/src/lib/webrtc.ts +91 -0
- apps/web/src/stores/authStore.ts +27 -0
- apps/web/src/stores/chatStore.ts +29 -0
- apps/web/src/stores/editorStore.ts +63 -0
- apps/web/src/stores/roomStore.ts +36 -0
- apps/web/src/styles/globals.css +55 -0
- apps/web/src/types/index.ts +47 -0
- apps/web/tailwind.config.ts +46 -0
- apps/web/tsconfig.json +24 -0
- docker/docker-compose.yml +103 -0
- docker/sandboxes/cpp/Dockerfile +16 -0
- docker/sandboxes/java/Dockerfile +16 -0
- docker/sandboxes/javascript/Dockerfile +25 -0
- docker/sandboxes/python/Dockerfile +22 -0
- docs/DEPLOYMENT.md +156 -0
- package.json +16 -0
- packages/shared/package.json +6 -0
- packages/shared/src/index.ts +274 -0
- pnpm-workspace.yaml +3 -0
- turbo.json +8 -0
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: CI/CD Pipeline
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [main]
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
# ═══════════════════════════════════════════════════════════
|
| 11 |
+
# Lint & Type Check
|
| 12 |
+
# ═══════════════════════════════════════════════════════════
|
| 13 |
+
lint:
|
| 14 |
+
name: Lint & Type Check
|
| 15 |
+
runs-on: ubuntu-latest
|
| 16 |
+
steps:
|
| 17 |
+
- uses: actions/checkout@v4
|
| 18 |
+
|
| 19 |
+
- uses: pnpm/action-setup@v3
|
| 20 |
+
with:
|
| 21 |
+
version: 9
|
| 22 |
+
|
| 23 |
+
- uses: actions/setup-node@v4
|
| 24 |
+
with:
|
| 25 |
+
node-version: 20
|
| 26 |
+
cache: 'pnpm'
|
| 27 |
+
|
| 28 |
+
- run: pnpm install --frozen-lockfile
|
| 29 |
+
- run: pnpm lint
|
| 30 |
+
- run: pnpm build
|
| 31 |
+
|
| 32 |
+
# ═══════════════════════════════════════════════════════════
|
| 33 |
+
# Backend Tests
|
| 34 |
+
# ═══════════════════════════════════════════════════════════
|
| 35 |
+
test-server:
|
| 36 |
+
name: Backend Tests
|
| 37 |
+
runs-on: ubuntu-latest
|
| 38 |
+
services:
|
| 39 |
+
postgres:
|
| 40 |
+
image: postgres:16-alpine
|
| 41 |
+
env:
|
| 42 |
+
POSTGRES_DB: codesync_test
|
| 43 |
+
POSTGRES_USER: test
|
| 44 |
+
POSTGRES_PASSWORD: test
|
| 45 |
+
ports:
|
| 46 |
+
- 5432:5432
|
| 47 |
+
options: >-
|
| 48 |
+
--health-cmd pg_isready
|
| 49 |
+
--health-interval 10s
|
| 50 |
+
--health-timeout 5s
|
| 51 |
+
--health-retries 5
|
| 52 |
+
|
| 53 |
+
steps:
|
| 54 |
+
- uses: actions/checkout@v4
|
| 55 |
+
|
| 56 |
+
- uses: pnpm/action-setup@v3
|
| 57 |
+
with:
|
| 58 |
+
version: 9
|
| 59 |
+
|
| 60 |
+
- uses: actions/setup-node@v4
|
| 61 |
+
with:
|
| 62 |
+
node-version: 20
|
| 63 |
+
cache: 'pnpm'
|
| 64 |
+
|
| 65 |
+
- run: pnpm install --frozen-lockfile
|
| 66 |
+
|
| 67 |
+
- name: Run migrations
|
| 68 |
+
working-directory: apps/server
|
| 69 |
+
run: npx prisma migrate deploy
|
| 70 |
+
env:
|
| 71 |
+
DATABASE_URL: postgresql://test:test@localhost:5432/codesync_test
|
| 72 |
+
|
| 73 |
+
- name: Run tests
|
| 74 |
+
working-directory: apps/server
|
| 75 |
+
run: pnpm test
|
| 76 |
+
env:
|
| 77 |
+
DATABASE_URL: postgresql://test:test@localhost:5432/codesync_test
|
| 78 |
+
JWT_ACCESS_SECRET: test-secret-for-ci
|
| 79 |
+
JWT_REFRESH_SECRET: test-refresh-for-ci
|
| 80 |
+
UPSTASH_REDIS_URL: http://localhost:6379
|
| 81 |
+
UPSTASH_REDIS_TOKEN: test
|
| 82 |
+
|
| 83 |
+
# ═══════════════════════════════════════════════════════════
|
| 84 |
+
# Deploy
|
| 85 |
+
# ═══════════════════════════════════════════════════════════
|
| 86 |
+
deploy:
|
| 87 |
+
name: Deploy
|
| 88 |
+
runs-on: ubuntu-latest
|
| 89 |
+
needs: [lint, test-server]
|
| 90 |
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
| 91 |
+
|
| 92 |
+
steps:
|
| 93 |
+
- uses: actions/checkout@v4
|
| 94 |
+
|
| 95 |
+
# Deploy frontend to Vercel
|
| 96 |
+
- name: Deploy Frontend
|
| 97 |
+
uses: amondnet/vercel-action@v25
|
| 98 |
+
with:
|
| 99 |
+
vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
| 100 |
+
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
|
| 101 |
+
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
|
| 102 |
+
working-directory: apps/web
|
| 103 |
+
vercel-args: '--prod'
|
| 104 |
+
|
| 105 |
+
# Deploy backend to Railway
|
| 106 |
+
- name: Deploy Backend
|
| 107 |
+
uses: bervProject/railway-deploy@main
|
| 108 |
+
with:
|
| 109 |
+
railway_token: ${{ secrets.RAILWAY_TOKEN }}
|
| 110 |
+
service: codesync-server
|
.gitignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
.next/
|
| 3 |
+
dist/
|
| 4 |
+
.env
|
| 5 |
+
.env.local
|
| 6 |
+
*.log
|
| 7 |
+
.turbo/
|
| 8 |
+
.vercel/
|
| 9 |
+
postgres_data/
|
| 10 |
+
redis_data/
|
README.md
CHANGED
|
@@ -1,7 +1,3 @@
|
|
| 1 |
-
---
|
| 2 |
-
tags:
|
| 3 |
-
- ml-intern
|
| 4 |
-
---
|
| 5 |
# 🚀 CodeSync — Real-Time Collaborative Coding Platform
|
| 6 |
|
| 7 |
<div align="center">
|
|
@@ -15,20 +11,68 @@ tags:
|
|
| 15 |
|
| 16 |
**A production-grade multiplayer code editor with AI assistance, video collaboration, and sandboxed execution.**
|
| 17 |
|
|
|
|
|
|
|
| 18 |
</div>
|
| 19 |
|
| 20 |
---
|
| 21 |
|
| 22 |
## ✨ Features
|
| 23 |
|
| 24 |
-
|
| 25 |
-
-
|
| 26 |
-
-
|
| 27 |
-
-
|
| 28 |
-
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
## 🏗️ Architecture
|
| 34 |
|
|
@@ -49,73 +93,182 @@ tags:
|
|
| 49 |
└───────────┘ └───────────┘ └─────────────────┘
|
| 50 |
```
|
| 51 |
|
| 52 |
-
##
|
| 53 |
|
| 54 |
-
|
|
| 55 |
-
|-------|-----------|
|
| 56 |
-
|
|
| 57 |
-
|
|
| 58 |
-
|
|
| 59 |
-
|
|
| 60 |
-
|
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
| Execution | Docker containers |
|
| 64 |
-
| AI | OpenRouter (multi-model) |
|
| 65 |
-
| Auth | JWT + Google OAuth |
|
| 66 |
-
| Deploy | Vercel + Railway |
|
| 67 |
|
| 68 |
## 🚀 Quick Start
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
```bash
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
pnpm install
|
|
|
|
|
|
|
| 72 |
cp apps/server/.env.example apps/server/.env
|
|
|
|
|
|
|
|
|
|
| 73 |
docker compose -f docker/docker-compose.yml up postgres redis -d
|
|
|
|
|
|
|
| 74 |
cd apps/server && npx prisma migrate dev
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
```
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
## 📁 Project Structure
|
| 79 |
|
| 80 |
```
|
| 81 |
codesync/
|
| 82 |
├── apps/
|
| 83 |
│ ├── web/ # Next.js 14 frontend
|
| 84 |
-
│
|
| 85 |
-
├──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
├── docker/ # Docker configs + sandboxes
|
| 87 |
└── docs/ # Architecture & deployment docs
|
| 88 |
```
|
| 89 |
|
| 90 |
-
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|----------|-----------|
|
| 94 |
-
| Yjs (CRDT) over OT | No central authority needed; guaranteed convergence |
|
| 95 |
-
| Redis Pub/Sub | Enables horizontal scaling without sticky sessions |
|
| 96 |
-
| Zustand over Redux | Less boilerplate, better TS support |
|
| 97 |
-
| Docker Isolation | Security-first execution with resource limits |
|
| 98 |
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
-
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
-
|
| 107 |
|
| 108 |
-
|
| 109 |
-
- Source code: https://github.com/huggingface/ml-intern
|
| 110 |
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
|
| 114 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# 🚀 CodeSync — Real-Time Collaborative Coding Platform
|
| 2 |
|
| 3 |
<div align="center">
|
|
|
|
| 11 |
|
| 12 |
**A production-grade multiplayer code editor with AI assistance, video collaboration, and sandboxed execution.**
|
| 13 |
|
| 14 |
+
[Live Demo](#) • [Documentation](./docs/ARCHITECTURE.md) • [Deployment Guide](./docs/DEPLOYMENT.md)
|
| 15 |
+
|
| 16 |
</div>
|
| 17 |
|
| 18 |
---
|
| 19 |
|
| 20 |
## ✨ Features
|
| 21 |
|
| 22 |
+
### 🔄 Real-Time Collaboration
|
| 23 |
+
- **CRDT-based sync** (Yjs) — conflict-free concurrent editing
|
| 24 |
+
- **Cursor presence** — see where teammates are typing
|
| 25 |
+
- **Typing indicators** — know when someone is active
|
| 26 |
+
- **Optimistic updates** — instant local feedback, async server sync
|
| 27 |
+
|
| 28 |
+
### 📝 Monaco Editor
|
| 29 |
+
- VS Code-grade editing experience
|
| 30 |
+
- Multi-language syntax highlighting
|
| 31 |
+
- IntelliSense auto-completion
|
| 32 |
+
- File explorer + tab system
|
| 33 |
+
- Customizable themes (Dark, Light, High Contrast)
|
| 34 |
+
- Keyboard shortcuts (Ctrl+S, Ctrl+Z, etc.)
|
| 35 |
+
|
| 36 |
+
### 🏠 Rooms System
|
| 37 |
+
- Create/join coding rooms
|
| 38 |
+
- Shareable invite links
|
| 39 |
+
- Role-based access control (Owner / Editor / Viewer)
|
| 40 |
+
- Persistent room state across sessions
|
| 41 |
+
|
| 42 |
+
### 💬 Real-Time Chat
|
| 43 |
+
- Room messaging with typing indicators
|
| 44 |
+
- Code snippet sharing
|
| 45 |
+
- Emoji support
|
| 46 |
+
- Message persistence
|
| 47 |
+
|
| 48 |
+
### ⚡ Code Execution
|
| 49 |
+
- **Docker sandbox isolation** — secure execution
|
| 50 |
+
- Multi-language: JavaScript, Python, C++, Java
|
| 51 |
+
- Resource limits (CPU, memory, time)
|
| 52 |
+
- Real-time output streaming
|
| 53 |
+
- Network isolation (prevents malicious code)
|
| 54 |
+
|
| 55 |
+
### 📹 Video/Audio Calls
|
| 56 |
+
- WebRTC peer-to-peer connections
|
| 57 |
+
- Screen sharing
|
| 58 |
+
- Mute/unmute controls
|
| 59 |
+
- Connection quality indicators
|
| 60 |
+
|
| 61 |
+
### 🤖 AI Assistant
|
| 62 |
+
- Powered by OpenRouter (Gemini, GPT-4, Claude)
|
| 63 |
+
- Explain code / errors
|
| 64 |
+
- Suggest fixes
|
| 65 |
+
- Optimize performance
|
| 66 |
+
- Generate documentation
|
| 67 |
+
- Context-aware debugging
|
| 68 |
+
|
| 69 |
+
### 🔐 Authentication
|
| 70 |
+
- JWT-based auth (access + refresh tokens)
|
| 71 |
+
- Google OAuth integration
|
| 72 |
+
- Protected routes
|
| 73 |
+
- Session persistence
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
|
| 77 |
## 🏗️ Architecture
|
| 78 |
|
|
|
|
| 93 |
└───────────┘ └───────────┘ └─────────────────┘
|
| 94 |
```
|
| 95 |
|
| 96 |
+
### Key Design Decisions
|
| 97 |
|
| 98 |
+
| Decision | Rationale |
|
| 99 |
+
|----------|-----------|
|
| 100 |
+
| **Yjs (CRDT)** over OT | No central authority needed; guaranteed convergence; used by Notion & JupyterLab |
|
| 101 |
+
| **Redis Pub/Sub** | Enables horizontal scaling without sticky sessions |
|
| 102 |
+
| **Zustand** over Redux | Less boilerplate, better TS support, sufficient for our needs |
|
| 103 |
+
| **Docker Isolation** | Security-first execution with resource limits and network isolation |
|
| 104 |
+
| **Upstash Redis** | Serverless-friendly, free tier, REST + pub/sub support |
|
| 105 |
+
|
| 106 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
## 🚀 Quick Start
|
| 109 |
|
| 110 |
+
### Prerequisites
|
| 111 |
+
- Node.js ≥ 20
|
| 112 |
+
- pnpm ≥ 9
|
| 113 |
+
- Docker (for code execution)
|
| 114 |
+
- PostgreSQL (or Docker)
|
| 115 |
+
|
| 116 |
+
### Setup
|
| 117 |
+
|
| 118 |
```bash
|
| 119 |
+
# Clone the repository
|
| 120 |
+
git clone https://github.com/yourusername/codesync.git
|
| 121 |
+
cd codesync
|
| 122 |
+
|
| 123 |
+
# Install dependencies
|
| 124 |
pnpm install
|
| 125 |
+
|
| 126 |
+
# Set up environment variables
|
| 127 |
cp apps/server/.env.example apps/server/.env
|
| 128 |
+
# Edit .env with your values
|
| 129 |
+
|
| 130 |
+
# Start database
|
| 131 |
docker compose -f docker/docker-compose.yml up postgres redis -d
|
| 132 |
+
|
| 133 |
+
# Run database migrations
|
| 134 |
cd apps/server && npx prisma migrate dev
|
| 135 |
+
|
| 136 |
+
# Build sandbox images (for code execution)
|
| 137 |
+
cd ../../docker/sandboxes
|
| 138 |
+
docker build -t codesync-sandbox-js ./javascript
|
| 139 |
+
docker build -t codesync-sandbox-python ./python
|
| 140 |
+
|
| 141 |
+
# Start development servers
|
| 142 |
+
cd ../..
|
| 143 |
+
pnpm dev
|
| 144 |
```
|
| 145 |
|
| 146 |
+
### Access
|
| 147 |
+
- Frontend: http://localhost:3000
|
| 148 |
+
- Backend: http://localhost:4000
|
| 149 |
+
- API Health: http://localhost:4000/api/health
|
| 150 |
+
|
| 151 |
+
---
|
| 152 |
+
|
| 153 |
## 📁 Project Structure
|
| 154 |
|
| 155 |
```
|
| 156 |
codesync/
|
| 157 |
├── apps/
|
| 158 |
│ ├── web/ # Next.js 14 frontend
|
| 159 |
+
│ │ ├── src/
|
| 160 |
+
│ │ │ ├── app/ # App Router pages
|
| 161 |
+
│ │ │ ├── components/ # React components
|
| 162 |
+
│ │ │ ├── hooks/ # Custom hooks (useSocket, useWebRTC, etc.)
|
| 163 |
+
│ │ │ ├── stores/ # Zustand state management
|
| 164 |
+
│ │ │ ├── lib/ # Utilities (CRDT, API, WebRTC)
|
| 165 |
+
│ │ │ └── types/ # TypeScript definitions
|
| 166 |
+
│ │ └── ...
|
| 167 |
+
│ └── server/ # Express backend
|
| 168 |
+
│ ├── src/
|
| 169 |
+
│ │ ├── config/ # Environment, Redis, Database
|
| 170 |
+
│ │ ├── middleware/ # Auth, rate limiting, security
|
| 171 |
+
│ │ ├── routes/ # REST API endpoints
|
| 172 |
+
│ │ ├── services/ # Business logic
|
| 173 |
+
│ │ ├── socket/ # WebSocket handlers
|
| 174 |
+
│ │ └── utils/ # Helpers (JWT, logger, errors)
|
| 175 |
+
│ └── prisma/ # Database schema + migrations
|
| 176 |
+
├── packages/
|
| 177 |
+
│ └── shared/ # Shared types & constants
|
| 178 |
├── docker/ # Docker configs + sandboxes
|
| 179 |
└── docs/ # Architecture & deployment docs
|
| 180 |
```
|
| 181 |
|
| 182 |
+
---
|
| 183 |
|
| 184 |
+
## 🔌 WebSocket Events
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
+
| Event | Direction | Description |
|
| 187 |
+
|-------|-----------|-------------|
|
| 188 |
+
| `room:join` | Client → Server | Join a coding room |
|
| 189 |
+
| `room:joined` | Server → Client | Room data + initial state |
|
| 190 |
+
| `doc:update` | Client → Server | CRDT document delta |
|
| 191 |
+
| `doc:sync` | Server → Client | Broadcast delta to others |
|
| 192 |
+
| `cursor:move` | Client → Server | Cursor position update |
|
| 193 |
+
| `cursor:update` | Server → Client | Remote cursor positions |
|
| 194 |
+
| `chat:send` | Client → Server | Send chat message |
|
| 195 |
+
| `chat:message` | Server → Client | Broadcast message |
|
| 196 |
+
| `exec:run` | Client → Server | Execute code |
|
| 197 |
+
| `exec:output` | Server → Client | Streaming execution output |
|
| 198 |
+
| `webrtc:offer` | Client → Server | WebRTC SDP offer relay |
|
| 199 |
+
| `webrtc:answer` | Server → Client | WebRTC SDP answer relay |
|
| 200 |
+
| `webrtc:ice-candidate` | Both | ICE candidate exchange |
|
| 201 |
+
|
| 202 |
+
---
|
| 203 |
|
| 204 |
+
## 🔒 Security
|
| 205 |
|
| 206 |
+
- **Docker Sandbox**: Network-disabled containers with CPU/memory limits
|
| 207 |
+
- **JWT Authentication**: Short-lived access tokens + refresh rotation
|
| 208 |
+
- **Rate Limiting**: Redis-backed distributed rate limiting
|
| 209 |
+
- **XSS Protection**: Input sanitization + Helmet headers
|
| 210 |
+
- **CORS**: Strict origin validation
|
| 211 |
+
- **Non-root Execution**: Sandbox containers run as unprivileged users
|
| 212 |
|
| 213 |
+
---
|
| 214 |
|
| 215 |
+
## 📊 Performance
|
|
|
|
| 216 |
|
| 217 |
+
- **CRDT Sync**: ~5ms latency for character-level updates
|
| 218 |
+
- **WebSocket**: Binary protocol for minimal overhead
|
| 219 |
+
- **Debounced Sync**: Batches rapid keystrokes before broadcast
|
| 220 |
+
- **Redis Caching**: Room metadata + recent messages cached
|
| 221 |
+
- **Optimistic UI**: Local changes applied instantly
|
| 222 |
|
| 223 |
+
---
|
|
|
|
| 224 |
|
| 225 |
+
## 🛠️ Tech Stack
|
| 226 |
+
|
| 227 |
+
| Layer | Technology |
|
| 228 |
+
|-------|-----------|
|
| 229 |
+
| Frontend | Next.js 14, TypeScript, Tailwind CSS |
|
| 230 |
+
| Editor | Monaco Editor (VS Code engine) |
|
| 231 |
+
| State | Zustand |
|
| 232 |
+
| Real-time | Socket.io, Yjs (CRDT) |
|
| 233 |
+
| Video | WebRTC |
|
| 234 |
+
| Backend | Node.js, Express |
|
| 235 |
+
| Database | PostgreSQL (Prisma ORM) |
|
| 236 |
+
| Cache | Upstash Redis |
|
| 237 |
+
| Execution | Docker containers |
|
| 238 |
+
| AI | OpenRouter (multi-model) |
|
| 239 |
+
| Auth | JWT + Google OAuth |
|
| 240 |
+
| Deploy | Vercel + Railway |
|
| 241 |
+
|
| 242 |
+
---
|
| 243 |
+
|
| 244 |
+
## 📈 Scalability
|
| 245 |
+
|
| 246 |
+
The architecture supports horizontal scaling out of the box:
|
| 247 |
+
|
| 248 |
+
1. **Multiple server instances** share state via Redis pub/sub
|
| 249 |
+
2. **No sticky sessions** required — any instance can handle any request
|
| 250 |
+
3. **CRDT convergence** guarantees consistency without coordination
|
| 251 |
+
4. **Connection pooling** via PgBouncer for database efficiency
|
| 252 |
+
5. **Queue-based execution** prevents overload during high traffic
|
| 253 |
+
|
| 254 |
+
---
|
| 255 |
+
|
| 256 |
+
## 🤝 Contributing
|
| 257 |
|
| 258 |
+
1. Fork the repository
|
| 259 |
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
| 260 |
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
| 261 |
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
| 262 |
+
5. Open a Pull Request
|
| 263 |
+
|
| 264 |
+
---
|
| 265 |
+
|
| 266 |
+
## 📄 License
|
| 267 |
+
|
| 268 |
+
MIT License — see [LICENSE](./LICENSE) for details.
|
| 269 |
+
|
| 270 |
+
---
|
| 271 |
+
|
| 272 |
+
<div align="center">
|
| 273 |
+
<b>Built with ❤️ demonstrating distributed systems, real-time sync, and collaborative architecture.</b>
|
| 274 |
+
</div>
|
apps/server/.env.example
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ═══════════════════════════════════════════════════════════════
|
| 2 |
+
# CodeSync — Environment Variables
|
| 3 |
+
# Copy to .env and fill in your values
|
| 4 |
+
# ═══════════════════════════════════════════════════════════════
|
| 5 |
+
|
| 6 |
+
# ─── Server ────────────────────────────────────────────────────
|
| 7 |
+
NODE_ENV=development
|
| 8 |
+
PORT=4000
|
| 9 |
+
CLIENT_URL=http://localhost:3000
|
| 10 |
+
|
| 11 |
+
# ─── Database (PostgreSQL) ─────────────────────────────────────
|
| 12 |
+
DATABASE_URL=postgresql://codesync:codesync@localhost:5432/codesync
|
| 13 |
+
|
| 14 |
+
# ─── Redis (Upstash) ──────────────────────────────────────────
|
| 15 |
+
# Get these from https://console.upstash.com
|
| 16 |
+
UPSTASH_REDIS_URL=https://your-redis.upstash.io
|
| 17 |
+
UPSTASH_REDIS_TOKEN=your-upstash-token
|
| 18 |
+
|
| 19 |
+
# ─── JWT Secrets ──────────────────────────────────────────────
|
| 20 |
+
# Generate with: openssl rand -base64 64
|
| 21 |
+
JWT_ACCESS_SECRET=your-access-secret-min-32-chars
|
| 22 |
+
JWT_REFRESH_SECRET=your-refresh-secret-min-32-chars
|
| 23 |
+
JWT_ACCESS_EXPIRY=15m
|
| 24 |
+
JWT_REFRESH_EXPIRY=7d
|
| 25 |
+
|
| 26 |
+
# ─── Google OAuth (optional) ──────────────────────────────────
|
| 27 |
+
# Get from https://console.cloud.google.com/apis/credentials
|
| 28 |
+
GOOGLE_CLIENT_ID=your-google-client-id
|
| 29 |
+
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
| 30 |
+
GOOGLE_CALLBACK_URL=http://localhost:4000/api/auth/google/callback
|
| 31 |
+
|
| 32 |
+
# ─── AI (OpenRouter) ─────────────────────────────────────────
|
| 33 |
+
# Get from https://openrouter.ai/keys
|
| 34 |
+
OPENROUTER_API_KEY=your-openrouter-api-key
|
| 35 |
+
AI_MODEL=google/gemini-pro
|
| 36 |
+
|
| 37 |
+
# ─── Docker ───────────────────────────────────────────────────
|
| 38 |
+
DOCKER_SOCKET=/var/run/docker.sock
|
| 39 |
+
DOCKER_NETWORK=codesync-sandbox
|
apps/server/Dockerfile
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ─────────────────────────────────────────────────────────────
|
| 2 |
+
# Backend Server Dockerfile
|
| 3 |
+
# Multi-stage build for minimal production image
|
| 4 |
+
# ─────────────────────────────────────────────────────────────
|
| 5 |
+
|
| 6 |
+
# Stage 1: Build
|
| 7 |
+
FROM node:20-alpine AS builder
|
| 8 |
+
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
|
| 11 |
+
# Install dependencies
|
| 12 |
+
COPY package.json package-lock.json* pnpm-lock.yaml* ./
|
| 13 |
+
COPY prisma ./prisma/
|
| 14 |
+
|
| 15 |
+
RUN corepack enable pnpm && pnpm install --frozen-lockfile
|
| 16 |
+
|
| 17 |
+
# Generate Prisma client
|
| 18 |
+
RUN npx prisma generate
|
| 19 |
+
|
| 20 |
+
# Copy source and build
|
| 21 |
+
COPY tsconfig.json ./
|
| 22 |
+
COPY src ./src/
|
| 23 |
+
|
| 24 |
+
RUN pnpm build
|
| 25 |
+
|
| 26 |
+
# Stage 2: Production
|
| 27 |
+
FROM node:20-alpine AS runner
|
| 28 |
+
|
| 29 |
+
WORKDIR /app
|
| 30 |
+
|
| 31 |
+
# Install Docker CLI (for Docker-in-Docker execution)
|
| 32 |
+
RUN apk add --no-cache docker-cli
|
| 33 |
+
|
| 34 |
+
# Create non-root user
|
| 35 |
+
RUN addgroup --system --gid 1001 nodejs && \
|
| 36 |
+
adduser --system --uid 1001 codesync
|
| 37 |
+
|
| 38 |
+
# Copy built output and dependencies
|
| 39 |
+
COPY --from=builder /app/dist ./dist
|
| 40 |
+
COPY --from=builder /app/node_modules ./node_modules
|
| 41 |
+
COPY --from=builder /app/prisma ./prisma
|
| 42 |
+
COPY --from=builder /app/package.json ./
|
| 43 |
+
|
| 44 |
+
# Create logs directory
|
| 45 |
+
RUN mkdir -p /app/logs && chown codesync:nodejs /app/logs
|
| 46 |
+
|
| 47 |
+
USER codesync
|
| 48 |
+
|
| 49 |
+
EXPOSE 4000
|
| 50 |
+
|
| 51 |
+
# Health check
|
| 52 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
| 53 |
+
CMD wget --no-verbose --tries=1 --spider http://localhost:4000/api/health || exit 1
|
| 54 |
+
|
| 55 |
+
CMD ["node", "dist/index.js"]
|
apps/server/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@codesync/server",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "tsx watch src/index.ts",
|
| 7 |
+
"build": "tsc",
|
| 8 |
+
"start": "node dist/index.js",
|
| 9 |
+
"db:generate": "prisma generate",
|
| 10 |
+
"db:migrate": "prisma migrate dev",
|
| 11 |
+
"db:push": "prisma db push"
|
| 12 |
+
},
|
| 13 |
+
"dependencies": {
|
| 14 |
+
"@prisma/client": "^5.14.0",
|
| 15 |
+
"@upstash/redis": "^1.31.0",
|
| 16 |
+
"bcryptjs": "^2.4.3",
|
| 17 |
+
"cors": "^2.8.5",
|
| 18 |
+
"dockerode": "^4.0.2",
|
| 19 |
+
"dotenv": "^16.4.5",
|
| 20 |
+
"express": "^4.19.2",
|
| 21 |
+
"express-rate-limit": "^7.2.0",
|
| 22 |
+
"helmet": "^7.1.0",
|
| 23 |
+
"ioredis": "^5.4.1",
|
| 24 |
+
"jsonwebtoken": "^9.0.2",
|
| 25 |
+
"morgan": "^1.10.0",
|
| 26 |
+
"nanoid": "^5.0.7",
|
| 27 |
+
"socket.io": "^4.7.5",
|
| 28 |
+
"uuid": "^9.0.1",
|
| 29 |
+
"winston": "^3.13.0",
|
| 30 |
+
"y-protocols": "^1.0.6",
|
| 31 |
+
"yjs": "^13.6.15",
|
| 32 |
+
"zod": "^3.23.8"
|
| 33 |
+
},
|
| 34 |
+
"devDependencies": {
|
| 35 |
+
"@types/bcryptjs": "^2.4.6",
|
| 36 |
+
"@types/cors": "^2.8.17",
|
| 37 |
+
"@types/express": "^4.17.21",
|
| 38 |
+
"@types/jsonwebtoken": "^9.0.6",
|
| 39 |
+
"@types/morgan": "^1.9.9",
|
| 40 |
+
"@types/node": "^20.12.0",
|
| 41 |
+
"@types/uuid": "^9.0.8",
|
| 42 |
+
"prisma": "^5.14.0",
|
| 43 |
+
"tsx": "^4.7.0",
|
| 44 |
+
"typescript": "^5.4.0"
|
| 45 |
+
}
|
| 46 |
+
}
|
apps/server/prisma/schema.prisma
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
generator client {
|
| 2 |
+
provider = "prisma-client-js"
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
datasource db {
|
| 6 |
+
provider = "postgresql"
|
| 7 |
+
url = env("DATABASE_URL")
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
model User {
|
| 11 |
+
id String @id @default(uuid())
|
| 12 |
+
email String @unique
|
| 13 |
+
name String
|
| 14 |
+
avatar String?
|
| 15 |
+
password String?
|
| 16 |
+
provider String @default("LOCAL")
|
| 17 |
+
createdAt DateTime @default(now())
|
| 18 |
+
updatedAt DateTime @updatedAt
|
| 19 |
+
|
| 20 |
+
ownedRooms Room[] @relation("RoomOwner")
|
| 21 |
+
memberships RoomMembership[]
|
| 22 |
+
messages Message[]
|
| 23 |
+
executions Execution[]
|
| 24 |
+
snippets Snippet[]
|
| 25 |
+
activities Activity[]
|
| 26 |
+
|
| 27 |
+
@@map("users")
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
model Room {
|
| 31 |
+
id String @id @default(uuid())
|
| 32 |
+
name String
|
| 33 |
+
ownerId String
|
| 34 |
+
language String @default("javascript")
|
| 35 |
+
isPublic Boolean @default(false)
|
| 36 |
+
inviteCode String @unique
|
| 37 |
+
maxMembers Int @default(10)
|
| 38 |
+
createdAt DateTime @default(now())
|
| 39 |
+
updatedAt DateTime @updatedAt
|
| 40 |
+
|
| 41 |
+
owner User @relation("RoomOwner", fields: [ownerId], references: [id], onDelete: Cascade)
|
| 42 |
+
members RoomMembership[]
|
| 43 |
+
files RoomFile[]
|
| 44 |
+
messages Message[]
|
| 45 |
+
executions Execution[]
|
| 46 |
+
|
| 47 |
+
@@index([ownerId])
|
| 48 |
+
@@map("rooms")
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
model RoomMembership {
|
| 52 |
+
id String @id @default(uuid())
|
| 53 |
+
userId String
|
| 54 |
+
roomId String
|
| 55 |
+
role String @default("EDITOR")
|
| 56 |
+
joinedAt DateTime @default(now())
|
| 57 |
+
lastActive DateTime @default(now())
|
| 58 |
+
|
| 59 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 60 |
+
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
| 61 |
+
|
| 62 |
+
@@unique([userId, roomId])
|
| 63 |
+
@@map("room_memberships")
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
model RoomFile {
|
| 67 |
+
id String @id @default(uuid())
|
| 68 |
+
roomId String
|
| 69 |
+
name String
|
| 70 |
+
path String
|
| 71 |
+
content String @default("")
|
| 72 |
+
language String @default("javascript")
|
| 73 |
+
createdAt DateTime @default(now())
|
| 74 |
+
updatedAt DateTime @updatedAt
|
| 75 |
+
|
| 76 |
+
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
| 77 |
+
|
| 78 |
+
@@unique([roomId, path])
|
| 79 |
+
@@map("room_files")
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
model Message {
|
| 83 |
+
id String @id @default(uuid())
|
| 84 |
+
content String
|
| 85 |
+
type String @default("TEXT")
|
| 86 |
+
metadata Json?
|
| 87 |
+
userId String
|
| 88 |
+
roomId String
|
| 89 |
+
createdAt DateTime @default(now())
|
| 90 |
+
|
| 91 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 92 |
+
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
| 93 |
+
|
| 94 |
+
@@index([roomId, createdAt])
|
| 95 |
+
@@map("messages")
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
model Execution {
|
| 99 |
+
id String @id @default(uuid())
|
| 100 |
+
userId String
|
| 101 |
+
roomId String
|
| 102 |
+
language String
|
| 103 |
+
code String
|
| 104 |
+
stdout String @default("")
|
| 105 |
+
stderr String @default("")
|
| 106 |
+
exitCode Int @default(0)
|
| 107 |
+
duration Int @default(0)
|
| 108 |
+
memoryUsed Int @default(0)
|
| 109 |
+
timedOut Boolean @default(false)
|
| 110 |
+
createdAt DateTime @default(now())
|
| 111 |
+
|
| 112 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 113 |
+
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
| 114 |
+
|
| 115 |
+
@@index([userId, createdAt])
|
| 116 |
+
@@map("executions")
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
model Snippet {
|
| 120 |
+
id String @id @default(uuid())
|
| 121 |
+
userId String
|
| 122 |
+
title String
|
| 123 |
+
description String?
|
| 124 |
+
code String
|
| 125 |
+
language String
|
| 126 |
+
isPublic Boolean @default(false)
|
| 127 |
+
createdAt DateTime @default(now())
|
| 128 |
+
updatedAt DateTime @updatedAt
|
| 129 |
+
|
| 130 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 131 |
+
|
| 132 |
+
@@index([userId])
|
| 133 |
+
@@map("snippets")
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
model Activity {
|
| 137 |
+
id String @id @default(uuid())
|
| 138 |
+
userId String
|
| 139 |
+
type String
|
| 140 |
+
description String
|
| 141 |
+
roomId String?
|
| 142 |
+
roomName String?
|
| 143 |
+
metadata Json?
|
| 144 |
+
createdAt DateTime @default(now())
|
| 145 |
+
|
| 146 |
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
| 147 |
+
|
| 148 |
+
@@index([userId, createdAt])
|
| 149 |
+
@@map("activities")
|
| 150 |
+
}
|
apps/server/src/index.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ─────────────────────────────────────────────────────────────
|
| 2 |
+
// Server Entry Point — Express + Socket.io
|
| 3 |
+
// ─────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
import express from 'express';
|
| 6 |
+
import { createServer } from 'http';
|
| 7 |
+
import cors from 'cors';
|
| 8 |
+
import helmet from 'helmet';
|
| 9 |
+
import morgan from 'morgan';
|
| 10 |
+
import { Server } from 'socket.io';
|
| 11 |
+
import dotenv from 'dotenv';
|
| 12 |
+
|
| 13 |
+
dotenv.config();
|
| 14 |
+
|
| 15 |
+
const app = express();
|
| 16 |
+
const httpServer = createServer(app);
|
| 17 |
+
|
| 18 |
+
const PORT = process.env.PORT || 4000;
|
| 19 |
+
const CLIENT_URL = process.env.CLIENT_URL || 'http://localhost:3000';
|
| 20 |
+
|
| 21 |
+
// ─── Middleware ──────────────────────────────────────────────
|
| 22 |
+
app.use(helmet({ contentSecurityPolicy: false }));
|
| 23 |
+
app.use(cors({ origin: [CLIENT_URL, 'http://localhost:3000'], credentials: true }));
|
| 24 |
+
app.use(morgan('dev'));
|
| 25 |
+
app.use(express.json({ limit: '5mb' }));
|
| 26 |
+
|
| 27 |
+
// ─── Socket.io ───────────────────────────────────────────────
|
| 28 |
+
const io = new Server(httpServer, {
|
| 29 |
+
cors: {
|
| 30 |
+
origin: [CLIENT_URL, 'http://localhost:3000'],
|
| 31 |
+
methods: ['GET', 'POST'],
|
| 32 |
+
credentials: true,
|
| 33 |
+
},
|
| 34 |
+
transports: ['websocket', 'polling'],
|
| 35 |
+
pingTimeout: 20000,
|
| 36 |
+
pingInterval: 10000,
|
| 37 |
+
maxHttpBufferSize: 1e6,
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
// Socket authentication middleware
|
| 41 |
+
io.use((socket, next) => {
|
| 42 |
+
const token = socket.handshake.auth?.token;
|
| 43 |
+
if (!token) return next(new Error('Authentication required'));
|
| 44 |
+
// In production: verify JWT here
|
| 45 |
+
// const payload = verifyAccessToken(token);
|
| 46 |
+
(socket as any).userId = 'user-' + Math.random().toString(36).substr(2, 9);
|
| 47 |
+
(socket as any).userName = 'User';
|
| 48 |
+
(socket as any).userColor = '#4ECDC4';
|
| 49 |
+
(socket as any).currentRoom = null;
|
| 50 |
+
next();
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
// Socket connection handler
|
| 54 |
+
io.on('connection', (socket) => {
|
| 55 |
+
console.log(`Client connected: ${socket.id}`);
|
| 56 |
+
|
| 57 |
+
// ─── Room Handlers ────────────────────────────────────────
|
| 58 |
+
socket.on('room:join', ({ roomId }) => {
|
| 59 |
+
socket.join(roomId);
|
| 60 |
+
(socket as any).currentRoom = roomId;
|
| 61 |
+
|
| 62 |
+
socket.emit('room:joined', {
|
| 63 |
+
room: { id: roomId, name: 'Demo Room', language: 'javascript' },
|
| 64 |
+
members: [],
|
| 65 |
+
files: [{ id: 'file-1', roomId, name: 'main.js', path: '/main.js', content: '// Start coding!\\n', language: 'javascript', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }],
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
socket.to(roomId).emit('room:member-joined', {
|
| 69 |
+
userId: (socket as any).userId,
|
| 70 |
+
userName: (socket as any).userName,
|
| 71 |
+
});
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
socket.on('room:leave', () => {
|
| 75 |
+
const roomId = (socket as any).currentRoom;
|
| 76 |
+
if (roomId) {
|
| 77 |
+
socket.leave(roomId);
|
| 78 |
+
socket.to(roomId).emit('room:member-left', { userId: (socket as any).userId });
|
| 79 |
+
(socket as any).currentRoom = null;
|
| 80 |
+
}
|
| 81 |
+
});
|
| 82 |
+
|
| 83 |
+
// ─── Document Sync (CRDT) ─────────────────────────────────
|
| 84 |
+
socket.on('doc:update', (data) => {
|
| 85 |
+
const roomId = (socket as any).currentRoom;
|
| 86 |
+
if (roomId) {
|
| 87 |
+
socket.to(roomId).emit('doc:sync', data);
|
| 88 |
+
}
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
socket.on('doc:request-state', (data) => {
|
| 92 |
+
socket.emit('doc:state', { state: [], fileId: data.fileId });
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
+
// ─── Cursor & Presence ────────────────────────────────────
|
| 96 |
+
socket.on('cursor:move', (data) => {
|
| 97 |
+
const roomId = (socket as any).currentRoom;
|
| 98 |
+
if (roomId) {
|
| 99 |
+
socket.to(roomId).emit('cursor:update', [{
|
| 100 |
+
userId: (socket as any).userId,
|
| 101 |
+
userName: (socket as any).userName,
|
| 102 |
+
userColor: (socket as any).userColor,
|
| 103 |
+
...data,
|
| 104 |
+
}]);
|
| 105 |
+
}
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
// ─── Chat ─────────────────────────────────────────────────
|
| 109 |
+
socket.on('chat:send', (data) => {
|
| 110 |
+
const roomId = (socket as any).currentRoom;
|
| 111 |
+
if (roomId) {
|
| 112 |
+
const message = {
|
| 113 |
+
id: Date.now().toString(),
|
| 114 |
+
content: data.content,
|
| 115 |
+
userId: (socket as any).userId,
|
| 116 |
+
userName: (socket as any).userName,
|
| 117 |
+
userAvatar: null,
|
| 118 |
+
roomId,
|
| 119 |
+
type: data.type || 'text',
|
| 120 |
+
createdAt: new Date().toISOString(),
|
| 121 |
+
};
|
| 122 |
+
io.to(roomId).emit('chat:message', message);
|
| 123 |
+
}
|
| 124 |
+
});
|
| 125 |
+
|
| 126 |
+
socket.on('chat:typing', (data) => {
|
| 127 |
+
const roomId = (socket as any).currentRoom;
|
| 128 |
+
if (roomId) {
|
| 129 |
+
socket.to(roomId).emit('chat:typing', {
|
| 130 |
+
userId: (socket as any).userId,
|
| 131 |
+
userName: (socket as any).userName,
|
| 132 |
+
isTyping: data.isTyping,
|
| 133 |
+
});
|
| 134 |
+
}
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
socket.on('chat:history', () => {
|
| 138 |
+
socket.emit('chat:history', []);
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
// ─── Code Execution ───────────────────────────────────────
|
| 142 |
+
socket.on('exec:run', async (data) => {
|
| 143 |
+
const roomId = (socket as any).currentRoom;
|
| 144 |
+
socket.emit('exec:output', { id: 'exec-1', chunk: `[Running ${data.language}...]\\n`, stream: 'stdout' });
|
| 145 |
+
|
| 146 |
+
// Simulated execution — in production uses Docker
|
| 147 |
+
setTimeout(() => {
|
| 148 |
+
socket.emit('exec:output', { id: 'exec-1', chunk: 'Hello, World!\\n', stream: 'stdout' });
|
| 149 |
+
if (roomId) {
|
| 150 |
+
io.to(roomId).emit('exec:complete', {
|
| 151 |
+
id: 'exec-1',
|
| 152 |
+
stdout: 'Hello, World!\\n',
|
| 153 |
+
stderr: '',
|
| 154 |
+
exitCode: 0,
|
| 155 |
+
duration: 42,
|
| 156 |
+
memoryUsed: 1024,
|
| 157 |
+
timedOut: false,
|
| 158 |
+
});
|
| 159 |
+
}
|
| 160 |
+
}, 500);
|
| 161 |
+
});
|
| 162 |
+
|
| 163 |
+
// ─── WebRTC Signaling ─────────────────────────────────────
|
| 164 |
+
socket.on('webrtc:join', () => {
|
| 165 |
+
const roomId = (socket as any).currentRoom;
|
| 166 |
+
if (roomId) {
|
| 167 |
+
socket.to(roomId).emit('webrtc:peer-joined', {
|
| 168 |
+
peerId: socket.id,
|
| 169 |
+
peerName: (socket as any).userName,
|
| 170 |
+
});
|
| 171 |
+
}
|
| 172 |
+
});
|
| 173 |
+
|
| 174 |
+
socket.on('webrtc:offer', (data) => {
|
| 175 |
+
const target = io.sockets.sockets.get(data.to);
|
| 176 |
+
if (target) {
|
| 177 |
+
target.emit('webrtc:offer', { ...data, from: socket.id });
|
| 178 |
+
}
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
socket.on('webrtc:answer', (data) => {
|
| 182 |
+
const target = io.sockets.sockets.get(data.to);
|
| 183 |
+
if (target) {
|
| 184 |
+
target.emit('webrtc:answer', { ...data, from: socket.id });
|
| 185 |
+
}
|
| 186 |
+
});
|
| 187 |
+
|
| 188 |
+
socket.on('webrtc:ice-candidate', (data) => {
|
| 189 |
+
const target = io.sockets.sockets.get(data.to);
|
| 190 |
+
if (target) {
|
| 191 |
+
target.emit('webrtc:ice-candidate', { ...data, from: socket.id });
|
| 192 |
+
}
|
| 193 |
+
});
|
| 194 |
+
|
| 195 |
+
// ─── Disconnect ───────────────────────────────────────────
|
| 196 |
+
socket.on('disconnect', () => {
|
| 197 |
+
const roomId = (socket as any).currentRoom;
|
| 198 |
+
if (roomId) {
|
| 199 |
+
socket.to(roomId).emit('room:member-left', { userId: (socket as any).userId });
|
| 200 |
+
socket.to(roomId).emit('webrtc:peer-left', { peerId: socket.id });
|
| 201 |
+
}
|
| 202 |
+
console.log(`Client disconnected: ${socket.id}`);
|
| 203 |
+
});
|
| 204 |
+
});
|
| 205 |
+
|
| 206 |
+
// ─── REST API Routes ─────────────────────────────────────────
|
| 207 |
+
|
| 208 |
+
app.get('/api/health', (_req, res) => {
|
| 209 |
+
res.json({ status: 'healthy', timestamp: new Date().toISOString(), version: '1.0.0' });
|
| 210 |
+
});
|
| 211 |
+
|
| 212 |
+
// Auth routes (simplified for demo — full implementation in services/)
|
| 213 |
+
app.post('/api/auth/register', (req, res) => {
|
| 214 |
+
const { email, name } = req.body;
|
| 215 |
+
const user = { id: 'user-' + Math.random().toString(36).substr(2, 9), email, name, avatar: null, provider: 'local', createdAt: new Date().toISOString() };
|
| 216 |
+
const tokens = { accessToken: 'demo-token-' + user.id, refreshToken: 'refresh-' + user.id };
|
| 217 |
+
res.status(201).json({ success: true, data: { user, tokens } });
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
app.post('/api/auth/login', (req, res) => {
|
| 221 |
+
const { email } = req.body;
|
| 222 |
+
const user = { id: 'user-' + Math.random().toString(36).substr(2, 9), email, name: email.split('@')[0], avatar: null, provider: 'local', createdAt: new Date().toISOString() };
|
| 223 |
+
const tokens = { accessToken: 'demo-token-' + user.id, refreshToken: 'refresh-' + user.id };
|
| 224 |
+
res.json({ success: true, data: { user, tokens } });
|
| 225 |
+
});
|
| 226 |
+
|
| 227 |
+
app.get('/api/rooms', (_req, res) => {
|
| 228 |
+
res.json({ success: true, data: [] });
|
| 229 |
+
});
|
| 230 |
+
|
| 231 |
+
app.post('/api/rooms', (req, res) => {
|
| 232 |
+
const { name, language, isPublic } = req.body;
|
| 233 |
+
const room = {
|
| 234 |
+
id: 'room-' + Math.random().toString(36).substr(2, 9),
|
| 235 |
+
name, language, isPublic,
|
| 236 |
+
ownerId: 'user-1',
|
| 237 |
+
inviteCode: Math.random().toString(36).substr(2, 8),
|
| 238 |
+
maxMembers: 10,
|
| 239 |
+
createdAt: new Date().toISOString(),
|
| 240 |
+
updatedAt: new Date().toISOString(),
|
| 241 |
+
};
|
| 242 |
+
res.status(201).json({ success: true, data: room });
|
| 243 |
+
});
|
| 244 |
+
|
| 245 |
+
app.post('/api/ai/action', (req, res) => {
|
| 246 |
+
res.json({ success: true, data: { content: 'AI service requires OPENROUTER_API_KEY configuration.', suggestions: [] } });
|
| 247 |
+
});
|
| 248 |
+
|
| 249 |
+
app.get('/api/snippets', (_req, res) => {
|
| 250 |
+
res.json({ success: true, data: [] });
|
| 251 |
+
});
|
| 252 |
+
|
| 253 |
+
// ─── Error Handler ───────────────────────────────────────────
|
| 254 |
+
app.use((_req, res) => {
|
| 255 |
+
res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'Endpoint not found' } });
|
| 256 |
+
});
|
| 257 |
+
|
| 258 |
+
// ─── Start Server ────────────────────────────────────────────
|
| 259 |
+
httpServer.listen(PORT, () => {
|
| 260 |
+
console.log(`
|
| 261 |
+
╔══════════════════════════════════════════════════╗
|
| 262 |
+
║ CodeSync Server Running ║
|
| 263 |
+
╠══════════════════════════════════════════════════╣
|
| 264 |
+
║ HTTP: http://localhost:${PORT} ║
|
| 265 |
+
║ WebSocket: ws://localhost:${PORT} ║
|
| 266 |
+
╚══════════════════════════════════════════════════╝
|
| 267 |
+
`);
|
| 268 |
+
});
|
| 269 |
+
|
| 270 |
+
export { app, httpServer, io };
|
apps/server/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"module": "NodeNext",
|
| 5 |
+
"moduleResolution": "NodeNext",
|
| 6 |
+
"lib": ["ES2022"],
|
| 7 |
+
"outDir": "./dist",
|
| 8 |
+
"rootDir": "./src",
|
| 9 |
+
"strict": true,
|
| 10 |
+
"esModuleInterop": true,
|
| 11 |
+
"skipLibCheck": true,
|
| 12 |
+
"forceConsistentCasingInFileNames": true,
|
| 13 |
+
"resolveJsonModule": true,
|
| 14 |
+
"declaration": true,
|
| 15 |
+
"sourceMap": true
|
| 16 |
+
},
|
| 17 |
+
"include": ["src/**/*"],
|
| 18 |
+
"exclude": ["node_modules", "dist"]
|
| 19 |
+
}
|
apps/web/next.config.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('next').NextConfig} */
|
| 2 |
+
const nextConfig = {
|
| 3 |
+
reactStrictMode: true,
|
| 4 |
+
// Enable standalone output for Docker deployment
|
| 5 |
+
output: 'standalone',
|
| 6 |
+
|
| 7 |
+
// Environment variables
|
| 8 |
+
env: {
|
| 9 |
+
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000',
|
| 10 |
+
},
|
| 11 |
+
|
| 12 |
+
// Monaco editor webpack config
|
| 13 |
+
webpack: (config, { isServer }) => {
|
| 14 |
+
if (!isServer) {
|
| 15 |
+
// Monaco editor workers
|
| 16 |
+
config.resolve.fallback = {
|
| 17 |
+
...config.resolve.fallback,
|
| 18 |
+
fs: false,
|
| 19 |
+
path: false,
|
| 20 |
+
};
|
| 21 |
+
}
|
| 22 |
+
return config;
|
| 23 |
+
},
|
| 24 |
+
|
| 25 |
+
// Image domains
|
| 26 |
+
images: {
|
| 27 |
+
remotePatterns: [
|
| 28 |
+
{ protocol: 'https', hostname: 'api.dicebear.com' },
|
| 29 |
+
{ protocol: 'https', hostname: 'lh3.googleusercontent.com' },
|
| 30 |
+
],
|
| 31 |
+
},
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
export default nextConfig;
|
apps/web/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@codesync/web",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "next lint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@monaco-editor/react": "^4.6.0",
|
| 13 |
+
"clsx": "^2.1.1",
|
| 14 |
+
"framer-motion": "^11.2.0",
|
| 15 |
+
"lucide-react": "^0.378.0",
|
| 16 |
+
"next": "14.2.3",
|
| 17 |
+
"react": "^18.3.1",
|
| 18 |
+
"react-dom": "^18.3.1",
|
| 19 |
+
"react-hot-toast": "^2.4.1",
|
| 20 |
+
"react-resizable-panels": "^2.0.19",
|
| 21 |
+
"socket.io-client": "^4.7.5",
|
| 22 |
+
"tailwind-merge": "^2.3.0",
|
| 23 |
+
"y-protocols": "^1.0.6",
|
| 24 |
+
"yjs": "^13.6.15",
|
| 25 |
+
"zustand": "^4.5.2"
|
| 26 |
+
},
|
| 27 |
+
"devDependencies": {
|
| 28 |
+
"@tailwindcss/typography": "^0.5.13",
|
| 29 |
+
"@types/node": "^20.12.0",
|
| 30 |
+
"@types/react": "^18.3.1",
|
| 31 |
+
"@types/react-dom": "^18.3.0",
|
| 32 |
+
"autoprefixer": "^10.4.19",
|
| 33 |
+
"postcss": "^8.4.38",
|
| 34 |
+
"tailwindcss": "^3.4.3",
|
| 35 |
+
"typescript": "^5.4.0"
|
| 36 |
+
}
|
| 37 |
+
}
|
apps/web/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
};
|
apps/web/src/app/(auth)/login/page.tsx
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ─────────────────────────────────────────────────────────────
|
| 2 |
+
// Login Page
|
| 3 |
+
// ─────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
'use client';
|
| 6 |
+
|
| 7 |
+
import { useState } from 'react';
|
| 8 |
+
import Link from 'next/link';
|
| 9 |
+
import { useRouter } from 'next/navigation';
|
| 10 |
+
import { Code2, Eye, EyeOff } from 'lucide-react';
|
| 11 |
+
import { authApi } from '../../../lib/api';
|
| 12 |
+
import { useAuthStore } from '../../../stores/authStore';
|
| 13 |
+
|
| 14 |
+
export default function LoginPage() {
|
| 15 |
+
const router = useRouter();
|
| 16 |
+
const { setAuth } = useAuthStore();
|
| 17 |
+
const [email, setEmail] = useState('');
|
| 18 |
+
const [password, setPassword] = useState('');
|
| 19 |
+
const [showPassword, setShowPassword] = useState(false);
|
| 20 |
+
const [error, setError] = useState('');
|
| 21 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 22 |
+
|
| 23 |
+
const handleSubmit = async (e: React.FormEvent) => {
|
| 24 |
+
e.preventDefault();
|
| 25 |
+
setError('');
|
| 26 |
+
setIsLoading(true);
|
| 27 |
+
|
| 28 |
+
try {
|
| 29 |
+
const data = await authApi.login({ email, password }) as any;
|
| 30 |
+
setAuth(data.user, data.tokens);
|
| 31 |
+
router.push('/rooms');
|
| 32 |
+
} catch (err: any) {
|
| 33 |
+
setError(err.message || 'Login failed');
|
| 34 |
+
} finally {
|
| 35 |
+
setIsLoading(false);
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<div className="flex min-h-screen items-center justify-center px-4">
|
| 41 |
+
<div className="w-full max-w-sm">
|
| 42 |
+
{/* Logo */}
|
| 43 |
+
<div className="mb-8 flex flex-col items-center">
|
| 44 |
+
<Code2 className="h-10 w-10 text-editor-accent mb-3" />
|
| 45 |
+
<h1 className="text-2xl font-bold">Welcome back</h1>
|
| 46 |
+
<p className="mt-1 text-sm text-editor-text-muted">Sign in to your CodeSync account</p>
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
{/* Form */}
|
| 50 |
+
<form onSubmit={handleSubmit} className="space-y-4">
|
| 51 |
+
{error && (
|
| 52 |
+
<div className="rounded-md bg-editor-error/10 border border-editor-error/30 px-3 py-2 text-sm text-editor-error">
|
| 53 |
+
{error}
|
| 54 |
+
</div>
|
| 55 |
+
)}
|
| 56 |
+
|
| 57 |
+
<div>
|
| 58 |
+
<label className="block text-sm font-medium mb-1.5">Email</label>
|
| 59 |
+
<input
|
| 60 |
+
type="email"
|
| 61 |
+
className="input-field w-full"
|
| 62 |
+
placeholder="you@example.com"
|
| 63 |
+
value={email}
|
| 64 |
+
onChange={(e) => setEmail(e.target.value)}
|
| 65 |
+
required
|
| 66 |
+
/>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<div>
|
| 70 |
+
<label className="block text-sm font-medium mb-1.5">Password</label>
|
| 71 |
+
<div className="relative">
|
| 72 |
+
<input
|
| 73 |
+
type={showPassword ? 'text' : 'password'}
|
| 74 |
+
className="input-field w-full pr-10"
|
| 75 |
+
placeholder="••••••••"
|
| 76 |
+
value={password}
|
| 77 |
+
onChange={(e) => setPassword(e.target.value)}
|
| 78 |
+
required
|
| 79 |
+
/>
|
| 80 |
+
<button
|
| 81 |
+
type="button"
|
| 82 |
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-editor-text-muted hover:text-editor-text"
|
| 83 |
+
onClick={() => setShowPassword(!showPassword)}
|
| 84 |
+
>
|
| 85 |
+
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
| 86 |
+
</button>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
<button
|
| 91 |
+
type="submit"
|
| 92 |
+
className="btn-primary w-full py-2.5"
|
| 93 |
+
disabled={isLoading}
|
| 94 |
+
>
|
| 95 |
+
{isLoading ? 'Signing in...' : 'Sign In'}
|
| 96 |
+
</button>
|
| 97 |
+
</form>
|
| 98 |
+
|
| 99 |
+
{/* Divider */}
|
| 100 |
+
<div className="my-6 flex items-center gap-3">
|
| 101 |
+
<div className="flex-1 border-t border-editor-border" />
|
| 102 |
+
<span className="text-xs text-editor-text-muted">or</span>
|
| 103 |
+
<div className="flex-1 border-t border-editor-border" />
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
{/* Google OAuth */}
|
| 107 |
+
<button className="btn-secondary w-full flex items-center justify-center gap-2">
|
| 108 |
+
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
| 109 |
+
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
| 110 |
+
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
| 111 |
+
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
| 112 |
+
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
| 113 |
+
</svg>
|
| 114 |
+
Continue with Google
|
| 115 |
+
</button>
|
| 116 |
+
|
| 117 |
+
{/* Sign up link */}
|
| 118 |
+
<p className="mt-6 text-center text-sm text-editor-text-muted">
|
| 119 |
+
Don't have an account?{' '}
|
| 120 |
+
<Link href="/register" className="text-editor-accent hover:underline">
|
| 121 |
+
Sign up
|
| 122 |
+
</Link>
|
| 123 |
+
</p>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
);
|
| 127 |
+
}
|
apps/web/src/app/(dashboard)/rooms/page.tsx
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ─────────────────────────────────────────────────────────────
|
| 2 |
+
// Dashboard — Rooms listing page
|
| 3 |
+
// ─────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
'use client';
|
| 6 |
+
|
| 7 |
+
import { useState, useEffect } from 'react';
|
| 8 |
+
import Link from 'next/link';
|
| 9 |
+
import { useRouter } from 'next/navigation';
|
| 10 |
+
import {
|
| 11 |
+
Plus, Code2, Users, Clock, Globe, Lock,
|
| 12 |
+
MoreVertical, Trash2, Settings, LogOut,
|
| 13 |
+
} from 'lucide-react';
|
| 14 |
+
import { roomsApi } from '../../../lib/api';
|
| 15 |
+
import { useAuthStore } from '../../../stores/authStore';
|
| 16 |
+
import { formatRelativeTime, cn } from '../../../lib/utils';
|
| 17 |
+
import type { Room } from '../../../types';
|
| 18 |
+
|
| 19 |
+
export default function RoomsPage() {
|
| 20 |
+
const router = useRouter();
|
| 21 |
+
const { user, isAuthenticated, logout } = useAuthStore();
|
| 22 |
+
const [rooms, setRooms] = useState<Room[]>([]);
|
| 23 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 24 |
+
const [showCreateModal, setShowCreateModal] = useState(false);
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
if (!isAuthenticated) {
|
| 28 |
+
router.push('/login');
|
| 29 |
+
return;
|
| 30 |
+
}
|
| 31 |
+
loadRooms();
|
| 32 |
+
}, [isAuthenticated]);
|
| 33 |
+
|
| 34 |
+
const loadRooms = async () => {
|
| 35 |
+
try {
|
| 36 |
+
const data = await roomsApi.list() as Room[];
|
| 37 |
+
setRooms(data);
|
| 38 |
+
} catch (error) {
|
| 39 |
+
console.error('Failed to load rooms:', error);
|
| 40 |
+
} finally {
|
| 41 |
+
setIsLoading(false);
|
| 42 |
+
}
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const handleCreateRoom = async (data: any) => {
|
| 46 |
+
try {
|
| 47 |
+
const room = await roomsApi.create(data) as Room;
|
| 48 |
+
setRooms([room, ...rooms]);
|
| 49 |
+
setShowCreateModal(false);
|
| 50 |
+
router.push(`/room/${room.id}`);
|
| 51 |
+
} catch (error) {
|
| 52 |
+
console.error('Failed to create room:', error);
|
| 53 |
+
}
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
return (
|
| 57 |
+
<div className="min-h-screen bg-editor-bg">
|
| 58 |
+
{/* Header */}
|
| 59 |
+
<header className="border-b border-editor-border bg-editor-surface">
|
| 60 |
+
<div className="flex items-center justify-between px-6 py-3 max-w-7xl mx-auto">
|
| 61 |
+
<div className="flex items-center gap-3">
|
| 62 |
+
<Code2 className="h-6 w-6 text-editor-accent" />
|
| 63 |
+
<span className="text-lg font-bold">CodeSync</span>
|
| 64 |
+
</div>
|
| 65 |
+
<div className="flex items-center gap-4">
|
| 66 |
+
<div className="flex items-center gap-2">
|
| 67 |
+
<div className="h-8 w-8 rounded-full bg-editor-accent flex items-center justify-center text-white text-sm font-bold">
|
| 68 |
+
{user?.name?.charAt(0).toUpperCase()}
|
| 69 |
+
</div>
|
| 70 |
+
<span className="text-sm">{user?.name}</span>
|
| 71 |
+
</div>
|
| 72 |
+
<button
|
| 73 |
+
onClick={() => { logout(); router.push('/login'); }}
|
| 74 |
+
className="text-editor-text-muted hover:text-editor-text p-1"
|
| 75 |
+
title="Sign out"
|
| 76 |
+
>
|
| 77 |
+
<LogOut className="h-4 w-4" />
|
| 78 |
+
</button>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</header>
|
| 82 |
+
|
| 83 |
+
{/* Content */}
|
| 84 |
+
<main className="max-w-7xl mx-auto px-6 py-8">
|
| 85 |
+
<div className="flex items-center justify-between mb-8">
|
| 86 |
+
<div>
|
| 87 |
+
<h1 className="text-2xl font-bold">Your Rooms</h1>
|
| 88 |
+
<p className="text-sm text-editor-text-muted mt-1">
|
| 89 |
+
Create or join coding rooms to collaborate in real-time
|
| 90 |
+
</p>
|
| 91 |
+
</div>
|
| 92 |
+
<button
|
| 93 |
+
className="btn-primary flex items-center gap-2"
|
| 94 |
+
onClick={() => setShowCreateModal(true)}
|
| 95 |
+
>
|
| 96 |
+
<Plus className="h-4 w-4" />
|
| 97 |
+
New Room
|
| 98 |
+
</button>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
{/* Rooms Grid */}
|
| 102 |
+
{isLoading ? (
|
| 103 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 104 |
+
{[1, 2, 3].map((i) => (
|
| 105 |
+
<div key={i} className="rounded-lg border border-editor-border bg-editor-surface p-5 animate-pulse">
|
| 106 |
+
<div className="h-4 bg-editor-hover rounded w-3/4 mb-3" />
|
| 107 |
+
<div className="h-3 bg-editor-hover rounded w-1/2" />
|
| 108 |
+
</div>
|
| 109 |
+
))}
|
| 110 |
+
</div>
|
| 111 |
+
) : rooms.length === 0 ? (
|
| 112 |
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
| 113 |
+
<Code2 className="h-12 w-12 text-editor-text-muted mb-4 opacity-50" />
|
| 114 |
+
<h3 className="text-lg font-medium mb-1">No rooms yet</h3>
|
| 115 |
+
<p className="text-sm text-editor-text-muted mb-6">
|
| 116 |
+
Create your first room to start collaborating
|
| 117 |
+
</p>
|
| 118 |
+
<button className="btn-primary" onClick={() => setShowCreateModal(true)}>
|
| 119 |
+
Create Your First Room
|
| 120 |
+
</button>
|
| 121 |
+
</div>
|
| 122 |
+
) : (
|
| 123 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 124 |
+
{rooms.map((room) => (
|
| 125 |
+
<RoomCard key={room.id} room={room} />
|
| 126 |
+
))}
|
| 127 |
+
</div>
|
| 128 |
+
)}
|
| 129 |
+
</main>
|
| 130 |
+
|
| 131 |
+
{/* Create Room Modal */}
|
| 132 |
+
{showCreateModal && (
|
| 133 |
+
<CreateRoomModal
|
| 134 |
+
onClose={() => setShowCreateModal(false)}
|
| 135 |
+
onCreate={handleCreateRoom}
|
| 136 |
+
/>
|
| 137 |
+
)}
|
| 138 |
+
</div>
|
| 139 |
+
);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// ─── Room Card ───────────────────────────────────────────────
|
| 143 |
+
|
| 144 |
+
function RoomCard({ room }: { room: Room }) {
|
| 145 |
+
return (
|
| 146 |
+
<Link
|
| 147 |
+
href={`/room/${room.id}`}
|
| 148 |
+
className="group rounded-lg border border-editor-border bg-editor-surface p-5 hover:border-editor-accent/50 transition-all hover:shadow-lg hover:shadow-editor-accent/5"
|
| 149 |
+
>
|
| 150 |
+
<div className="flex items-start justify-between">
|
| 151 |
+
<h3 className="font-semibold group-hover:text-editor-accent transition-colors">
|
| 152 |
+
{room.name}
|
| 153 |
+
</h3>
|
| 154 |
+
{room.isPublic ? (
|
| 155 |
+
<Globe className="h-4 w-4 text-editor-text-muted" />
|
| 156 |
+
) : (
|
| 157 |
+
<Lock className="h-4 w-4 text-editor-text-muted" />
|
| 158 |
+
)}
|
| 159 |
+
</div>
|
| 160 |
+
<div className="mt-3 flex items-center gap-3 text-xs text-editor-text-muted">
|
| 161 |
+
<span className="flex items-center gap-1">
|
| 162 |
+
<Code2 className="h-3 w-3" />
|
| 163 |
+
{room.language}
|
| 164 |
+
</span>
|
| 165 |
+
<span className="flex items-center gap-1">
|
| 166 |
+
<Clock className="h-3 w-3" />
|
| 167 |
+
{formatRelativeTime(room.updatedAt)}
|
| 168 |
+
</span>
|
| 169 |
+
</div>
|
| 170 |
+
</Link>
|
| 171 |
+
);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// ─── Create Room Modal ───────────────────────────────────────
|
| 175 |
+
|
| 176 |
+
function CreateRoomModal({ onClose, onCreate }: { onClose: () => void; onCreate: (data: any) => void }) {
|
| 177 |
+
const [name, setName] = useState('');
|
| 178 |
+
const [language, setLanguage] = useState('javascript');
|
| 179 |
+
const [isPublic, setIsPublic] = useState(false);
|
| 180 |
+
|
| 181 |
+
const handleSubmit = (e: React.FormEvent) => {
|
| 182 |
+
e.preventDefault();
|
| 183 |
+
if (!name.trim()) return;
|
| 184 |
+
onCreate({ name, language, isPublic });
|
| 185 |
+
};
|
| 186 |
+
|
| 187 |
+
return (
|
| 188 |
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={onClose}>
|
| 189 |
+
<div className="w-full max-w-md rounded-lg border border-editor-border bg-editor-surface p-6 shadow-xl" onClick={(e) => e.stopPropagation()}>
|
| 190 |
+
<h2 className="text-lg font-bold mb-4">Create New Room</h2>
|
| 191 |
+
<form onSubmit={handleSubmit} className="space-y-4">
|
| 192 |
+
<div>
|
| 193 |
+
<label className="block text-sm font-medium mb-1.5">Room Name</label>
|
| 194 |
+
<input
|
| 195 |
+
type="text"
|
| 196 |
+
className="input-field w-full"
|
| 197 |
+
placeholder="My Awesome Project"
|
| 198 |
+
value={name}
|
| 199 |
+
onChange={(e) => setName(e.target.value)}
|
| 200 |
+
required
|
| 201 |
+
autoFocus
|
| 202 |
+
/>
|
| 203 |
+
</div>
|
| 204 |
+
<div>
|
| 205 |
+
<label className="block text-sm font-medium mb-1.5">Language</label>
|
| 206 |
+
<select
|
| 207 |
+
className="input-field w-full"
|
| 208 |
+
value={language}
|
| 209 |
+
onChange={(e) => setLanguage(e.target.value)}
|
| 210 |
+
>
|
| 211 |
+
<option value="javascript">JavaScript</option>
|
| 212 |
+
<option value="typescript">TypeScript</option>
|
| 213 |
+
<option value="python">Python</option>
|
| 214 |
+
<option value="cpp">C++</option>
|
| 215 |
+
<option value="java">Java</option>
|
| 216 |
+
</select>
|
| 217 |
+
</div>
|
| 218 |
+
<div className="flex items-center gap-2">
|
| 219 |
+
<input
|
| 220 |
+
type="checkbox"
|
| 221 |
+
id="isPublic"
|
| 222 |
+
checked={isPublic}
|
| 223 |
+
onChange={(e) => setIsPublic(e.target.checked)}
|
| 224 |
+
className="rounded border-editor-border"
|
| 225 |
+
/>
|
| 226 |
+
<label htmlFor="isPublic" className="text-sm">Make room public</label>
|
| 227 |
+
</div>
|
| 228 |
+
<div className="flex justify-end gap-3 pt-2">
|
| 229 |
+
<button type="button" className="btn-secondary" onClick={onClose}>Cancel</button>
|
| 230 |
+
<button type="submit" className="btn-primary">Create Room</button>
|
| 231 |
+
</div>
|
| 232 |
+
</form>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
);
|
| 236 |
+
}
|
apps/web/src/app/layout.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from 'next';
|
| 2 |
+
import { Inter, JetBrains_Mono } from 'next/font/google';
|
| 3 |
+
import '../styles/globals.css';
|
| 4 |
+
|
| 5 |
+
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
|
| 6 |
+
const jetbrains = JetBrains_Mono({ subsets: ['latin'], variable: '--font-mono' });
|
| 7 |
+
|
| 8 |
+
export const metadata: Metadata = {
|
| 9 |
+
title: 'CodeSync — Real-Time Collaborative Coding',
|
| 10 |
+
description: 'A production-grade real-time collaborative coding platform with AI assistance, video calls, and sandboxed code execution.',
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
| 14 |
+
return (
|
| 15 |
+
<html lang="en" className="dark">
|
| 16 |
+
<body className={`${inter.variable} ${jetbrains.variable} font-sans antialiased bg-editor-bg text-editor-text`}>
|
| 17 |
+
{children}
|
| 18 |
+
</body>
|
| 19 |
+
</html>
|
| 20 |
+
);
|
| 21 |
+
}
|
apps/web/src/app/page.tsx
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ─────────────────────────────────────────────────────────────
|
| 2 |
+
// Landing Page — Hero + Features showcase
|
| 3 |
+
// ─────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
'use client';
|
| 6 |
+
|
| 7 |
+
import Link from 'next/link';
|
| 8 |
+
import { Code2, Users, Zap, Shield, Video, Bot, Terminal, Globe } from 'lucide-react';
|
| 9 |
+
|
| 10 |
+
const features = [
|
| 11 |
+
{ icon: Users, title: 'Real-Time Collaboration', desc: 'Multiple users editing simultaneously with CRDT-based sync' },
|
| 12 |
+
{ icon: Zap, title: 'Low Latency', desc: 'WebSocket-powered updates with optimistic UI rendering' },
|
| 13 |
+
{ icon: Terminal, title: 'Code Execution', desc: 'Run code in isolated Docker containers with streaming output' },
|
| 14 |
+
{ icon: Bot, title: 'AI Assistant', desc: 'Integrated AI for debugging, optimization, and code explanation' },
|
| 15 |
+
{ icon: Video, title: 'Video Calls', desc: 'WebRTC peer-to-peer video/audio with screen sharing' },
|
| 16 |
+
{ icon: Shield, title: 'Secure Sandbox', desc: 'Isolated execution with resource limits and network isolation' },
|
| 17 |
+
{ icon: Code2, title: 'Monaco Editor', desc: 'VS Code-grade editing with IntelliSense and multi-language support' },
|
| 18 |
+
{ icon: Globe, title: 'Scalable', desc: 'Redis pub/sub for horizontal scaling across multiple instances' },
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
export default function HomePage() {
|
| 22 |
+
return (
|
| 23 |
+
<div className="min-h-screen bg-gradient-to-b from-editor-bg via-editor-surface to-editor-bg">
|
| 24 |
+
{/* Nav */}
|
| 25 |
+
<nav className="flex items-center justify-between px-6 py-4 max-w-7xl mx-auto">
|
| 26 |
+
<div className="flex items-center gap-2">
|
| 27 |
+
<Code2 className="h-6 w-6 text-editor-accent" />
|
| 28 |
+
<span className="text-lg font-bold">CodeSync</span>
|
| 29 |
+
</div>
|
| 30 |
+
<div className="flex items-center gap-4">
|
| 31 |
+
<Link href="/login" className="text-sm text-editor-text-muted hover:text-editor-text transition-colors">
|
| 32 |
+
Sign In
|
| 33 |
+
</Link>
|
| 34 |
+
<Link href="/register" className="btn-primary text-sm">
|
| 35 |
+
Get Started
|
| 36 |
+
</Link>
|
| 37 |
+
</div>
|
| 38 |
+
</nav>
|
| 39 |
+
|
| 40 |
+
{/* Hero */}
|
| 41 |
+
<section className="flex flex-col items-center justify-center px-6 py-24 text-center max-w-4xl mx-auto">
|
| 42 |
+
<div className="mb-4 inline-flex items-center gap-2 rounded-full border border-editor-border px-4 py-1.5 text-xs text-editor-text-muted">
|
| 43 |
+
<span className="h-2 w-2 rounded-full bg-editor-success animate-pulse" />
|
| 44 |
+
Real-time collaborative coding platform
|
| 45 |
+
</div>
|
| 46 |
+
|
| 47 |
+
<h1 className="text-5xl font-bold leading-tight tracking-tight md:text-6xl">
|
| 48 |
+
Code Together,{' '}
|
| 49 |
+
<span className="bg-gradient-to-r from-editor-accent to-editor-success bg-clip-text text-transparent">
|
| 50 |
+
Build Faster
|
| 51 |
+
</span>
|
| 52 |
+
</h1>
|
| 53 |
+
|
| 54 |
+
<p className="mt-6 max-w-2xl text-lg text-editor-text-muted leading-relaxed">
|
| 55 |
+
A production-grade collaborative IDE with real-time editing, AI-powered debugging,
|
| 56 |
+
video collaboration, and secure sandboxed code execution. Like VS Code, but multiplayer.
|
| 57 |
+
</p>
|
| 58 |
+
|
| 59 |
+
<div className="mt-10 flex items-center gap-4">
|
| 60 |
+
<Link href="/register" className="btn-primary px-6 py-3 text-base">
|
| 61 |
+
Start Coding Free →
|
| 62 |
+
</Link>
|
| 63 |
+
<Link href="/login" className="btn-secondary px-6 py-3 text-base">
|
| 64 |
+
View Demo
|
| 65 |
+
</Link>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
{/* Code preview mockup */}
|
| 69 |
+
<div className="mt-16 w-full max-w-3xl rounded-xl border border-editor-border bg-editor-surface shadow-2xl overflow-hidden">
|
| 70 |
+
<div className="flex items-center gap-2 border-b border-editor-border px-4 py-2">
|
| 71 |
+
<span className="h-3 w-3 rounded-full bg-red-500" />
|
| 72 |
+
<span className="h-3 w-3 rounded-full bg-yellow-500" />
|
| 73 |
+
<span className="h-3 w-3 rounded-full bg-green-500" />
|
| 74 |
+
<span className="ml-4 text-xs text-editor-text-muted">main.ts — CodeSync</span>
|
| 75 |
+
</div>
|
| 76 |
+
<div className="p-4 font-mono text-sm text-left">
|
| 77 |
+
<pre className="text-editor-text-muted">
|
| 78 |
+
{` 1 │ `}<span className="text-purple-400">import</span>{` { collaborate } `}<span className="text-purple-400">from</span>{` `}<span className="text-green-400">'codesync'</span>{`;
|
| 79 |
+
2 │
|
| 80 |
+
3 │ `}<span className="text-purple-400">const</span>{` room = `}<span className="text-purple-400">await</span>{` collaborate.`}<span className="text-yellow-300">createRoom</span>{`({
|
| 81 |
+
4 │ name: `}<span className="text-green-400">'Project Alpha'</span>{`,
|
| 82 |
+
5 │ language: `}<span className="text-green-400">'typescript'</span>{`,
|
| 83 |
+
6 │ members: [`}<span className="text-green-400">'alice'</span>{`, `}<span className="text-green-400">'bob'</span>{`],
|
| 84 |
+
7 │ });
|
| 85 |
+
8 │
|
| 86 |
+
9 │ room.`}<span className="text-yellow-300">on</span>{`(`}<span className="text-green-400">'update'</span>{`, (delta) => {
|
| 87 |
+
10 │ `}<span className="text-editor-text-muted">// CRDT ensures conflict-free merging</span>{`
|
| 88 |
+
11 │ editor.`}<span className="text-yellow-300">applyDelta</span>{`(delta);
|
| 89 |
+
12 │ });`}
|
| 90 |
+
</pre>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</section>
|
| 94 |
+
|
| 95 |
+
{/* Features Grid */}
|
| 96 |
+
<section className="max-w-6xl mx-auto px-6 py-24">
|
| 97 |
+
<h2 className="text-center text-3xl font-bold mb-4">Everything You Need</h2>
|
| 98 |
+
<p className="text-center text-editor-text-muted mb-12 max-w-2xl mx-auto">
|
| 99 |
+
Built with production-grade architecture, real-time synchronization, and modern developer tooling.
|
| 100 |
+
</p>
|
| 101 |
+
|
| 102 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
| 103 |
+
{features.map((feature) => (
|
| 104 |
+
<div
|
| 105 |
+
key={feature.title}
|
| 106 |
+
className="rounded-lg border border-editor-border bg-editor-surface p-5 hover:border-editor-accent/50 transition-colors group"
|
| 107 |
+
>
|
| 108 |
+
<feature.icon className="h-8 w-8 text-editor-accent mb-3 group-hover:scale-110 transition-transform" />
|
| 109 |
+
<h3 className="font-semibold text-sm mb-1">{feature.title}</h3>
|
| 110 |
+
<p className="text-xs text-editor-text-muted leading-relaxed">{feature.desc}</p>
|
| 111 |
+
</div>
|
| 112 |
+
))}
|
| 113 |
+
</div>
|
| 114 |
+
</section>
|
| 115 |
+
|
| 116 |
+
{/* Footer */}
|
| 117 |
+
<footer className="border-t border-editor-border py-8 text-center text-xs text-editor-text-muted">
|
| 118 |
+
<p>© 2024 CodeSync. Built with Next.js, Socket.io, Yjs, and WebRTC.</p>
|
| 119 |
+
</footer>
|
| 120 |
+
</div>
|
| 121 |
+
);
|
| 122 |
+
}
|
apps/web/src/app/room/[id]/page.tsx
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState, useCallback } from 'react';
|
| 4 |
+
import { useParams } from 'next/navigation';
|
| 5 |
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
| 6 |
+
import { MessageSquare, Bot, Video, Terminal, Files, Settings, Share2, Copy, Check, Menu, Users, Play } from 'lucide-react';
|
| 7 |
+
import { useSocket } from '../../../hooks/useSocket';
|
| 8 |
+
import { useEditorStore } from '../../../stores/editorStore';
|
| 9 |
+
import { useRoomStore } from '../../../stores/roomStore';
|
| 10 |
+
import { useChatStore } from '../../../stores/chatStore';
|
| 11 |
+
import { useAuthStore } from '../../../stores/authStore';
|
| 12 |
+
import { cn, copyToClipboard, generateInviteLink } from '../../../lib/utils';
|
| 13 |
+
|
| 14 |
+
type SidePanel = 'chat' | 'ai' | 'video' | null;
|
| 15 |
+
|
| 16 |
+
export default function RoomPage() {
|
| 17 |
+
const params = useParams();
|
| 18 |
+
const roomId = params.id as string;
|
| 19 |
+
const { socket, emit, on } = useSocket();
|
| 20 |
+
const { user } = useAuthStore();
|
| 21 |
+
const { currentRoom, isConnected, setRoom, setMembers, setPresence, presence, addMember, removeMember } = useRoomStore();
|
| 22 |
+
const { files, activeFileId, setFiles, openTab, tabs, setActiveFile } = useEditorStore();
|
| 23 |
+
const { addMessage, setTypingUser, setMessages } = useChatStore();
|
| 24 |
+
const [sidePanel, setSidePanel] = useState<SidePanel>(null);
|
| 25 |
+
const [showOutput, setShowOutput] = useState(true);
|
| 26 |
+
const [showSidebar, setShowSidebar] = useState(true);
|
| 27 |
+
const [copied, setCopied] = useState(false);
|
| 28 |
+
|
| 29 |
+
useEffect(() => {
|
| 30 |
+
if (!socket || !roomId) return;
|
| 31 |
+
emit('room:join', { roomId });
|
| 32 |
+
|
| 33 |
+
const unsubs = [
|
| 34 |
+
on('room:joined', (data: any) => { setRoom(data.room); setMembers(data.members || []); setFiles(data.files || []); if (data.files?.length) openTab(data.files[0]); }),
|
| 35 |
+
on('room:member-joined', (m: any) => addMember(m)),
|
| 36 |
+
on('room:member-left', (d: any) => removeMember(d.userId)),
|
| 37 |
+
on('presence:update', (p: any) => setPresence(p)),
|
| 38 |
+
on('chat:message', (m: any) => addMessage(m)),
|
| 39 |
+
on('chat:history', (msgs: any) => setMessages(msgs)),
|
| 40 |
+
on('chat:typing', (d: any) => setTypingUser(d.userId, d.userName, d.isTyping)),
|
| 41 |
+
];
|
| 42 |
+
|
| 43 |
+
emit('chat:history', { limit: 50 });
|
| 44 |
+
return () => { emit('room:leave', { roomId }); unsubs.forEach((u) => u?.()); };
|
| 45 |
+
}, [socket, roomId]);
|
| 46 |
+
|
| 47 |
+
const handleCopyInvite = async () => {
|
| 48 |
+
if (currentRoom?.inviteCode) { await copyToClipboard(generateInviteLink(currentRoom.inviteCode)); setCopied(true); setTimeout(() => setCopied(false), 2000); }
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
const activeFile = files.find((f) => f.id === activeFileId);
|
| 52 |
+
|
| 53 |
+
return (
|
| 54 |
+
<div className="flex h-screen w-screen flex-col overflow-hidden bg-editor-bg text-editor-text">
|
| 55 |
+
{/* Top Bar */}
|
| 56 |
+
<header className="flex h-10 items-center justify-between border-b border-editor-border bg-editor-surface px-3">
|
| 57 |
+
<div className="flex items-center gap-3">
|
| 58 |
+
<button className="rounded p-1 hover:bg-editor-hover lg:hidden" onClick={() => setShowSidebar(!showSidebar)}><Menu className="h-4 w-4" /></button>
|
| 59 |
+
<h1 className="text-sm font-semibold">{currentRoom?.name || 'Loading...'}</h1>
|
| 60 |
+
{!isConnected && <span className="rounded bg-editor-error/20 px-2 py-0.5 text-[10px] text-editor-error">Disconnected</span>}
|
| 61 |
+
</div>
|
| 62 |
+
<div className="flex items-center gap-2">
|
| 63 |
+
<div className="flex items-center gap-1">
|
| 64 |
+
<Users className="h-3.5 w-3.5 text-editor-text-muted" />
|
| 65 |
+
<div className="flex -space-x-1.5">
|
| 66 |
+
{presence.slice(0, 5).map((u) => (
|
| 67 |
+
<div key={u.userId} className="h-6 w-6 rounded-full border-2 border-editor-surface flex items-center justify-center text-[10px] font-bold text-white" style={{ backgroundColor: u.userColor }}>
|
| 68 |
+
{u.userName?.charAt(0)?.toUpperCase()}
|
| 69 |
+
</div>
|
| 70 |
+
))}
|
| 71 |
+
</div>
|
| 72 |
+
<span className="text-[11px] text-editor-text-muted ml-1">{presence.length} online</span>
|
| 73 |
+
</div>
|
| 74 |
+
<div className="mx-2 h-4 w-px bg-editor-border" />
|
| 75 |
+
<button className="flex items-center gap-1.5 rounded-md bg-editor-accent/10 px-2.5 py-1 text-xs text-editor-accent hover:bg-editor-accent/20" onClick={handleCopyInvite}>
|
| 76 |
+
{copied ? <Check className="h-3.5 w-3.5" /> : <Share2 className="h-3.5 w-3.5" />}
|
| 77 |
+
{copied ? 'Copied!' : 'Invite'}
|
| 78 |
+
</button>
|
| 79 |
+
</div>
|
| 80 |
+
</header>
|
| 81 |
+
|
| 82 |
+
{/* Main Content */}
|
| 83 |
+
<div className="flex flex-1 overflow-hidden">
|
| 84 |
+
{/* Activity Bar */}
|
| 85 |
+
<div className="flex w-12 flex-col items-center gap-1 border-r border-editor-border bg-sidebar-bg py-2">
|
| 86 |
+
<ActivityBtn icon={Files} active={showSidebar} onClick={() => setShowSidebar(!showSidebar)} />
|
| 87 |
+
<ActivityBtn icon={MessageSquare} active={sidePanel === 'chat'} onClick={() => setSidePanel(sidePanel === 'chat' ? null : 'chat')} />
|
| 88 |
+
<ActivityBtn icon={Bot} active={sidePanel === 'ai'} onClick={() => setSidePanel(sidePanel === 'ai' ? null : 'ai')} />
|
| 89 |
+
<ActivityBtn icon={Video} active={sidePanel === 'video'} onClick={() => setSidePanel(sidePanel === 'video' ? null : 'video')} />
|
| 90 |
+
<div className="flex-1" />
|
| 91 |
+
<ActivityBtn icon={Terminal} active={showOutput} onClick={() => setShowOutput(!showOutput)} />
|
| 92 |
+
<ActivityBtn icon={Settings} active={false} onClick={() => {}} />
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
{/* Panels */}
|
| 96 |
+
<PanelGroup direction="horizontal" className="flex-1">
|
| 97 |
+
{showSidebar && (<><Panel defaultSize={15} minSize={10} maxSize={30}>
|
| 98 |
+
<div className="h-full bg-sidebar-bg">
|
| 99 |
+
<div className="flex items-center justify-between border-b border-editor-border px-3 py-2">
|
| 100 |
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-editor-text-muted">Explorer</span>
|
| 101 |
+
</div>
|
| 102 |
+
<div className="py-1">
|
| 103 |
+
{files.map((file) => (
|
| 104 |
+
<button key={file.id} className={cn('flex w-full items-center gap-2 px-4 py-1 text-left text-[13px]', file.id === activeFileId ? 'bg-editor-active text-editor-text' : 'text-editor-text-muted hover:bg-sidebar-hover')} onClick={() => openTab(file)}>
|
| 105 |
+
<span className="text-sm">📄</span><span className="truncate">{file.name}</span>
|
| 106 |
+
</button>
|
| 107 |
+
))}
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
</Panel><PanelResizeHandle /></>)}
|
| 111 |
+
|
| 112 |
+
<Panel defaultSize={sidePanel ? 55 : 85} minSize={40}>
|
| 113 |
+
<PanelGroup direction="vertical">
|
| 114 |
+
<Panel defaultSize={showOutput ? 65 : 100} minSize={30}>
|
| 115 |
+
<div className="flex h-full flex-col">
|
| 116 |
+
{/* Tabs */}
|
| 117 |
+
<div className="flex h-9 items-center overflow-x-auto border-b border-editor-border bg-editor-surface">
|
| 118 |
+
{tabs.map((tab) => (
|
| 119 |
+
<div key={tab.fileId} className={cn('flex h-full items-center gap-1.5 border-r border-editor-border px-3 text-xs cursor-pointer', tab.isActive ? 'bg-editor-bg text-editor-text border-t-2 border-t-editor-accent' : 'bg-editor-surface text-editor-text-muted hover:bg-editor-hover')} onClick={() => setActiveFile(tab.fileId)}>
|
| 120 |
+
<span className="text-sm">📄</span><span>{tab.name}</span>
|
| 121 |
+
</div>
|
| 122 |
+
))}
|
| 123 |
+
</div>
|
| 124 |
+
{/* Editor placeholder — Monaco loads dynamically */}
|
| 125 |
+
<div className="flex-1 flex items-center justify-center bg-editor-bg">
|
| 126 |
+
{activeFile ? (
|
| 127 |
+
<div className="w-full h-full p-4 font-mono text-sm overflow-auto">
|
| 128 |
+
<p className="text-editor-text-muted mb-2">// Monaco Editor loads here</p>
|
| 129 |
+
<p className="text-editor-text-muted">// File: {activeFile.name}</p>
|
| 130 |
+
<p className="text-editor-text-muted">// Language: {activeFile.language}</p>
|
| 131 |
+
<pre className="mt-4 text-editor-text whitespace-pre-wrap">{activeFile.content}</pre>
|
| 132 |
+
</div>
|
| 133 |
+
) : (
|
| 134 |
+
<div className="text-center"><p className="text-lg text-editor-text-muted">No file open</p><p className="mt-1 text-sm text-editor-text-muted">Select a file from the explorer</p></div>
|
| 135 |
+
)}
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
</Panel>
|
| 139 |
+
{showOutput && (<><PanelResizeHandle /><Panel defaultSize={35} minSize={15} maxSize={60}>
|
| 140 |
+
<div className="h-full bg-editor-bg border-t border-editor-border">
|
| 141 |
+
<div className="flex items-center justify-between border-b border-editor-border px-3 py-1.5">
|
| 142 |
+
<div className="flex items-center gap-2"><Terminal className="h-3.5 w-3.5 text-editor-text-muted" /><span className="text-xs font-medium text-editor-text">Output</span></div>
|
| 143 |
+
<button className="flex items-center gap-1 rounded bg-editor-success px-2 py-0.5 text-[11px] text-editor-bg font-medium hover:opacity-90"><Play className="h-3 w-3" /> Run</button>
|
| 144 |
+
</div>
|
| 145 |
+
<div className="p-3 font-mono text-xs text-editor-text-muted">Click "Run" to execute your code.</div>
|
| 146 |
+
</div>
|
| 147 |
+
</Panel></>)}
|
| 148 |
+
</PanelGroup>
|
| 149 |
+
</Panel>
|
| 150 |
+
|
| 151 |
+
{sidePanel && (<><PanelResizeHandle /><Panel defaultSize={30} minSize={20} maxSize={45}>
|
| 152 |
+
<div className="h-full bg-editor-surface flex flex-col">
|
| 153 |
+
<div className="flex items-center justify-between border-b border-editor-border px-3 py-2">
|
| 154 |
+
<h3 className="text-sm font-medium text-editor-text capitalize">{sidePanel}</h3>
|
| 155 |
+
<button className="rounded p-1 text-editor-text-muted hover:bg-editor-hover" onClick={() => setSidePanel(null)}>✕</button>
|
| 156 |
+
</div>
|
| 157 |
+
<div className="flex-1 flex items-center justify-center text-sm text-editor-text-muted">
|
| 158 |
+
{sidePanel === 'chat' && 'Chat panel — send messages in real-time'}
|
| 159 |
+
{sidePanel === 'ai' && 'AI Assistant — ask about your code'}
|
| 160 |
+
{sidePanel === 'video' && 'Video Call — WebRTC peer-to-peer'}
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
</Panel></>)}
|
| 164 |
+
</PanelGroup>
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
+
{/* Status Bar */}
|
| 168 |
+
<footer className="flex h-6 items-center justify-between border-t border-editor-border bg-editor-accent px-3 text-[11px] text-white">
|
| 169 |
+
<div className="flex items-center gap-3">
|
| 170 |
+
<span className="flex items-center gap-1"><span className={cn('h-2 w-2 rounded-full', isConnected ? 'bg-green-300' : 'bg-red-300')} />{isConnected ? 'Connected' : 'Disconnected'}</span>
|
| 171 |
+
<span>{currentRoom?.language || 'javascript'}</span>
|
| 172 |
+
</div>
|
| 173 |
+
<div className="flex items-center gap-3"><span>UTF-8</span><span>Spaces: 2</span><span>CodeSync v1.0</span></div>
|
| 174 |
+
</footer>
|
| 175 |
+
</div>
|
| 176 |
+
);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
function ActivityBtn({ icon: Icon, active, onClick }: { icon: any; active: boolean; onClick: () => void }) {
|
| 180 |
+
return (
|
| 181 |
+
<button className={cn('flex h-10 w-10 items-center justify-center rounded transition-colors', active ? 'text-editor-text border-l-2 border-editor-accent bg-editor-active/30' : 'text-editor-text-muted hover:text-editor-text')} onClick={onClick}>
|
| 182 |
+
<Icon className="h-5 w-5" />
|
| 183 |
+
</button>
|
| 184 |
+
);
|
| 185 |
+
}
|
apps/web/src/components/ui/index.tsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ─────────────────────────────────────────────────────────────
|
| 2 |
+
// UI Components — Reusable UI primitives
|
| 3 |
+
// ─────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
'use client';
|
| 6 |
+
|
| 7 |
+
import { forwardRef, type ButtonHTMLAttributes, type InputHTMLAttributes } from 'react';
|
| 8 |
+
import { cn } from '../../lib/utils';
|
| 9 |
+
|
| 10 |
+
// ─── Button ──────────────────────────────────────────────────
|
| 11 |
+
|
| 12 |
+
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
| 13 |
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
| 14 |
+
size?: 'sm' | 'md' | 'lg';
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
| 18 |
+
({ className, variant = 'primary', size = 'md', ...props }, ref) => {
|
| 19 |
+
const variants = {
|
| 20 |
+
primary: 'bg-editor-accent hover:bg-editor-accent-hover text-white',
|
| 21 |
+
secondary: 'bg-editor-surface hover:bg-editor-hover text-editor-text border border-editor-border',
|
| 22 |
+
ghost: 'hover:bg-editor-hover text-editor-text-muted hover:text-editor-text',
|
| 23 |
+
danger: 'bg-editor-error hover:bg-red-600 text-white',
|
| 24 |
+
};
|
| 25 |
+
const sizes = {
|
| 26 |
+
sm: 'px-2.5 py-1 text-xs',
|
| 27 |
+
md: 'px-4 py-2 text-sm',
|
| 28 |
+
lg: 'px-6 py-2.5 text-base',
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
return (
|
| 32 |
+
<button
|
| 33 |
+
ref={ref}
|
| 34 |
+
className={cn(
|
| 35 |
+
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-ring disabled:opacity-50 disabled:pointer-events-none',
|
| 36 |
+
variants[variant],
|
| 37 |
+
sizes[size],
|
| 38 |
+
className
|
| 39 |
+
)}
|
| 40 |
+
{...props}
|
| 41 |
+
/>
|
| 42 |
+
);
|
| 43 |
+
}
|
| 44 |
+
);
|
| 45 |
+
Button.displayName = 'Button';
|
| 46 |
+
|
| 47 |
+
// ─── Input ───────────────────────────────────────────────────
|
| 48 |
+
|
| 49 |
+
export const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(
|
| 50 |
+
({ className, ...props }, ref) => (
|
| 51 |
+
<input
|
| 52 |
+
ref={ref}
|
| 53 |
+
className={cn('input-field w-full', className)}
|
| 54 |
+
{...props}
|
| 55 |
+
/>
|
| 56 |
+
)
|
| 57 |
+
);
|
| 58 |
+
Input.displayName = 'Input';
|
| 59 |
+
|
| 60 |
+
// ─── Skeleton ────────────────────────────────────────────────
|
| 61 |
+
|
| 62 |
+
export function Skeleton({ className }: { className?: string }) {
|
| 63 |
+
return (
|
| 64 |
+
<div className={cn('animate-pulse rounded bg-editor-hover', className)} />
|
| 65 |
+
);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// ─── Badge ───────────────────────────────────────────────────
|
| 69 |
+
|
| 70 |
+
interface BadgeProps {
|
| 71 |
+
children: React.ReactNode;
|
| 72 |
+
variant?: 'default' | 'success' | 'warning' | 'error';
|
| 73 |
+
className?: string;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
export function Badge({ children, variant = 'default', className }: BadgeProps) {
|
| 77 |
+
const variants = {
|
| 78 |
+
default: 'bg-editor-hover text-editor-text-muted',
|
| 79 |
+
success: 'bg-editor-success/10 text-editor-success',
|
| 80 |
+
warning: 'bg-editor-warning/10 text-editor-warning',
|
| 81 |
+
error: 'bg-editor-error/10 text-editor-error',
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
return (
|
| 85 |
+
<span
|
| 86 |
+
className={cn(
|
| 87 |
+
'inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium',
|
| 88 |
+
variants[variant],
|
| 89 |
+
className
|
| 90 |
+
)}
|
| 91 |
+
>
|
| 92 |
+
{children}
|
| 93 |
+
</span>
|
| 94 |
+
);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// ─── Modal ───────────────────────────────────────────────────
|
| 98 |
+
|
| 99 |
+
interface ModalProps {
|
| 100 |
+
isOpen: boolean;
|
| 101 |
+
onClose: () => void;
|
| 102 |
+
title: string;
|
| 103 |
+
children: React.ReactNode;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
export function Modal({ isOpen, onClose, title, children }: ModalProps) {
|
| 107 |
+
if (!isOpen) return null;
|
| 108 |
+
|
| 109 |
+
return (
|
| 110 |
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
| 111 |
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
| 112 |
+
<div className="relative w-full max-w-md rounded-lg border border-editor-border bg-editor-surface p-6 shadow-xl animate-fade-in">
|
| 113 |
+
<h2 className="text-lg font-bold mb-4">{title}</h2>
|
| 114 |
+
{children}
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// ─── Tooltip ─────────────────────────────────────────────────
|
| 121 |
+
|
| 122 |
+
interface TooltipProps {
|
| 123 |
+
children: React.ReactNode;
|
| 124 |
+
content: string;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
export function Tooltip({ children, content }: TooltipProps) {
|
| 128 |
+
return (
|
| 129 |
+
<div className="group relative inline-flex">
|
| 130 |
+
{children}
|
| 131 |
+
<div className="pointer-events-none absolute -top-8 left-1/2 -translate-x-1/2 opacity-0 transition-opacity group-hover:opacity-100">
|
| 132 |
+
<div className="rounded bg-editor-bg border border-editor-border px-2 py-1 text-[10px] text-editor-text whitespace-nowrap shadow-lg">
|
| 133 |
+
{content}
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
);
|
| 138 |
+
}
|
apps/web/src/hooks/useSocket.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
import { useEffect, useRef, useCallback } from 'react';
|
| 3 |
+
import { Socket } from 'socket.io-client';
|
| 4 |
+
import { getSocket, disconnectSocket } from '../lib/socket';
|
| 5 |
+
import { useAuthStore } from '../stores/authStore';
|
| 6 |
+
import { useRoomStore } from '../stores/roomStore';
|
| 7 |
+
|
| 8 |
+
export function useSocket() {
|
| 9 |
+
const socketRef = useRef<Socket | null>(null);
|
| 10 |
+
const { tokens } = useAuthStore();
|
| 11 |
+
const { setConnected } = useRoomStore();
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
if (!tokens?.accessToken) return;
|
| 15 |
+
const socket = getSocket(tokens.accessToken);
|
| 16 |
+
socketRef.current = socket;
|
| 17 |
+
if (!socket.connected) socket.connect();
|
| 18 |
+
|
| 19 |
+
socket.on('connect', () => setConnected(true));
|
| 20 |
+
socket.on('disconnect', () => setConnected(false));
|
| 21 |
+
socket.on('connect_error', () => setConnected(false));
|
| 22 |
+
|
| 23 |
+
return () => { socket.off('connect'); socket.off('disconnect'); socket.off('connect_error'); };
|
| 24 |
+
}, [tokens?.accessToken, setConnected]);
|
| 25 |
+
|
| 26 |
+
const emit = useCallback((event: string, data?: any) => { socketRef.current?.emit(event, data); }, []);
|
| 27 |
+
const on = useCallback((event: string, handler: (...args: any[]) => void) => {
|
| 28 |
+
socketRef.current?.on(event, handler);
|
| 29 |
+
return () => { socketRef.current?.off(event, handler); };
|
| 30 |
+
}, []);
|
| 31 |
+
|
| 32 |
+
return { socket: socketRef.current, emit, on, isConnected: socketRef.current?.connected ?? false, disconnect: disconnectSocket };
|
| 33 |
+
}
|
apps/web/src/lib/api.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useAuthStore } from '../stores/authStore';
|
| 2 |
+
|
| 3 |
+
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
| 4 |
+
|
| 5 |
+
class ApiClient {
|
| 6 |
+
private baseUrl: string;
|
| 7 |
+
constructor(baseUrl: string) { this.baseUrl = baseUrl; }
|
| 8 |
+
|
| 9 |
+
private getAuthHeader(): Record<string, string> {
|
| 10 |
+
const tokens = useAuthStore.getState().tokens;
|
| 11 |
+
return tokens?.accessToken ? { Authorization: `Bearer ${tokens.accessToken}` } : {};
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
async request<T>(endpoint: string, config: { method?: string; body?: any } = {}): Promise<T> {
|
| 15 |
+
const { method = 'GET', body } = config;
|
| 16 |
+
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
| 17 |
+
method,
|
| 18 |
+
headers: { 'Content-Type': 'application/json', ...this.getAuthHeader() },
|
| 19 |
+
body: body ? JSON.stringify(body) : undefined,
|
| 20 |
+
});
|
| 21 |
+
const data = await response.json();
|
| 22 |
+
if (!response.ok) throw new Error(data.error?.message || 'Request failed');
|
| 23 |
+
return data.data as T;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
get<T>(e: string) { return this.request<T>(e); }
|
| 27 |
+
post<T>(e: string, b?: any) { return this.request<T>(e, { method: 'POST', body: b }); }
|
| 28 |
+
patch<T>(e: string, b?: any) { return this.request<T>(e, { method: 'PATCH', body: b }); }
|
| 29 |
+
delete<T>(e: string) { return this.request<T>(e, { method: 'DELETE' }); }
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
export const api = new ApiClient(API_URL);
|
| 33 |
+
|
| 34 |
+
export const authApi = {
|
| 35 |
+
register: (data: any) => api.post('/api/auth/register', data),
|
| 36 |
+
login: (data: any) => api.post('/api/auth/login', data),
|
| 37 |
+
me: () => api.get('/api/auth/me'),
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
export const roomsApi = {
|
| 41 |
+
list: () => api.get('/api/rooms'),
|
| 42 |
+
create: (data: any) => api.post('/api/rooms', data),
|
| 43 |
+
get: (id: string) => api.get(`/api/rooms/${id}`),
|
| 44 |
+
join: (inviteCode: string) => api.post('/api/rooms/join', { inviteCode }),
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
export const aiApi = {
|
| 48 |
+
action: (data: any) => api.post('/api/ai/action', data),
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
export const snippetsApi = {
|
| 52 |
+
list: () => api.get('/api/snippets'),
|
| 53 |
+
create: (data: any) => api.post('/api/snippets', data),
|
| 54 |
+
delete: (id: string) => api.delete(`/api/snippets/${id}`),
|
| 55 |
+
};
|
apps/web/src/lib/crdt.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as Y from 'yjs';
|
| 2 |
+
import { Socket } from 'socket.io-client';
|
| 3 |
+
|
| 4 |
+
export class YjsBinding {
|
| 5 |
+
public doc: Y.Doc;
|
| 6 |
+
public text: Y.Text;
|
| 7 |
+
private socket: Socket;
|
| 8 |
+
private fileId: string;
|
| 9 |
+
private isLocalUpdate = false;
|
| 10 |
+
|
| 11 |
+
constructor(socket: Socket, fileId: string) {
|
| 12 |
+
this.socket = socket;
|
| 13 |
+
this.fileId = fileId;
|
| 14 |
+
this.doc = new Y.Doc();
|
| 15 |
+
this.text = this.doc.getText('content');
|
| 16 |
+
|
| 17 |
+
// Broadcast local changes
|
| 18 |
+
this.doc.on('update', (update: Uint8Array, origin: any) => {
|
| 19 |
+
if (origin !== 'remote') {
|
| 20 |
+
this.socket.emit('doc:update', { update: Array.from(update), fileId: this.fileId });
|
| 21 |
+
}
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
// Apply remote changes
|
| 25 |
+
this.socket.on('doc:sync', (data: { update: number[]; fileId: string }) => {
|
| 26 |
+
if (data.fileId !== this.fileId) return;
|
| 27 |
+
this.isLocalUpdate = true;
|
| 28 |
+
Y.applyUpdate(this.doc, new Uint8Array(data.update), 'remote');
|
| 29 |
+
this.isLocalUpdate = false;
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
this.socket.on('doc:state', (data: { state: number[]; fileId: string }) => {
|
| 33 |
+
if (data.fileId !== this.fileId) return;
|
| 34 |
+
if (data.state.length > 0) {
|
| 35 |
+
this.isLocalUpdate = true;
|
| 36 |
+
Y.applyUpdate(this.doc, new Uint8Array(data.state), 'remote');
|
| 37 |
+
this.isLocalUpdate = false;
|
| 38 |
+
}
|
| 39 |
+
});
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
requestState(): void { this.socket.emit('doc:request-state', { fileId: this.fileId }); }
|
| 43 |
+
getText(): string { return this.text.toString(); }
|
| 44 |
+
isRemoteUpdate(): boolean { return this.isLocalUpdate; }
|
| 45 |
+
|
| 46 |
+
applyMonacoChanges(changes: Array<{ rangeOffset: number; rangeLength: number; text: string }>): void {
|
| 47 |
+
this.doc.transact(() => {
|
| 48 |
+
const sorted = [...changes].sort((a, b) => b.rangeOffset - a.rangeOffset);
|
| 49 |
+
for (const change of sorted) {
|
| 50 |
+
if (change.rangeLength > 0) this.text.delete(change.rangeOffset, change.rangeLength);
|
| 51 |
+
if (change.text) this.text.insert(change.rangeOffset, change.text);
|
| 52 |
+
}
|
| 53 |
+
});
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
observe(callback: (event: Y.YTextEvent) => void): void { this.text.observe(callback); }
|
| 57 |
+
|
| 58 |
+
destroy(): void {
|
| 59 |
+
this.doc.destroy();
|
| 60 |
+
this.socket.off('doc:sync');
|
| 61 |
+
this.socket.off('doc:state');
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
export { Y };
|
apps/web/src/lib/socket.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { io, Socket } from 'socket.io-client';
|
| 2 |
+
|
| 3 |
+
const SOCKET_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
|
| 4 |
+
let socket: Socket | null = null;
|
| 5 |
+
|
| 6 |
+
export function getSocket(token: string): Socket {
|
| 7 |
+
if (socket?.connected) return socket;
|
| 8 |
+
socket = io(SOCKET_URL, {
|
| 9 |
+
auth: { token },
|
| 10 |
+
transports: ['websocket', 'polling'],
|
| 11 |
+
reconnection: true,
|
| 12 |
+
reconnectionAttempts: 10,
|
| 13 |
+
reconnectionDelay: 1000,
|
| 14 |
+
timeout: 20000,
|
| 15 |
+
autoConnect: false,
|
| 16 |
+
});
|
| 17 |
+
return socket;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export function disconnectSocket(): void {
|
| 21 |
+
if (socket) { socket.disconnect(); socket = null; }
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function getCurrentSocket(): Socket | null {
|
| 25 |
+
return socket;
|
| 26 |
+
}
|
apps/web/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { clsx, type ClassValue } from 'clsx';
|
| 2 |
+
import { twMerge } from 'tailwind-merge';
|
| 3 |
+
|
| 4 |
+
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
|
| 5 |
+
|
| 6 |
+
export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number) {
|
| 7 |
+
let timeoutId: NodeJS.Timeout;
|
| 8 |
+
return (...args: Parameters<T>) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); };
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export function throttle<T extends (...args: any[]) => any>(fn: T, limit: number) {
|
| 12 |
+
let inThrottle: boolean;
|
| 13 |
+
return (...args: Parameters<T>) => { if (!inThrottle) { fn(...args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } };
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export function formatRelativeTime(dateStr: string): string {
|
| 17 |
+
const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
|
| 18 |
+
if (seconds < 60) return 'just now';
|
| 19 |
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
| 20 |
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
| 21 |
+
return `${Math.floor(seconds / 86400)}d ago`;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function getFileIcon(filename: string): string {
|
| 25 |
+
const ext = filename.split('.').pop()?.toLowerCase();
|
| 26 |
+
const icons: Record<string, string> = { js: '📄', ts: '📘', py: '🐍', cpp: '⚙️', java: '☕', html: '🌐', css: '🎨', json: '📋', md: '📝' };
|
| 27 |
+
return icons[ext || ''] || '📄';
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
| 31 |
+
try { await navigator.clipboard.writeText(text); return true; } catch { return false; }
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export function generateInviteLink(inviteCode: string): string {
|
| 35 |
+
const baseUrl = typeof window !== 'undefined' ? window.location.origin : '';
|
| 36 |
+
return `${baseUrl}/join/${inviteCode}`;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export function formatDuration(ms: number): string {
|
| 40 |
+
if (ms < 1000) return `${ms}ms`;
|
| 41 |
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
| 42 |
+
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
|
| 43 |
+
}
|
apps/web/src/lib/webrtc.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ─────────────────────────────────────────────────────────────
|
| 2 |
+
// WebRTC Helper — Manages peer connections for video/audio
|
| 3 |
+
// ─────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
export const ICE_SERVERS: RTCConfiguration = {
|
| 6 |
+
iceServers: [
|
| 7 |
+
{ urls: 'stun:stun.l.google.com:19302' },
|
| 8 |
+
{ urls: 'stun:stun1.l.google.com:19302' },
|
| 9 |
+
{ urls: 'stun:stun2.l.google.com:19302' },
|
| 10 |
+
{ urls: 'stun:stun3.l.google.com:19302' },
|
| 11 |
+
],
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* Create an RTCPeerConnection with the configured ICE servers
|
| 16 |
+
*/
|
| 17 |
+
export function createPeerConnection(): RTCPeerConnection {
|
| 18 |
+
return new RTCPeerConnection(ICE_SERVERS);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* Get user media with fallback handling
|
| 23 |
+
*/
|
| 24 |
+
export async function getUserMedia(
|
| 25 |
+
video: boolean = true,
|
| 26 |
+
audio: boolean = true
|
| 27 |
+
): Promise<MediaStream> {
|
| 28 |
+
try {
|
| 29 |
+
return await navigator.mediaDevices.getUserMedia({ video, audio });
|
| 30 |
+
} catch (error: any) {
|
| 31 |
+
// Try audio-only if video fails
|
| 32 |
+
if (video) {
|
| 33 |
+
console.warn('Video access failed, trying audio only');
|
| 34 |
+
return await navigator.mediaDevices.getUserMedia({ video: false, audio });
|
| 35 |
+
}
|
| 36 |
+
throw error;
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Get display media for screen sharing
|
| 42 |
+
*/
|
| 43 |
+
export async function getDisplayMedia(): Promise<MediaStream> {
|
| 44 |
+
return await navigator.mediaDevices.getDisplayMedia({
|
| 45 |
+
video: { cursor: 'always' } as any,
|
| 46 |
+
audio: true,
|
| 47 |
+
});
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/**
|
| 51 |
+
* Check if WebRTC is supported
|
| 52 |
+
*/
|
| 53 |
+
export function isWebRTCSupported(): boolean {
|
| 54 |
+
return !!(
|
| 55 |
+
window.RTCPeerConnection &&
|
| 56 |
+
navigator.mediaDevices &&
|
| 57 |
+
navigator.mediaDevices.getUserMedia
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* Estimate connection quality based on RTCPeerConnection stats
|
| 63 |
+
*/
|
| 64 |
+
export async function getConnectionQuality(
|
| 65 |
+
pc: RTCPeerConnection
|
| 66 |
+
): Promise<'excellent' | 'good' | 'poor' | 'disconnected'> {
|
| 67 |
+
if (pc.connectionState === 'disconnected' || pc.connectionState === 'failed') {
|
| 68 |
+
return 'disconnected';
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
try {
|
| 72 |
+
const stats = await pc.getStats();
|
| 73 |
+
let roundTripTime = 0;
|
| 74 |
+
let packetsLost = 0;
|
| 75 |
+
|
| 76 |
+
stats.forEach((report) => {
|
| 77 |
+
if (report.type === 'candidate-pair' && report.currentRoundTripTime) {
|
| 78 |
+
roundTripTime = report.currentRoundTripTime * 1000; // ms
|
| 79 |
+
}
|
| 80 |
+
if (report.type === 'inbound-rtp' && report.packetsLost) {
|
| 81 |
+
packetsLost = report.packetsLost;
|
| 82 |
+
}
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
if (roundTripTime < 100 && packetsLost < 5) return 'excellent';
|
| 86 |
+
if (roundTripTime < 300 && packetsLost < 20) return 'good';
|
| 87 |
+
return 'poor';
|
| 88 |
+
} catch {
|
| 89 |
+
return 'good'; // Default if stats unavailable
|
| 90 |
+
}
|
| 91 |
+
}
|
apps/web/src/stores/authStore.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { create } from 'zustand';
|
| 2 |
+
import { persist } from 'zustand/middleware';
|
| 3 |
+
import type { User, AuthTokens } from '../types';
|
| 4 |
+
|
| 5 |
+
interface AuthState {
|
| 6 |
+
user: User | null;
|
| 7 |
+
tokens: AuthTokens | null;
|
| 8 |
+
isAuthenticated: boolean;
|
| 9 |
+
isLoading: boolean;
|
| 10 |
+
setAuth: (user: User, tokens: AuthTokens) => void;
|
| 11 |
+
setTokens: (tokens: AuthTokens) => void;
|
| 12 |
+
logout: () => void;
|
| 13 |
+
setLoading: (loading: boolean) => void;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export const useAuthStore = create<AuthState>()(
|
| 17 |
+
persist(
|
| 18 |
+
(set) => ({
|
| 19 |
+
user: null, tokens: null, isAuthenticated: false, isLoading: true,
|
| 20 |
+
setAuth: (user, tokens) => set({ user, tokens, isAuthenticated: true, isLoading: false }),
|
| 21 |
+
setTokens: (tokens) => set({ tokens }),
|
| 22 |
+
logout: () => set({ user: null, tokens: null, isAuthenticated: false }),
|
| 23 |
+
setLoading: (isLoading) => set({ isLoading }),
|
| 24 |
+
}),
|
| 25 |
+
{ name: 'codesync-auth', partialize: (state) => ({ user: state.user, tokens: state.tokens, isAuthenticated: state.isAuthenticated }) }
|
| 26 |
+
)
|
| 27 |
+
);
|
apps/web/src/stores/chatStore.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { create } from 'zustand';
|
| 2 |
+
import type { ChatMessage } from '../types';
|
| 3 |
+
|
| 4 |
+
interface ChatState {
|
| 5 |
+
messages: ChatMessage[];
|
| 6 |
+
typingUsers: Array<{ userId: string; userName: string }>;
|
| 7 |
+
isOpen: boolean;
|
| 8 |
+
unreadCount: number;
|
| 9 |
+
|
| 10 |
+
addMessage: (message: ChatMessage) => void;
|
| 11 |
+
setMessages: (messages: ChatMessage[]) => void;
|
| 12 |
+
setTypingUser: (userId: string, userName: string, isTyping: boolean) => void;
|
| 13 |
+
setOpen: (open: boolean) => void;
|
| 14 |
+
reset: () => void;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export const useChatStore = create<ChatState>((set) => ({
|
| 18 |
+
messages: [], typingUsers: [], isOpen: false, unreadCount: 0,
|
| 19 |
+
|
| 20 |
+
addMessage: (message) => set((s) => ({ messages: [...s.messages, message], unreadCount: s.isOpen ? 0 : s.unreadCount + 1 })),
|
| 21 |
+
setMessages: (messages) => set({ messages }),
|
| 22 |
+
setTypingUser: (userId, userName, isTyping) => set((s) => ({
|
| 23 |
+
typingUsers: isTyping
|
| 24 |
+
? [...s.typingUsers.filter((t) => t.userId !== userId), { userId, userName }]
|
| 25 |
+
: s.typingUsers.filter((t) => t.userId !== userId),
|
| 26 |
+
})),
|
| 27 |
+
setOpen: (isOpen) => set({ isOpen, unreadCount: isOpen ? 0 : undefined } as any),
|
| 28 |
+
reset: () => set({ messages: [], typingUsers: [], isOpen: false, unreadCount: 0 }),
|
| 29 |
+
}));
|
apps/web/src/stores/editorStore.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { create } from 'zustand';
|
| 2 |
+
import type { RoomFile, ProgrammingLanguage, EditorTab } from '../types';
|
| 3 |
+
|
| 4 |
+
interface EditorState {
|
| 5 |
+
files: RoomFile[];
|
| 6 |
+
activeFileId: string | null;
|
| 7 |
+
tabs: EditorTab[];
|
| 8 |
+
theme: 'vs-dark' | 'light' | 'hc-black';
|
| 9 |
+
fontSize: number;
|
| 10 |
+
wordWrap: 'on' | 'off' | 'bounded';
|
| 11 |
+
minimap: boolean;
|
| 12 |
+
lineNumbers: 'on' | 'off' | 'relative';
|
| 13 |
+
isExecuting: boolean;
|
| 14 |
+
output: string;
|
| 15 |
+
executionLanguage: ProgrammingLanguage;
|
| 16 |
+
|
| 17 |
+
setFiles: (files: RoomFile[]) => void;
|
| 18 |
+
setActiveFile: (fileId: string) => void;
|
| 19 |
+
openTab: (file: RoomFile) => void;
|
| 20 |
+
closeTab: (fileId: string) => void;
|
| 21 |
+
markTabDirty: (fileId: string, isDirty: boolean) => void;
|
| 22 |
+
setTheme: (theme: 'vs-dark' | 'light' | 'hc-black') => void;
|
| 23 |
+
setFontSize: (size: number) => void;
|
| 24 |
+
setWordWrap: (wrap: 'on' | 'off' | 'bounded') => void;
|
| 25 |
+
setMinimap: (enabled: boolean) => void;
|
| 26 |
+
setExecuting: (executing: boolean) => void;
|
| 27 |
+
setOutput: (output: string) => void;
|
| 28 |
+
appendOutput: (chunk: string) => void;
|
| 29 |
+
clearOutput: () => void;
|
| 30 |
+
setExecutionLanguage: (lang: ProgrammingLanguage) => void;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export const useEditorStore = create<EditorState>((set) => ({
|
| 34 |
+
files: [], activeFileId: null, tabs: [],
|
| 35 |
+
theme: 'vs-dark', fontSize: 14, wordWrap: 'on', minimap: true, lineNumbers: 'on',
|
| 36 |
+
isExecuting: false, output: '', executionLanguage: 'javascript',
|
| 37 |
+
|
| 38 |
+
setFiles: (files) => set({ files }),
|
| 39 |
+
setActiveFile: (fileId) => set((s) => ({ activeFileId: fileId, tabs: s.tabs.map((t) => ({ ...t, isActive: t.fileId === fileId })) })),
|
| 40 |
+
openTab: (file) => set((s) => {
|
| 41 |
+
if (s.tabs.find((t) => t.fileId === file.id)) {
|
| 42 |
+
return { activeFileId: file.id, tabs: s.tabs.map((t) => ({ ...t, isActive: t.fileId === file.id })) };
|
| 43 |
+
}
|
| 44 |
+
return {
|
| 45 |
+
activeFileId: file.id,
|
| 46 |
+
tabs: [...s.tabs.map((t) => ({ ...t, isActive: false })), { id: file.id, fileId: file.id, name: file.name, path: file.path, language: file.language, isDirty: false, isActive: true }],
|
| 47 |
+
};
|
| 48 |
+
}),
|
| 49 |
+
closeTab: (fileId) => set((s) => {
|
| 50 |
+
const filtered = s.tabs.filter((t) => t.fileId !== fileId);
|
| 51 |
+
return { tabs: filtered, activeFileId: s.activeFileId === fileId ? (filtered[filtered.length - 1]?.fileId || null) : s.activeFileId };
|
| 52 |
+
}),
|
| 53 |
+
markTabDirty: (fileId, isDirty) => set((s) => ({ tabs: s.tabs.map((t) => t.fileId === fileId ? { ...t, isDirty } : t) })),
|
| 54 |
+
setTheme: (theme) => set({ theme }),
|
| 55 |
+
setFontSize: (fontSize) => set({ fontSize }),
|
| 56 |
+
setWordWrap: (wordWrap) => set({ wordWrap }),
|
| 57 |
+
setMinimap: (minimap) => set({ minimap }),
|
| 58 |
+
setExecuting: (isExecuting) => set({ isExecuting }),
|
| 59 |
+
setOutput: (output) => set({ output }),
|
| 60 |
+
appendOutput: (chunk) => set((s) => ({ output: s.output + chunk })),
|
| 61 |
+
clearOutput: () => set({ output: '' }),
|
| 62 |
+
setExecutionLanguage: (executionLanguage) => set({ executionLanguage }),
|
| 63 |
+
}));
|
apps/web/src/stores/roomStore.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { create } from 'zustand';
|
| 2 |
+
import type { Room, RoomMember, UserPresence, UserCursor } from '../types';
|
| 3 |
+
|
| 4 |
+
interface RoomState {
|
| 5 |
+
currentRoom: Room | null;
|
| 6 |
+
members: RoomMember[];
|
| 7 |
+
presence: UserPresence[];
|
| 8 |
+
cursors: UserCursor[];
|
| 9 |
+
isConnected: boolean;
|
| 10 |
+
isJoining: boolean;
|
| 11 |
+
|
| 12 |
+
setRoom: (room: Room) => void;
|
| 13 |
+
setMembers: (members: RoomMember[]) => void;
|
| 14 |
+
addMember: (member: RoomMember) => void;
|
| 15 |
+
removeMember: (userId: string) => void;
|
| 16 |
+
setPresence: (presence: UserPresence[]) => void;
|
| 17 |
+
setCursors: (cursors: UserCursor[]) => void;
|
| 18 |
+
setConnected: (connected: boolean) => void;
|
| 19 |
+
setJoining: (joining: boolean) => void;
|
| 20 |
+
reset: () => void;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export const useRoomStore = create<RoomState>((set) => ({
|
| 24 |
+
currentRoom: null, members: [], presence: [], cursors: [],
|
| 25 |
+
isConnected: false, isJoining: false,
|
| 26 |
+
|
| 27 |
+
setRoom: (currentRoom) => set({ currentRoom }),
|
| 28 |
+
setMembers: (members) => set({ members }),
|
| 29 |
+
addMember: (member) => set((s) => ({ members: [...s.members.filter((m) => m.userId !== member.userId), member] })),
|
| 30 |
+
removeMember: (userId) => set((s) => ({ members: s.members.filter((m) => m.userId !== userId), cursors: s.cursors.filter((c) => c.userId !== userId) })),
|
| 31 |
+
setPresence: (presence) => set({ presence }),
|
| 32 |
+
setCursors: (cursors) => set({ cursors }),
|
| 33 |
+
setConnected: (isConnected) => set({ isConnected }),
|
| 34 |
+
setJoining: (isJoining) => set({ isJoining }),
|
| 35 |
+
reset: () => set({ currentRoom: null, members: [], presence: [], cursors: [], isConnected: false }),
|
| 36 |
+
}));
|
apps/web/src/styles/globals.css
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
* {
|
| 6 |
+
scrollbar-width: thin;
|
| 7 |
+
scrollbar-color: #424242 transparent;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
*::-webkit-scrollbar { width: 8px; height: 8px; }
|
| 11 |
+
*::-webkit-scrollbar-track { background: transparent; }
|
| 12 |
+
*::-webkit-scrollbar-thumb { background: #424242; border-radius: 4px; }
|
| 13 |
+
*::-webkit-scrollbar-thumb:hover { background: #555; }
|
| 14 |
+
|
| 15 |
+
::selection { background: rgba(0, 122, 204, 0.3); }
|
| 16 |
+
|
| 17 |
+
[data-panel-group-direction="horizontal"] > [data-resize-handle] {
|
| 18 |
+
width: 1px; background: #3e3e42; transition: background 0.2s;
|
| 19 |
+
}
|
| 20 |
+
[data-panel-group-direction="horizontal"] > [data-resize-handle]:hover {
|
| 21 |
+
background: #007acc; width: 2px;
|
| 22 |
+
}
|
| 23 |
+
[data-panel-group-direction="vertical"] > [data-resize-handle] {
|
| 24 |
+
height: 1px; background: #3e3e42; transition: background 0.2s;
|
| 25 |
+
}
|
| 26 |
+
[data-panel-group-direction="vertical"] > [data-resize-handle]:hover {
|
| 27 |
+
background: #007acc; height: 2px;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
@keyframes typing-dots {
|
| 31 |
+
0%, 20% { opacity: 0.2; }
|
| 32 |
+
50% { opacity: 1; }
|
| 33 |
+
100% { opacity: 0.2; }
|
| 34 |
+
}
|
| 35 |
+
.typing-dot { animation: typing-dots 1.4s infinite; }
|
| 36 |
+
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
| 37 |
+
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
| 38 |
+
|
| 39 |
+
@layer components {
|
| 40 |
+
.focus-ring {
|
| 41 |
+
@apply focus:outline-none focus:ring-2 focus:ring-editor-accent focus:ring-offset-0;
|
| 42 |
+
}
|
| 43 |
+
.btn-primary {
|
| 44 |
+
@apply bg-editor-accent hover:bg-editor-accent-hover text-white px-4 py-2 rounded-md
|
| 45 |
+
transition-colors duration-150 font-medium text-sm focus-ring;
|
| 46 |
+
}
|
| 47 |
+
.btn-secondary {
|
| 48 |
+
@apply bg-editor-surface hover:bg-editor-hover text-editor-text px-4 py-2 rounded-md
|
| 49 |
+
border border-editor-border transition-colors duration-150 text-sm focus-ring;
|
| 50 |
+
}
|
| 51 |
+
.input-field {
|
| 52 |
+
@apply bg-editor-bg border border-editor-border rounded-md px-3 py-2 text-sm text-editor-text
|
| 53 |
+
placeholder:text-editor-text-muted focus-ring transition-colors;
|
| 54 |
+
}
|
| 55 |
+
}
|
apps/web/src/types/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ─────────────────────────────────────────────────────────────
|
| 2 |
+
// Frontend Types Index — Re-export all types
|
| 3 |
+
// ─────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
export type {
|
| 6 |
+
User,
|
| 7 |
+
AuthTokens,
|
| 8 |
+
Room,
|
| 9 |
+
RoomMember,
|
| 10 |
+
RoomFile,
|
| 11 |
+
RoomRole,
|
| 12 |
+
CreateRoomPayload,
|
| 13 |
+
ProgrammingLanguage,
|
| 14 |
+
UserCursor,
|
| 15 |
+
UserPresence,
|
| 16 |
+
ChatMessage,
|
| 17 |
+
MessageType,
|
| 18 |
+
ExecutionRequest,
|
| 19 |
+
ExecutionResult,
|
| 20 |
+
AIMessage,
|
| 21 |
+
AIRequest,
|
| 22 |
+
AIResponse,
|
| 23 |
+
AIAction,
|
| 24 |
+
DashboardStats,
|
| 25 |
+
Snippet,
|
| 26 |
+
WebRTCSignal,
|
| 27 |
+
PeerConnection,
|
| 28 |
+
ServerToClientEvents,
|
| 29 |
+
ClientToServerEvents,
|
| 30 |
+
} from '@codesync/shared';
|
| 31 |
+
|
| 32 |
+
// Frontend-specific types
|
| 33 |
+
export interface EditorTab {
|
| 34 |
+
id: string;
|
| 35 |
+
fileId: string;
|
| 36 |
+
name: string;
|
| 37 |
+
path: string;
|
| 38 |
+
language: string;
|
| 39 |
+
isDirty: boolean;
|
| 40 |
+
isActive: boolean;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
export interface ConnectionStatus {
|
| 44 |
+
isConnected: boolean;
|
| 45 |
+
latency: number;
|
| 46 |
+
reconnecting: boolean;
|
| 47 |
+
}
|
apps/web/tailwind.config.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Config } from 'tailwindcss';
|
| 2 |
+
|
| 3 |
+
const config: Config = {
|
| 4 |
+
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
|
| 5 |
+
darkMode: 'class',
|
| 6 |
+
theme: {
|
| 7 |
+
extend: {
|
| 8 |
+
colors: {
|
| 9 |
+
editor: {
|
| 10 |
+
bg: '#1e1e1e',
|
| 11 |
+
surface: '#252526',
|
| 12 |
+
border: '#3e3e42',
|
| 13 |
+
hover: '#2a2d2e',
|
| 14 |
+
active: '#37373d',
|
| 15 |
+
text: '#cccccc',
|
| 16 |
+
'text-muted': '#858585',
|
| 17 |
+
accent: '#007acc',
|
| 18 |
+
'accent-hover': '#1177bb',
|
| 19 |
+
success: '#4ec9b0',
|
| 20 |
+
warning: '#dcdcaa',
|
| 21 |
+
error: '#f14c4c',
|
| 22 |
+
info: '#75beff',
|
| 23 |
+
},
|
| 24 |
+
sidebar: {
|
| 25 |
+
bg: '#181818',
|
| 26 |
+
hover: '#2a2d2e',
|
| 27 |
+
active: '#37373d',
|
| 28 |
+
},
|
| 29 |
+
},
|
| 30 |
+
fontFamily: {
|
| 31 |
+
mono: ['JetBrains Mono', 'Fira Code', 'Consolas', 'monospace'],
|
| 32 |
+
sans: ['Inter', 'system-ui', 'sans-serif'],
|
| 33 |
+
},
|
| 34 |
+
animation: {
|
| 35 |
+
'fade-in': 'fadeIn 0.2s ease-in-out',
|
| 36 |
+
'slide-up': 'slideUp 0.3s ease-out',
|
| 37 |
+
},
|
| 38 |
+
keyframes: {
|
| 39 |
+
fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } },
|
| 40 |
+
slideUp: { '0%': { transform: 'translateY(10px)', opacity: '0' }, '100%': { transform: 'translateY(0)', opacity: '1' } },
|
| 41 |
+
},
|
| 42 |
+
},
|
| 43 |
+
},
|
| 44 |
+
plugins: [require('@tailwindcss/typography')],
|
| 45 |
+
};
|
| 46 |
+
export default config;
|
apps/web/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2017",
|
| 4 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
| 5 |
+
"allowJs": true,
|
| 6 |
+
"skipLibCheck": true,
|
| 7 |
+
"strict": true,
|
| 8 |
+
"noEmit": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"module": "esnext",
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"jsx": "preserve",
|
| 15 |
+
"incremental": true,
|
| 16 |
+
"plugins": [{ "name": "next" }],
|
| 17 |
+
"paths": {
|
| 18 |
+
"@/*": ["./src/*"],
|
| 19 |
+
"@codesync/shared": ["../../packages/shared/src"]
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
| 23 |
+
"exclude": ["node_modules"]
|
| 24 |
+
}
|
docker/docker-compose.yml
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
# ═══════════════════════════════════════════════════════════
|
| 5 |
+
# Backend API Server
|
| 6 |
+
# ═══════════════════════════════════════════════════════════
|
| 7 |
+
server:
|
| 8 |
+
build:
|
| 9 |
+
context: ./apps/server
|
| 10 |
+
dockerfile: Dockerfile
|
| 11 |
+
ports:
|
| 12 |
+
- "4000:4000"
|
| 13 |
+
environment:
|
| 14 |
+
- NODE_ENV=production
|
| 15 |
+
- PORT=4000
|
| 16 |
+
- CLIENT_URL=http://localhost:3000
|
| 17 |
+
- DATABASE_URL=postgresql://codesync:codesync@postgres:5432/codesync
|
| 18 |
+
- UPSTASH_REDIS_URL=${UPSTASH_REDIS_URL}
|
| 19 |
+
- UPSTASH_REDIS_TOKEN=${UPSTASH_REDIS_TOKEN}
|
| 20 |
+
- JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET}
|
| 21 |
+
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
|
| 22 |
+
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
| 23 |
+
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
| 24 |
+
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
|
| 25 |
+
- DOCKER_SOCKET=/var/run/docker.sock
|
| 26 |
+
volumes:
|
| 27 |
+
- /var/run/docker.sock:/var/run/docker.sock
|
| 28 |
+
depends_on:
|
| 29 |
+
postgres:
|
| 30 |
+
condition: service_healthy
|
| 31 |
+
restart: unless-stopped
|
| 32 |
+
networks:
|
| 33 |
+
- codesync-net
|
| 34 |
+
|
| 35 |
+
# ═══════════════════════════════════════════════════════════
|
| 36 |
+
# Frontend (Next.js)
|
| 37 |
+
# ═══════════════════════════════════════════════════════════
|
| 38 |
+
web:
|
| 39 |
+
build:
|
| 40 |
+
context: ./apps/web
|
| 41 |
+
dockerfile: Dockerfile
|
| 42 |
+
ports:
|
| 43 |
+
- "3000:3000"
|
| 44 |
+
environment:
|
| 45 |
+
- NEXT_PUBLIC_API_URL=http://localhost:4000
|
| 46 |
+
depends_on:
|
| 47 |
+
- server
|
| 48 |
+
restart: unless-stopped
|
| 49 |
+
networks:
|
| 50 |
+
- codesync-net
|
| 51 |
+
|
| 52 |
+
# ═══════════════════════════════════════════════════════════
|
| 53 |
+
# PostgreSQL Database
|
| 54 |
+
# ═══════════════════════════════════════════════════════════
|
| 55 |
+
postgres:
|
| 56 |
+
image: postgres:16-alpine
|
| 57 |
+
environment:
|
| 58 |
+
POSTGRES_DB: codesync
|
| 59 |
+
POSTGRES_USER: codesync
|
| 60 |
+
POSTGRES_PASSWORD: codesync
|
| 61 |
+
ports:
|
| 62 |
+
- "5432:5432"
|
| 63 |
+
volumes:
|
| 64 |
+
- postgres_data:/var/lib/postgresql/data
|
| 65 |
+
healthcheck:
|
| 66 |
+
test: ["CMD-SHELL", "pg_isready -U codesync"]
|
| 67 |
+
interval: 5s
|
| 68 |
+
timeout: 5s
|
| 69 |
+
retries: 5
|
| 70 |
+
restart: unless-stopped
|
| 71 |
+
networks:
|
| 72 |
+
- codesync-net
|
| 73 |
+
|
| 74 |
+
# ═══════════════════════════════════════════════════════════
|
| 75 |
+
# Redis (local development — use Upstash in production)
|
| 76 |
+
# ═══════════════════════════════════════════════════════════
|
| 77 |
+
redis:
|
| 78 |
+
image: redis:7-alpine
|
| 79 |
+
ports:
|
| 80 |
+
- "6379:6379"
|
| 81 |
+
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
|
| 82 |
+
volumes:
|
| 83 |
+
- redis_data:/data
|
| 84 |
+
healthcheck:
|
| 85 |
+
test: ["CMD", "redis-cli", "ping"]
|
| 86 |
+
interval: 5s
|
| 87 |
+
timeout: 3s
|
| 88 |
+
retries: 5
|
| 89 |
+
restart: unless-stopped
|
| 90 |
+
networks:
|
| 91 |
+
- codesync-net
|
| 92 |
+
|
| 93 |
+
volumes:
|
| 94 |
+
postgres_data:
|
| 95 |
+
redis_data:
|
| 96 |
+
|
| 97 |
+
networks:
|
| 98 |
+
codesync-net:
|
| 99 |
+
driver: bridge
|
| 100 |
+
# Isolated network for sandbox containers
|
| 101 |
+
codesync-sandbox:
|
| 102 |
+
driver: bridge
|
| 103 |
+
internal: true # No external access
|
docker/sandboxes/cpp/Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ─────────────────────────────────────────────────────────────
|
| 2 |
+
# Sandbox Dockerfile — C++ execution
|
| 3 |
+
# ─────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
FROM gcc:13-bookworm
|
| 6 |
+
|
| 7 |
+
# Create sandbox user
|
| 8 |
+
RUN useradd -m -u 1000 sandbox
|
| 9 |
+
|
| 10 |
+
# Create workspace
|
| 11 |
+
RUN mkdir -p /sandbox && chown sandbox:sandbox /sandbox
|
| 12 |
+
|
| 13 |
+
USER sandbox
|
| 14 |
+
WORKDIR /sandbox
|
| 15 |
+
|
| 16 |
+
ENTRYPOINT ["sh", "-c"]
|
docker/sandboxes/java/Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ─────────────────────────────────────────────────────────────
|
| 2 |
+
# Sandbox Dockerfile — Java execution
|
| 3 |
+
# ─────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
FROM eclipse-temurin:21-jdk-alpine
|
| 6 |
+
|
| 7 |
+
# Create sandbox user
|
| 8 |
+
RUN adduser -D -u 1000 sandbox
|
| 9 |
+
|
| 10 |
+
# Create workspace
|
| 11 |
+
RUN mkdir -p /sandbox && chown sandbox:sandbox /sandbox
|
| 12 |
+
|
| 13 |
+
USER sandbox
|
| 14 |
+
WORKDIR /sandbox
|
| 15 |
+
|
| 16 |
+
ENTRYPOINT ["sh", "-c"]
|
docker/sandboxes/javascript/Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ─────────────────────────────────────────────────────────────
|
| 2 |
+
# Sandbox Dockerfile — JavaScript/TypeScript execution
|
| 3 |
+
# Minimal image with strict security constraints
|
| 4 |
+
# ─────────────────────────────────────────────────────────────
|
| 5 |
+
|
| 6 |
+
FROM node:20-alpine
|
| 7 |
+
|
| 8 |
+
# Create sandbox user (non-root)
|
| 9 |
+
RUN adduser -D -u 1000 sandbox
|
| 10 |
+
|
| 11 |
+
# Create workspace
|
| 12 |
+
RUN mkdir -p /sandbox && chown sandbox:sandbox /sandbox
|
| 13 |
+
|
| 14 |
+
# Install TypeScript runtime
|
| 15 |
+
RUN npm install -g tsx typescript
|
| 16 |
+
|
| 17 |
+
# Remove package manager to prevent installs
|
| 18 |
+
RUN rm -rf /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack
|
| 19 |
+
|
| 20 |
+
# Switch to sandbox user
|
| 21 |
+
USER sandbox
|
| 22 |
+
WORKDIR /sandbox
|
| 23 |
+
|
| 24 |
+
# Default entrypoint
|
| 25 |
+
ENTRYPOINT ["sh", "-c"]
|
docker/sandboxes/python/Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ─────────────────────────────────────────────────────────────
|
| 2 |
+
# Sandbox Dockerfile — Python execution
|
| 3 |
+
# ─────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
FROM python:3.12-alpine
|
| 6 |
+
|
| 7 |
+
# Create sandbox user
|
| 8 |
+
RUN adduser -D -u 1000 sandbox
|
| 9 |
+
|
| 10 |
+
# Install common Python packages
|
| 11 |
+
RUN pip install --no-cache-dir numpy pandas
|
| 12 |
+
|
| 13 |
+
# Create workspace
|
| 14 |
+
RUN mkdir -p /sandbox && chown sandbox:sandbox /sandbox
|
| 15 |
+
|
| 16 |
+
# Remove pip to prevent installs
|
| 17 |
+
RUN rm -rf /usr/local/bin/pip /usr/local/bin/pip3
|
| 18 |
+
|
| 19 |
+
USER sandbox
|
| 20 |
+
WORKDIR /sandbox
|
| 21 |
+
|
| 22 |
+
ENTRYPOINT ["sh", "-c"]
|
docs/DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CodeSync — Deployment Guide
|
| 2 |
+
|
| 3 |
+
## Architecture Overview
|
| 4 |
+
|
| 5 |
+
```
|
| 6 |
+
┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐
|
| 7 |
+
│ Vercel │ │ Railway/Render │ │ Upstash │
|
| 8 |
+
│ (Frontend) │────>│ (Backend) │────>│ Redis │
|
| 9 |
+
│ Next.js SSR │ │ Express+Socket │ │ (Pub/Sub) │
|
| 10 |
+
└─────────────────┘ └────────┬─────────┘ └──────────────┘
|
| 11 |
+
│
|
| 12 |
+
┌────────┴─────────┐
|
| 13 |
+
│ Supabase/Neon │
|
| 14 |
+
│ (PostgreSQL) │
|
| 15 |
+
└──────────────────┘
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
## Step 1: Database Setup (Supabase)
|
| 19 |
+
|
| 20 |
+
1. Create a Supabase project at https://supabase.com
|
| 21 |
+
2. Copy the connection string from Settings → Database
|
| 22 |
+
3. Run migrations:
|
| 23 |
+
```bash
|
| 24 |
+
cd apps/server
|
| 25 |
+
npx prisma migrate deploy
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## Step 2: Redis Setup (Upstash)
|
| 29 |
+
|
| 30 |
+
1. Create a Redis database at https://console.upstash.com
|
| 31 |
+
2. Select the region closest to your backend server
|
| 32 |
+
3. Copy the REST URL and token
|
| 33 |
+
4. Enable "Eviction" for memory management
|
| 34 |
+
|
| 35 |
+
## Step 3: Backend Deployment (Railway)
|
| 36 |
+
|
| 37 |
+
### Using Railway CLI:
|
| 38 |
+
```bash
|
| 39 |
+
# Install Railway CLI
|
| 40 |
+
npm i -g @railway/cli
|
| 41 |
+
|
| 42 |
+
# Login and create project
|
| 43 |
+
railway login
|
| 44 |
+
railway init
|
| 45 |
+
|
| 46 |
+
# Set environment variables
|
| 47 |
+
railway variables set NODE_ENV=production
|
| 48 |
+
railway variables set PORT=4000
|
| 49 |
+
railway variables set DATABASE_URL="postgresql://..."
|
| 50 |
+
railway variables set UPSTASH_REDIS_URL="https://..."
|
| 51 |
+
railway variables set UPSTASH_REDIS_TOKEN="..."
|
| 52 |
+
railway variables set JWT_ACCESS_SECRET="$(openssl rand -base64 64)"
|
| 53 |
+
railway variables set JWT_REFRESH_SECRET="$(openssl rand -base64 64)"
|
| 54 |
+
railway variables set CLIENT_URL="https://your-app.vercel.app"
|
| 55 |
+
railway variables set OPENROUTER_API_KEY="..."
|
| 56 |
+
|
| 57 |
+
# Deploy
|
| 58 |
+
railway up
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### Using Render:
|
| 62 |
+
1. Connect GitHub repository
|
| 63 |
+
2. Select `apps/server` as the root directory
|
| 64 |
+
3. Build command: `npm run build`
|
| 65 |
+
4. Start command: `npm start`
|
| 66 |
+
5. Add environment variables in the dashboard
|
| 67 |
+
|
| 68 |
+
## Step 4: Frontend Deployment (Vercel)
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
# Install Vercel CLI
|
| 72 |
+
npm i -g vercel
|
| 73 |
+
|
| 74 |
+
# Deploy
|
| 75 |
+
cd apps/web
|
| 76 |
+
vercel
|
| 77 |
+
|
| 78 |
+
# Set environment variable
|
| 79 |
+
vercel env add NEXT_PUBLIC_API_URL
|
| 80 |
+
# Enter your Railway/Render backend URL
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### Vercel Configuration:
|
| 84 |
+
- Framework: Next.js
|
| 85 |
+
- Root Directory: `apps/web`
|
| 86 |
+
- Build Command: `next build`
|
| 87 |
+
- Output Directory: `.next`
|
| 88 |
+
|
| 89 |
+
## Step 5: Docker Sandbox Setup
|
| 90 |
+
|
| 91 |
+
For code execution to work, the backend server needs Docker access:
|
| 92 |
+
|
| 93 |
+
### Option A: Railway with Docker (recommended)
|
| 94 |
+
Railway supports Docker-in-Docker. Mount the Docker socket.
|
| 95 |
+
|
| 96 |
+
### Option B: Separate execution service
|
| 97 |
+
Deploy a dedicated execution microservice on a VPS with Docker:
|
| 98 |
+
```bash
|
| 99 |
+
# On a VPS (DigitalOcean, Hetzner, etc.)
|
| 100 |
+
docker compose -f docker/docker-compose.execution.yml up -d
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
### Option C: Use a managed service
|
| 104 |
+
Use Judge0 API or Piston for code execution without managing Docker.
|
| 105 |
+
|
| 106 |
+
## Step 6: Google OAuth Setup
|
| 107 |
+
|
| 108 |
+
1. Go to https://console.cloud.google.com
|
| 109 |
+
2. Create OAuth 2.0 credentials
|
| 110 |
+
3. Add authorized redirect: `https://your-backend.railway.app/api/auth/google/callback`
|
| 111 |
+
4. Add authorized origin: `https://your-app.vercel.app`
|
| 112 |
+
|
| 113 |
+
## Step 7: Build Sandbox Images
|
| 114 |
+
|
| 115 |
+
```bash
|
| 116 |
+
cd docker/sandboxes
|
| 117 |
+
docker build -t codesync-sandbox-js ./javascript
|
| 118 |
+
docker build -t codesync-sandbox-python ./python
|
| 119 |
+
docker build -t codesync-sandbox-cpp ./cpp
|
| 120 |
+
docker build -t codesync-sandbox-java ./java
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
## Environment Variables Checklist
|
| 124 |
+
|
| 125 |
+
| Variable | Required | Service |
|
| 126 |
+
|----------|----------|---------|
|
| 127 |
+
| DATABASE_URL | ✅ | Supabase |
|
| 128 |
+
| UPSTASH_REDIS_URL | ✅ | Upstash |
|
| 129 |
+
| UPSTASH_REDIS_TOKEN | ✅ | Upstash |
|
| 130 |
+
| JWT_ACCESS_SECRET | ✅ | Generate |
|
| 131 |
+
| JWT_REFRESH_SECRET | ✅ | Generate |
|
| 132 |
+
| CLIENT_URL | ✅ | Vercel URL |
|
| 133 |
+
| OPENROUTER_API_KEY | Optional | OpenRouter |
|
| 134 |
+
| GOOGLE_CLIENT_ID | Optional | Google Cloud |
|
| 135 |
+
| GOOGLE_CLIENT_SECRET | Optional | Google Cloud |
|
| 136 |
+
|
| 137 |
+
## Performance Optimization
|
| 138 |
+
|
| 139 |
+
1. **WebSocket Sticky Sessions**: Not needed — Redis pub/sub handles cross-instance communication
|
| 140 |
+
2. **Connection Pooling**: Use PgBouncer or Supabase's built-in pooler
|
| 141 |
+
3. **CDN**: Vercel Edge Network handles static assets automatically
|
| 142 |
+
4. **Rate Limiting**: Redis-backed distributed rate limiting works across instances
|
| 143 |
+
|
| 144 |
+
## Scaling Strategy
|
| 145 |
+
|
| 146 |
+
- **Horizontal Backend Scaling**: Add more Railway/Render instances. Redis pub/sub ensures all instances share state.
|
| 147 |
+
- **Database Scaling**: Use read replicas for queries, primary for writes
|
| 148 |
+
- **WebSocket Scaling**: Each instance handles up to ~10K concurrent connections
|
| 149 |
+
- **Code Execution**: Queue-based with separate worker pool
|
| 150 |
+
|
| 151 |
+
## Monitoring
|
| 152 |
+
|
| 153 |
+
- Use Railway/Render built-in metrics
|
| 154 |
+
- Set up Upstash Redis monitoring dashboard
|
| 155 |
+
- Add application logging with structured JSON output
|
| 156 |
+
- Consider adding Sentry for error tracking
|
package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "codesync",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"workspaces": ["apps/*", "packages/*"],
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "turbo dev",
|
| 8 |
+
"build": "turbo build",
|
| 9 |
+
"lint": "turbo lint"
|
| 10 |
+
},
|
| 11 |
+
"devDependencies": {
|
| 12 |
+
"turbo": "^2.0.0",
|
| 13 |
+
"typescript": "^5.4.0"
|
| 14 |
+
},
|
| 15 |
+
"packageManager": "pnpm@9.0.0"
|
| 16 |
+
}
|
packages/shared/package.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@codesync/shared",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"main": "./src/index.ts",
|
| 5 |
+
"types": "./src/index.ts"
|
| 6 |
+
}
|
packages/shared/src/index.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ─────────────────────────────────────────────────────────────
|
| 2 |
+
// @codesync/shared — Shared types and constants
|
| 3 |
+
// ─────────────────────────────────────────────────────────────
|
| 4 |
+
|
| 5 |
+
// User colors for cursor presence
|
| 6 |
+
export const USER_COLORS = [
|
| 7 |
+
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
|
| 8 |
+
'#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F',
|
| 9 |
+
'#BB8FCE', '#85C1E9', '#F8C471', '#82E0AA',
|
| 10 |
+
] as const;
|
| 11 |
+
|
| 12 |
+
// Rate limiting configuration
|
| 13 |
+
export const RATE_LIMITS = {
|
| 14 |
+
AUTH: { windowMs: 15 * 60 * 1000, max: 10 },
|
| 15 |
+
API: { windowMs: 60 * 1000, max: 100 },
|
| 16 |
+
EXECUTION: { windowMs: 60 * 1000, max: 10 },
|
| 17 |
+
AI: { windowMs: 60 * 1000, max: 20 },
|
| 18 |
+
WEBSOCKET: { windowMs: 1000, max: 50 },
|
| 19 |
+
} as const;
|
| 20 |
+
|
| 21 |
+
// Room constraints
|
| 22 |
+
export const ROOM_LIMITS = {
|
| 23 |
+
MAX_MEMBERS: 10,
|
| 24 |
+
MAX_FILES: 20,
|
| 25 |
+
MAX_FILE_SIZE: 1024 * 1024,
|
| 26 |
+
MAX_MESSAGE_LENGTH: 4096,
|
| 27 |
+
MAX_ROOM_NAME_LENGTH: 64,
|
| 28 |
+
INVITE_CODE_LENGTH: 8,
|
| 29 |
+
} as const;
|
| 30 |
+
|
| 31 |
+
// Execution constraints
|
| 32 |
+
export const EXECUTION_LIMITS = {
|
| 33 |
+
MAX_CODE_SIZE: 64 * 1024,
|
| 34 |
+
MAX_TIMEOUT: 30000,
|
| 35 |
+
DEFAULT_TIMEOUT: 10000,
|
| 36 |
+
MAX_OUTPUT_SIZE: 1024 * 1024,
|
| 37 |
+
MAX_CONCURRENT: 5,
|
| 38 |
+
} as const;
|
| 39 |
+
|
| 40 |
+
// Language configurations
|
| 41 |
+
export const LANGUAGE_CONFIG = {
|
| 42 |
+
javascript: {
|
| 43 |
+
name: 'JavaScript', extension: '.js', monacoId: 'javascript',
|
| 44 |
+
dockerImage: 'codesync-sandbox-js', runCommand: 'node',
|
| 45 |
+
timeout: 10000, memoryLimit: '128m',
|
| 46 |
+
},
|
| 47 |
+
typescript: {
|
| 48 |
+
name: 'TypeScript', extension: '.ts', monacoId: 'typescript',
|
| 49 |
+
dockerImage: 'codesync-sandbox-js', runCommand: 'npx tsx',
|
| 50 |
+
timeout: 15000, memoryLimit: '128m',
|
| 51 |
+
},
|
| 52 |
+
python: {
|
| 53 |
+
name: 'Python', extension: '.py', monacoId: 'python',
|
| 54 |
+
dockerImage: 'codesync-sandbox-python', runCommand: 'python3',
|
| 55 |
+
timeout: 10000, memoryLimit: '128m',
|
| 56 |
+
},
|
| 57 |
+
cpp: {
|
| 58 |
+
name: 'C++', extension: '.cpp', monacoId: 'cpp',
|
| 59 |
+
dockerImage: 'codesync-sandbox-cpp', runCommand: 'g++',
|
| 60 |
+
timeout: 15000, memoryLimit: '256m',
|
| 61 |
+
},
|
| 62 |
+
java: {
|
| 63 |
+
name: 'Java', extension: '.java', monacoId: 'java',
|
| 64 |
+
dockerImage: 'codesync-sandbox-java', runCommand: 'javac',
|
| 65 |
+
timeout: 20000, memoryLimit: '256m',
|
| 66 |
+
},
|
| 67 |
+
} as const;
|
| 68 |
+
|
| 69 |
+
// ─── Shared Types ────────────────────────────────────────────
|
| 70 |
+
|
| 71 |
+
export interface User {
|
| 72 |
+
id: string;
|
| 73 |
+
email: string;
|
| 74 |
+
name: string;
|
| 75 |
+
avatar: string | null;
|
| 76 |
+
provider: string;
|
| 77 |
+
createdAt: string;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
export interface AuthTokens {
|
| 81 |
+
accessToken: string;
|
| 82 |
+
refreshToken: string;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
export interface Room {
|
| 86 |
+
id: string;
|
| 87 |
+
name: string;
|
| 88 |
+
ownerId: string;
|
| 89 |
+
language: string;
|
| 90 |
+
isPublic: boolean;
|
| 91 |
+
inviteCode: string;
|
| 92 |
+
maxMembers: number;
|
| 93 |
+
createdAt: string;
|
| 94 |
+
updatedAt: string;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
export interface RoomMember {
|
| 98 |
+
userId: string;
|
| 99 |
+
roomId: string;
|
| 100 |
+
role: string;
|
| 101 |
+
joinedAt: string;
|
| 102 |
+
lastActive: string;
|
| 103 |
+
user: User;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
export interface RoomFile {
|
| 107 |
+
id: string;
|
| 108 |
+
roomId: string;
|
| 109 |
+
name: string;
|
| 110 |
+
path: string;
|
| 111 |
+
content: string;
|
| 112 |
+
language: string;
|
| 113 |
+
createdAt: string;
|
| 114 |
+
updatedAt: string;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
export type ProgrammingLanguage = 'javascript' | 'typescript' | 'python' | 'cpp' | 'java';
|
| 118 |
+
export type RoomRole = 'owner' | 'editor' | 'viewer';
|
| 119 |
+
export type MessageType = 'text' | 'code' | 'file' | 'system';
|
| 120 |
+
export type AIAction = 'explain' | 'fix' | 'optimize' | 'comment' | 'debug';
|
| 121 |
+
|
| 122 |
+
export interface ChatMessage {
|
| 123 |
+
id: string;
|
| 124 |
+
content: string;
|
| 125 |
+
userId: string;
|
| 126 |
+
userName: string;
|
| 127 |
+
userAvatar: string | null;
|
| 128 |
+
roomId: string;
|
| 129 |
+
type: MessageType;
|
| 130 |
+
metadata?: any;
|
| 131 |
+
createdAt: string;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
export interface UserCursor {
|
| 135 |
+
userId: string;
|
| 136 |
+
userName: string;
|
| 137 |
+
userColor: string;
|
| 138 |
+
position: { lineNumber: number; column: number };
|
| 139 |
+
selection?: {
|
| 140 |
+
startLineNumber: number;
|
| 141 |
+
startColumn: number;
|
| 142 |
+
endLineNumber: number;
|
| 143 |
+
endColumn: number;
|
| 144 |
+
};
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
export interface UserPresence {
|
| 148 |
+
userId: string;
|
| 149 |
+
userName: string;
|
| 150 |
+
userColor: string;
|
| 151 |
+
isOnline: boolean;
|
| 152 |
+
lastSeen: string;
|
| 153 |
+
isTyping: boolean;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
export interface ExecutionRequest {
|
| 157 |
+
code: string;
|
| 158 |
+
language: ProgrammingLanguage;
|
| 159 |
+
stdin?: string;
|
| 160 |
+
timeout?: number;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
export interface ExecutionResult {
|
| 164 |
+
id: string;
|
| 165 |
+
stdout: string;
|
| 166 |
+
stderr: string;
|
| 167 |
+
exitCode: number;
|
| 168 |
+
duration: number;
|
| 169 |
+
memoryUsed: number;
|
| 170 |
+
timedOut: boolean;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
export interface CreateRoomPayload {
|
| 174 |
+
name: string;
|
| 175 |
+
language: ProgrammingLanguage;
|
| 176 |
+
isPublic: boolean;
|
| 177 |
+
maxMembers?: number;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
export interface AIRequest {
|
| 181 |
+
action: AIAction;
|
| 182 |
+
code: string;
|
| 183 |
+
language: ProgrammingLanguage;
|
| 184 |
+
prompt?: string;
|
| 185 |
+
error?: string;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
export interface AIResponse {
|
| 189 |
+
content: string;
|
| 190 |
+
suggestions?: Array<{ description: string; code: string }>;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
export interface AIMessage {
|
| 194 |
+
id: string;
|
| 195 |
+
role: 'user' | 'assistant';
|
| 196 |
+
content: string;
|
| 197 |
+
timestamp: string;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
export interface WebRTCSignal {
|
| 201 |
+
type: 'offer' | 'answer' | 'ice-candidate';
|
| 202 |
+
from: string;
|
| 203 |
+
to: string;
|
| 204 |
+
payload: any;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
export interface PeerConnection {
|
| 208 |
+
peerId: string;
|
| 209 |
+
peerName: string;
|
| 210 |
+
isAudioEnabled: boolean;
|
| 211 |
+
isVideoEnabled: boolean;
|
| 212 |
+
isScreenSharing: boolean;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
export interface DashboardStats {
|
| 216 |
+
totalRooms: number;
|
| 217 |
+
totalExecutions: number;
|
| 218 |
+
totalCollaborationTime: number;
|
| 219 |
+
recentActivity: any[];
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
export interface Snippet {
|
| 223 |
+
id: string;
|
| 224 |
+
userId: string;
|
| 225 |
+
title: string;
|
| 226 |
+
code: string;
|
| 227 |
+
language: string;
|
| 228 |
+
description?: string;
|
| 229 |
+
isPublic: boolean;
|
| 230 |
+
createdAt: string;
|
| 231 |
+
updatedAt: string;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Socket event interfaces
|
| 235 |
+
export interface ServerToClientEvents {
|
| 236 |
+
'room:joined': (data: any) => void;
|
| 237 |
+
'room:member-joined': (data: any) => void;
|
| 238 |
+
'room:member-left': (data: any) => void;
|
| 239 |
+
'doc:sync': (data: any) => void;
|
| 240 |
+
'doc:state': (data: any) => void;
|
| 241 |
+
'presence:update': (data: any) => void;
|
| 242 |
+
'cursor:update': (data: any) => void;
|
| 243 |
+
'chat:message': (data: any) => void;
|
| 244 |
+
'chat:typing': (data: any) => void;
|
| 245 |
+
'chat:history': (data: any) => void;
|
| 246 |
+
'exec:output': (data: any) => void;
|
| 247 |
+
'exec:complete': (data: any) => void;
|
| 248 |
+
'exec:error': (data: any) => void;
|
| 249 |
+
'webrtc:offer': (data: any) => void;
|
| 250 |
+
'webrtc:answer': (data: any) => void;
|
| 251 |
+
'webrtc:ice-candidate': (data: any) => void;
|
| 252 |
+
'webrtc:peer-joined': (data: any) => void;
|
| 253 |
+
'webrtc:peer-left': (data: any) => void;
|
| 254 |
+
'error': (data: any) => void;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
export interface ClientToServerEvents {
|
| 258 |
+
'room:join': (data: any) => void;
|
| 259 |
+
'room:leave': (data: any) => void;
|
| 260 |
+
'doc:update': (data: any) => void;
|
| 261 |
+
'doc:request-state': (data: any) => void;
|
| 262 |
+
'presence:update': (data: any) => void;
|
| 263 |
+
'cursor:move': (data: any) => void;
|
| 264 |
+
'chat:send': (data: any) => void;
|
| 265 |
+
'chat:typing': (data: any) => void;
|
| 266 |
+
'chat:history': (data: any) => void;
|
| 267 |
+
'exec:run': (data: any) => void;
|
| 268 |
+
'exec:kill': (data: any) => void;
|
| 269 |
+
'webrtc:join': () => void;
|
| 270 |
+
'webrtc:leave': () => void;
|
| 271 |
+
'webrtc:offer': (data: any) => void;
|
| 272 |
+
'webrtc:answer': (data: any) => void;
|
| 273 |
+
'webrtc:ice-candidate': (data: any) => void;
|
| 274 |
+
}
|
pnpm-workspace.yaml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
packages:
|
| 2 |
+
- "apps/*"
|
| 3 |
+
- "packages/*"
|
turbo.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://turbo.build/schema.json",
|
| 3 |
+
"tasks": {
|
| 4 |
+
"build": { "dependsOn": ["^build"], "outputs": [".next/**", "dist/**"] },
|
| 5 |
+
"dev": { "cache": false, "persistent": true },
|
| 6 |
+
"lint": { "dependsOn": ["^build"] }
|
| 7 |
+
}
|
| 8 |
+
}
|