Skip to content

Commit b202ab0

Browse files
authored
Merge pull request #72 from CoderRC/main
Implement live document editing, presence indicators, shared clipboard, and permission-based file/application sharing across users.
2 parents 86fc623 + df32808 commit b202ab0

File tree

16 files changed

+1702
-511
lines changed

16 files changed

+1702
-511
lines changed

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,19 @@
3131
"mongoose": "^8.18.2",
3232
"multer": "2.0.2",
3333
"next": "^14.0.0",
34+
"quill-delta": "^5.1.0",
3435
"react": "^18.0.0",
3536
"react-dom": "^18.0.0",
36-
"react-quill": "^0.0.2"
37+
"react-quill": "^2.0.0",
38+
"socket.io": "^4.8.1",
39+
"socket.io-client": "^4.8.1"
3740
},
3841
"devDependencies": {
3942
"@commitlint/cli": "^19.8.1",
4043
"@commitlint/config-conventional": "^19.8.1",
4144
"@testing-library/jest-dom": "^6.1.0",
4245
"@testing-library/react": "^14.0.0",
46+
"@types/quill": "^2.0.14",
4347
"autoprefixer": "^10.4.0",
4448
"babel-jest": "^29.7.0",
4549
"concurrently": "^8.2.0",

src/components/Desktop.js

Lines changed: 199 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,24 +133,213 @@ export default function Desktop() {
133133
const keyShortcutService =
134134
new WindowTopBarKeyShortcutRegistrationService();
135135

136+
// Enhanced menu handlers
136137
const menuHandlers = {
137-
onNew: () => {},
138-
onOpen: () => {},
139-
onOpenLocal: () => {},
140-
onSave: () => {},
141-
onSaveAs: () => {},
138+
onNew: () => {
139+
// Create a new blank document
140+
const newNote = {
141+
id: Date.now().toString(),
142+
title: 'Untitled Note',
143+
content: '',
144+
createdAt: new Date().toISOString(),
145+
};
146+
147+
// Save to localStorage or trigger state update
148+
const existingNotes = JSON.parse(
149+
localStorage.getItem('orbitos-notes') || '[]',
150+
);
151+
existingNotes.unshift(newNote);
152+
localStorage.setItem('orbitos-notes', JSON.stringify(existingNotes));
153+
154+
// Reload the notes app to show new document
155+
window.dispatchEvent(new CustomEvent('notes-refresh'));
156+
},
157+
onOpen: () => {
158+
// Show document picker modal
159+
const event = new CustomEvent('show-notes-picker', {
160+
detail: { action: 'open' },
161+
});
162+
window.dispatchEvent(event);
163+
},
164+
onOpenLocal: () => {
165+
// Create file input for local file opening
166+
const fileInput = document.createElement('input');
167+
fileInput.type = 'file';
168+
fileInput.accept = '.txt,.md,.json,text/plain';
169+
fileInput.style.display = 'none';
170+
171+
fileInput.onchange = (event) => {
172+
const file = event.target.files[0];
173+
if (!file) return;
174+
175+
const reader = new FileReader();
176+
reader.onload = (e) => {
177+
const content = e.target.result;
178+
const newNote = {
179+
id: Date.now().toString(),
180+
title: file.name.replace(/\.[^/.]+$/, ''), // Remove extension
181+
content: content,
182+
createdAt: new Date().toISOString(),
183+
isLocalFile: true,
184+
};
185+
186+
// Save to notes collection
187+
const existingNotes = JSON.parse(
188+
localStorage.getItem('orbitos-notes') || '[]',
189+
);
190+
existingNotes.unshift(newNote);
191+
localStorage.setItem(
192+
'orbitos-notes',
193+
JSON.stringify(existingNotes),
194+
);
195+
196+
// Load this note
197+
window.dispatchEvent(
198+
new CustomEvent('notes-load', {
199+
detail: { note: newNote },
200+
}),
201+
);
202+
};
203+
reader.readAsText(file);
204+
};
205+
206+
document.body.appendChild(fileInput);
207+
fileInput.click();
208+
document.body.removeChild(fileInput);
209+
},
210+
onSave: async () => {
211+
const currentNote = JSON.parse(
212+
localStorage.getItem('orbitos-current-note') || '{}',
213+
);
214+
const textarea = document.querySelector('.notes-textarea');
215+
const content = textarea ? textarea.value : '';
216+
217+
try {
218+
const response = await fetch('/api/files', {
219+
method: 'POST',
220+
headers: { 'Content-Type': 'application/json' },
221+
body: JSON.stringify({
222+
id: currentNote._id, // Use database ID
223+
title: currentNote.title,
224+
content: content,
225+
}),
226+
});
227+
228+
if (response.ok) {
229+
const { file } = await response.json();
230+
// Update current note with saved version from database
231+
localStorage.setItem(
232+
'orbitos-current-note',
233+
JSON.stringify(file),
234+
);
235+
window.dispatchEvent(
236+
new CustomEvent('show-notification', {
237+
detail: {
238+
message: 'Note saved successfully!',
239+
type: 'success',
240+
},
241+
}),
242+
);
243+
}
244+
} catch (error) {
245+
console.error('Save failed:', error);
246+
}
247+
},
248+
onSaveAs: async () => {
249+
const textarea = document.querySelector('.notes-textarea');
250+
const content = textarea ? textarea.value : '';
251+
const currentNote = JSON.parse(
252+
localStorage.getItem('orbitos-current-note') || '{}',
253+
);
254+
255+
const fileName = prompt(
256+
'Save as:',
257+
currentNote.name || 'Untitled Note',
258+
);
259+
if (!fileName) return;
260+
261+
try {
262+
const response = await fetch('/api/files', {
263+
method: 'POST',
264+
headers: { 'Content-Type': 'application/json' },
265+
body: JSON.stringify({
266+
name: fileName,
267+
content: content,
268+
}),
269+
});
270+
271+
if (response.ok) {
272+
const { file: savedFile } = await response.json();
273+
localStorage.setItem(
274+
'orbitos-current-note',
275+
JSON.stringify(savedFile),
276+
);
277+
278+
window.dispatchEvent(
279+
new CustomEvent('notes-title-update', {
280+
detail: { name: fileName }, // Update to use 'name'
281+
}),
282+
);
283+
284+
window.dispatchEvent(
285+
new CustomEvent('show-notification', {
286+
detail: {
287+
message: `File saved as "${fileName}"`,
288+
type: 'success',
289+
},
290+
}),
291+
);
292+
}
293+
} catch (error) {
294+
console.error('Save As failed:', error);
295+
window.dispatchEvent(
296+
new CustomEvent('show-notification', {
297+
detail: { message: 'Failed to save file', type: 'error' },
298+
}),
299+
);
300+
}
301+
},
142302
onPrint: () => window.print(),
143303
onUndo: () => document.execCommand('undo'),
144304
onRedo: () => document.execCommand('redo'),
145305
onCut: () => document.execCommand('cut'),
146306
onCopy: () => document.execCommand('copy'),
147307
onPaste: () => document.execCommand('paste'),
148-
onFind: () => {},
149-
onReplace: () => {},
308+
onFind: () => {
309+
window.dispatchEvent(
310+
new CustomEvent('toggle-find-replace', {
311+
detail: { show: true, mode: 'find' },
312+
}),
313+
);
314+
},
315+
onReplace: () => {
316+
window.dispatchEvent(
317+
new CustomEvent('toggle-find-replace', {
318+
detail: { show: true, mode: 'replace' },
319+
}),
320+
);
321+
},
150322
onFindInFiles: () => {},
151-
onFindNext: () => {},
152-
onFindPrevious: () => {},
153-
onSelectFindNext: () => {},
323+
onFindNext: () => {
324+
window.dispatchEvent(new Event('find-next'));
325+
},
326+
onFindPrevious: () => {
327+
window.dispatchEvent(new Event('find-previous'));
328+
},
329+
onSelectFindNext: () => {
330+
const textarea = document.querySelector('.notes-textarea');
331+
if (textarea && textarea.selectionStart !== textarea.selectionEnd) {
332+
const selectedText = textarea.value.substring(
333+
textarea.selectionStart,
334+
textarea.selectionEnd,
335+
);
336+
window.dispatchEvent(
337+
new CustomEvent('find-text', {
338+
detail: { text: selectedText, forward: true },
339+
}),
340+
);
341+
}
342+
},
154343
onSelectFindPrevious: () => {},
155344
onFindVolatileNext: () => {},
156345
onFindVolatilePrevious: () => {},
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React, { useState } from 'react';
2+
import { useTheme } from '@/context/ThemeContext';
3+
4+
const CollaborationToolbar = ({
5+
activeUsers,
6+
presence,
7+
onShare,
8+
documentId,
9+
}) => {
10+
const { theme } = useTheme();
11+
const [showShareDialog, setShowShareDialog] = useState(false);
12+
const [shareEmail, setShareEmail] = useState('');
13+
const [sharePermission, setSharePermission] = useState('view');
14+
15+
return (
16+
<div
17+
className={`flex items-center justify-between p-2 border-b ${theme.app.toolbar}`}
18+
>
19+
<div className="flex items-center space-x-2">
20+
<span className="text-sm">Collaborators:</span>
21+
{Object.values(presence).map((user) => (
22+
<div
23+
key={user.userId}
24+
className="flex items-center space-x-1 px-2 py-1 rounded-full bg-blue-100 text-blue-800"
25+
title={`Active: ${new Date(user.timestamp).toLocaleTimeString()}`}
26+
>
27+
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
28+
<span className="text-xs">{user.userId}</span>
29+
</div>
30+
))}
31+
</div>
32+
33+
<div className="flex items-center space-x-2">
34+
<button
35+
onClick={() => setShowShareDialog(true)}
36+
className={`px-3 py-1 rounded ${theme.app.button}`}
37+
>
38+
Share
39+
</button>
40+
</div>
41+
42+
{/* Share Dialog */}
43+
{showShareDialog && (
44+
<div className="absolute top-12 right-2 bg-white border rounded-lg shadow-lg p-4 z-50">
45+
<h3 className="font-semibold mb-2">Share Document</h3>
46+
<input
47+
type="email"
48+
placeholder="Enter email"
49+
value={shareEmail}
50+
onChange={(e) => setShareEmail(e.target.value)}
51+
className="border rounded px-2 py-1 mb-2 w-full"
52+
/>
53+
<select
54+
value={sharePermission}
55+
onChange={(e) => setSharePermission(e.target.value)}
56+
className="border rounded px-2 py-1 mb-2 w-full"
57+
>
58+
<option value="view">Can view</option>
59+
<option value="comment">Can comment</option>
60+
<option value="edit">Can edit</option>
61+
</select>
62+
<div className="flex space-x-2">
63+
<button
64+
onClick={() => {
65+
onShare(shareEmail, sharePermission);
66+
setShowShareDialog(false);
67+
}}
68+
className="flex-1 bg-blue-500 text-white rounded px-3 py-1"
69+
>
70+
Share
71+
</button>
72+
<button
73+
onClick={() => setShowShareDialog(false)}
74+
className="flex-1 bg-gray-300 rounded px-3 py-1"
75+
>
76+
Cancel
77+
</button>
78+
</div>
79+
</div>
80+
)}
81+
</div>
82+
);
83+
};
84+
85+
export default CollaborationToolbar;

0 commit comments

Comments
 (0)