vzcode 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +20 -5
  2. package/dist/assets/index-ccd52a89.css +1 -0
  3. package/dist/assets/index-d2d6b59b.js +89 -0
  4. package/dist/index.html +17 -3
  5. package/package.json +26 -21
  6. package/src/client/{App.jsx → App.tsx} +75 -120
  7. package/src/client/CodeEditor/getOrCreateEditor.ts +113 -0
  8. package/src/client/CodeEditor/index.tsx +50 -0
  9. package/src/client/{json1PresenceDisplay.js → CodeEditor/json1PresenceDisplay.js} +2 -2
  10. package/src/client/CodeEditor/style.scss +11 -0
  11. package/src/client/Settings.tsx +76 -0
  12. package/src/client/Sidebar/DirectoryListing.tsx +77 -0
  13. package/src/client/Sidebar/{File.jsx → FileListing.tsx} +14 -4
  14. package/src/client/Sidebar/Listing.tsx +44 -0
  15. package/src/client/Sidebar/index.tsx +81 -0
  16. package/src/client/Sidebar/styles.scss +84 -0
  17. package/src/client/TabList/index.tsx +82 -0
  18. package/src/client/TabList/style.scss +32 -0
  19. package/src/client/{getFileTree.js → getFileTree.ts} +6 -3
  20. package/src/client/main.jsx +1 -1
  21. package/src/client/{sortFileTree.js → sortFileTree.ts} +7 -3
  22. package/src/client/style.scss +49 -0
  23. package/src/client/themes.ts +31 -0
  24. package/src/client/{Sidebar/useOpenDirectories.js → useOpenDirectories.ts} +18 -14
  25. package/src/client/useTabsState.ts +51 -0
  26. package/src/server/index.js +0 -0
  27. package/src/types.ts +69 -0
  28. package/dist/assets/index-29b6a606.css +0 -1
  29. package/dist/assets/index-523ecad7.js +0 -89
  30. package/src/client/CodeEditor.jsx +0 -31
  31. package/src/client/Sidebar/Directory.jsx +0 -62
  32. package/src/client/Sidebar/FileOrDirectory.jsx +0 -33
  33. package/src/client/Sidebar/index.jsx +0 -64
  34. package/src/client/Sidebar/styles.css +0 -88
  35. package/src/client/getOrCreateEditor.js +0 -84
  36. package/src/client/settings.jsx +0 -141
  37. package/src/client/style.css +0 -120
  38. /package/src/client/{json1PresenceBroadcast.js → CodeEditor/json1PresenceBroadcast.js} +0 -0
package/dist/index.html CHANGED
@@ -1,11 +1,25 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>VZCode</title>
7
- <script type="module" crossorigin src="/assets/index-523ecad7.js"></script>
8
- <link rel="stylesheet" href="/assets/index-29b6a606.css">
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link
14
+ href="https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css"
15
+ rel="stylesheet"
16
+ />
17
+ <link
18
+ href="https://unpkg.com/bootstrap@5.3.0/dist/css/bootstrap.min.css"
19
+ rel="stylesheet"
20
+ />
21
+ <script type="module" crossorigin src="/assets/index-d2d6b59b.js"></script>
22
+ <link rel="stylesheet" href="/assets/index-ccd52a89.css">
9
23
  </head>
10
24
  <body>
11
25
  <div id="root"></div>
package/package.json CHANGED
@@ -1,18 +1,20 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
+ "types": "src/types.ts",
7
8
  "bin": "src/server/index.js",
8
9
  "files": [
9
10
  "dist/**",
10
11
  "src/**"
11
12
  ],
12
13
  "scripts": {
13
- "test": "vitest",
14
+ "test": "vitest run",
14
15
  "test-interactive": "npm run build; cd test/sampleDirectories/kitchenSink; node ../../../src/server/index.js",
15
16
  "prettier": "prettier {*.*,**/*.*} --write",
17
+ "tsc": "tsc --noEmit",
16
18
  "dev": "vite",
17
19
  "build": "vite build",
18
20
  "build-release": "vite build --mode release",
@@ -44,36 +46,39 @@
44
46
  },
45
47
  "homepage": "https://github.com/vizhub-core/vzcode#readme",
46
48
  "dependencies": {
47
- "@codemirror/lang-css": "^6.1.1",
48
- "@codemirror/lang-html": "^6.4.2",
49
- "@codemirror/lang-javascript": "^6.1.4",
50
- "@codemirror/lang-markdown": "^6.1.0",
51
- "@codemirror/state": "^6.2.0",
52
- "@codemirror/theme-one-dark": "^6.1.1",
49
+ "@codemirror/lang-css": "^6.2.0",
50
+ "@codemirror/lang-html": "^6.4.5",
51
+ "@codemirror/lang-javascript": "^6.1.9",
52
+ "@codemirror/lang-markdown": "^6.2.0",
53
+ "@codemirror/state": "^6.2.1",
54
+ "@codemirror/theme-one-dark": "^6.1.2",
53
55
  "@teamwork/websocket-json-stream": "^2.0.0",
56
+ "@uiw/codemirror-themes-all": "^4.21.8",
54
57
  "codemirror": "^6.0.1",
55
58
  "codemirror-ot": "^4.0.0",
56
- "d3-array": "^3.2.3",
59
+ "d3-array": "^3.2.4",
57
60
  "diff-match-patch": "^1.0.5",
58
- "dotenv": "^16.0.3",
61
+ "dotenv": "^16.3.1",
59
62
  "express": "^4.18.2",
60
63
  "json0-ot-diff": "^1.1.2",
61
64
  "ngrok": "^4.3.3",
62
- "open": "^9.0.0",
65
+ "open": "^9.1.0",
63
66
  "react": "^18.2.0",
64
- "react-bootstrap": "^2.7.2",
67
+ "react-bootstrap": "^2.8.0",
65
68
  "react-dom": "^18.2.0",
66
- "react-select": "^5.7.1",
67
- "sharedb": "^3.3.0",
68
- "sharedb-client-browser": "^4.2.0",
69
+ "react-select": "^5.7.4",
70
+ "sharedb": "^4.0.0",
71
+ "sharedb-client-browser": "^4.2.1",
69
72
  "ws": "^8.13.0"
70
73
  },
71
74
  "devDependencies": {
72
- "@types/react": "^18.0.28",
73
- "@types/react-dom": "^18.0.11",
74
- "@vitejs/plugin-react": "^3.1.0",
75
- "prettier": "^2.8.6",
76
- "vite": "^4.2.1",
77
- "vitest": "^0.30.1"
75
+ "@types/react": "^18.2.17",
76
+ "@types/react-dom": "^18.2.7",
77
+ "@vitejs/plugin-react": "^4.0.3",
78
+ "prettier": "^3.0.0",
79
+ "sass": "^1.64.1",
80
+ "vite": "^4.4.7",
81
+ "vitest": "^0.33.0",
82
+ "typescript": "^5.1.6"
78
83
  }
79
84
  }
@@ -1,12 +1,17 @@
1
- import { useState, useEffect, useMemo, useCallback } from 'react';
1
+ import { useState, useEffect, useCallback } from 'react';
2
2
  import ShareDBClient from 'sharedb-client-browser/dist/sharedb-client-umd.cjs';
3
3
  import { json1Presence } from '../ot';
4
4
  import { randomId } from '../randomId';
5
5
  import { CodeEditor } from './CodeEditor';
6
6
  import { diff } from './diff';
7
- import { Settings } from './settings';
7
+ import { Settings } from './Settings';
8
8
  import { Sidebar } from './Sidebar';
9
- import './style.css';
9
+ import { FileId, Files } from '../types';
10
+ import { TabList } from './TabList';
11
+ import { useOpenDirectories } from './useOpenDirectories';
12
+ import { useTabsState } from './useTabsState';
13
+ import { defaultTheme } from './themes';
14
+ import './style.scss';
10
15
 
11
16
  // Register our custom JSON1 OT type that supports presence.
12
17
  // See https://github.com/vizhub-core/json1-presence
@@ -34,22 +39,30 @@ function App() {
34
39
 
35
40
  // The `doc.data` part of the ShareDB document,
36
41
  // updated on each change to decouple rendering from ShareDB.
37
- const [data, setData] = useState(null);
38
-
39
- // True if the file menu is open.
40
- const [isFileMenuOpen, setIsFileMenuOpen] = useState(false);
42
+ const [data, setData] = useState<Files>(null);
41
43
 
42
44
  // The id of the currently open file tab.
43
- const [activeFileId, setActiveFileId] = useState(null);
45
+ const [activeFileId, setActiveFileId] = useState<FileId>(null);
44
46
 
45
47
  // The ordered list of tabs.
46
- const [tabList, setTabList] = useState([]);
48
+ const [tabList, setTabList] = useState<Array<FileId>>([]);
49
+
50
+ // Logic for opening and closing tabs.
51
+ const { closeTab, openTab } = useTabsState(
52
+ activeFileId,
53
+ setActiveFileId,
54
+ tabList,
55
+ setTabList,
56
+ );
47
57
 
48
58
  // The current theme.
49
- const [theme, setTheme] = useState('OneDark');
59
+ const [theme, setTheme] = useState<string>(defaultTheme);
50
60
 
51
61
  // True to show the settings modal.
52
- const [settings, setSettings] = useState(false);
62
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false);
63
+
64
+ // The set of open directories.
65
+ const { isDirectoryOpen, toggleDirectory } = useOpenDirectories();
53
66
 
54
67
  // Set up the connection to ShareDB.
55
68
  useEffect(() => {
@@ -75,7 +88,7 @@ function App() {
75
88
  // Listen for all changes and update `data`.
76
89
  // This decouples rendering logic from ShareDB.
77
90
  // This callback gets called on each change.
78
- shareDBDoc.on('op', (op) => {
91
+ shareDBDoc.on('op', () => {
79
92
  setData(shareDBDoc.data);
80
93
  });
81
94
 
@@ -97,100 +110,58 @@ function App() {
97
110
 
98
111
  // TODO unsubscribe from presence
99
112
  // TODO unsubscribe from doc
113
+ return () => {
114
+ // shareDBDoc.destroy();
115
+ // docPresence.destroy();
116
+ };
100
117
  }, []);
101
118
 
102
- // Called when a tab is closed.
103
- const closeTab = useCallback(
104
- (fileIdToRemove) => {
105
- const i = tabList.findIndex((fileId) => fileId === fileIdToRemove);
106
-
107
- // Support calling `closeTab` on a fileId that is not open,
108
- // such as when a file is deleted..
109
- if (i !== -1) {
110
- // Remove the tab from the tab list.
111
- const newTabList = [...tabList.slice(0, i), ...tabList.slice(i + 1)];
112
- setTabList(newTabList);
113
-
114
- // If we are closing the active file,
115
- if (activeFileId === fileIdToRemove) {
116
- // set the new active file to the next tab over,
117
- if (newTabList.length > 0) {
118
- setActiveFileId(i === 0 ? newTabList[i] : newTabList[i - 1]);
119
- } else {
120
- // or clear out the active file
121
- // if we've closed the last tab.
122
- setActiveFileId(null);
123
- }
124
- }
125
- }
126
- },
127
- [tabList, activeFileId]
128
- );
129
-
130
- const handleCloseTabClick = useCallback(
131
- (fileIdToRemove) => (event) => {
132
- // Stop propagation so that the outer listener doesn't fire.
133
- event.stopPropagation();
134
- closeTab(fileIdToRemove);
135
- },
136
- [closeTab]
137
- );
138
-
139
119
  // Called when a file in the sidebar is double-clicked.
140
120
  const handleRenameFileClick = useCallback(
141
- (key) => {
121
+ (fileId: FileId) => {
142
122
  const newName = prompt('Enter new name');
143
123
  if (newName) {
144
124
  const currentDocument = shareDBDoc.data;
145
125
  const nextDocument = {
146
126
  ...currentDocument,
147
- [key]: {
148
- ...currentDocument[key],
127
+ [fileId]: {
128
+ ...currentDocument[fileId],
149
129
  name: newName,
150
130
  },
151
131
  };
152
132
  shareDBDoc.submitOp(diff(currentDocument, nextDocument));
153
133
  }
154
134
  },
155
- [shareDBDoc]
135
+ [shareDBDoc],
156
136
  );
157
137
 
158
138
  const deleteFile = useCallback(
159
- (key) => {
139
+ (fileId: FileId) => {
160
140
  // Close the tab in case it's open.
161
141
  // TODO consistently use either "key" or "fileId" naming
162
- closeTab(key);
142
+ closeTab(fileId);
163
143
 
164
144
  const currentDocument = shareDBDoc.data;
165
145
  const nextDocument = { ...currentDocument };
166
- delete nextDocument[key];
146
+ delete nextDocument[fileId];
167
147
  shareDBDoc.submitOp(diff(currentDocument, nextDocument));
168
148
  },
169
- [shareDBDoc, closeTab]
170
- );
171
-
172
- const handleFileClick = useCallback(
173
- (fileId) => {
174
- setActiveFileId(fileId);
175
- if (!tabList.includes(fileId)) {
176
- setTabList([...tabList, fileId]);
177
- }
178
- },
179
- [tabList]
149
+ [shareDBDoc, closeTab],
180
150
  );
181
151
 
182
152
  // TODO prompt the user "Are you sure?"
183
153
  const handleDeleteFileClick = useCallback(
184
- (fileId) => (event) => {
154
+ (fileId: FileId, event: React.MouseEvent) => {
185
155
  // Stop propagation so that the outer listener doesn't fire,
186
156
  // which would try to open this file in a tab.
187
157
  event.stopPropagation();
188
158
  deleteFile(fileId);
189
159
  },
190
- [deleteFile]
160
+ [deleteFile],
191
161
  );
192
162
 
193
163
  const createFile = useCallback(() => {
164
+ // TODO better UI, maybe Bootstrap modal? Maybe edit inline?
194
165
  const name = prompt('Enter new file name');
195
166
  if (!name) return;
196
167
  const currentDocument = shareDBDoc.data;
@@ -201,64 +172,48 @@ function App() {
201
172
  shareDBDoc.submitOp(diff(currentDocument, nextDocument));
202
173
  }, [shareDBDoc]);
203
174
 
204
- // True if we are ready to actually render the active tab.
205
- const tabValid = data && activeFileId;
206
-
207
175
  const handleSettingsClose = useCallback(() => {
208
- setSettings(false);
176
+ setIsSettingsOpen(false);
209
177
  }, []);
210
178
 
211
- const fileNameSplit = (fileName) => {
212
- const split = fileName.split('/');
213
- return split[split.length - 1];
214
- };
215
-
216
179
  return (
217
- <>
218
- <Settings
219
- show={settings}
220
- onClose={handleSettingsClose}
221
- setTheme={setTheme}
222
- />
223
- <div className="tab-list">
224
- {tabList.map((fileId) => (
225
- <div
226
- key={fileId}
227
- className={
228
- tabValid ? `tab${fileId === activeFileId ? ' active' : ''}` : null
229
- }
230
- onClick={() => {
231
- setActiveFileId(fileId);
232
- }}
233
- >
234
- {tabValid ? fileNameSplit(data[fileId].name) : ''}
235
- <div
236
- className={activeFileId ? 'bx bx-x tab-close' : ''}
237
- onClick={handleCloseTabClick(fileId)}
238
- ></div>
239
- </div>
240
- ))}
180
+ <div className="app">
181
+ <div className="left">
182
+ <Sidebar
183
+ createFile={createFile}
184
+ files={data}
185
+ handleRenameFileClick={handleRenameFileClick}
186
+ handleDeleteFileClick={handleDeleteFileClick}
187
+ handleFileClick={openTab}
188
+ setIsSettingsOpen={setIsSettingsOpen}
189
+ isDirectoryOpen={isDirectoryOpen}
190
+ toggleDirectory={toggleDirectory}
191
+ />
192
+ <Settings
193
+ show={isSettingsOpen}
194
+ onClose={handleSettingsClose}
195
+ setTheme={setTheme}
196
+ />
241
197
  </div>
242
- <Sidebar
243
- createFile={createFile}
244
- data={data}
245
- handleRenameFileClick={handleRenameFileClick}
246
- handleDeleteFileClick={handleDeleteFileClick}
247
- handleFileClick={handleFileClick}
248
- setSettings={setSettings}
249
- settings={settings}
250
- />
251
- {data && activeFileId ? (
252
- <CodeEditor
253
- className="editor"
254
- shareDBDoc={shareDBDoc}
255
- localPresence={localPresence}
256
- docPresence={docPresence}
198
+ <div className="right">
199
+ <TabList
200
+ files={data}
201
+ tabList={tabList}
257
202
  activeFileId={activeFileId}
258
- theme={theme}
203
+ setActiveFileId={setActiveFileId}
204
+ closeTab={closeTab}
259
205
  />
260
- ) : null}
261
- </>
206
+ {data && activeFileId ? (
207
+ <CodeEditor
208
+ shareDBDoc={shareDBDoc}
209
+ localPresence={localPresence}
210
+ docPresence={docPresence}
211
+ activeFileId={activeFileId}
212
+ theme={theme}
213
+ />
214
+ ) : null}
215
+ </div>
216
+ </div>
262
217
  );
263
218
  }
264
219
 
@@ -0,0 +1,113 @@
1
+ import { EditorView, basicSetup } from 'codemirror';
2
+ import { Compartment, EditorState } from '@codemirror/state';
3
+ import { javascript } from '@codemirror/lang-javascript';
4
+ import { markdown } from '@codemirror/lang-markdown';
5
+ import { html } from '@codemirror/lang-html';
6
+ import { css } from '@codemirror/lang-css';
7
+ import { json1Sync } from 'codemirror-ot';
8
+ import { json1Presence, textUnicode } from '../../ot';
9
+ import { FileId, ShareDBDoc } from '../../types';
10
+ import { json1PresenceBroadcast } from './json1PresenceBroadcast';
11
+ import { json1PresenceDisplay } from './json1PresenceDisplay';
12
+
13
+ // Singleton cache of CodeMirror instances
14
+ // These are created, but never destroyed.
15
+ // TODO consider invalidating this cache at some point.
16
+ // * Keys are file ids
17
+ // * Values are CodeMirror instances (TODO add CodeMirror type)
18
+ const editorCache = new Map<FileId, any>();
19
+
20
+ // Gets a value at a path in an object.
21
+ // e.g. getAtPath({a: {b: 1}}, ['a', 'b']) === 1
22
+ const getAtPath = (obj, path) => {
23
+ let current = obj;
24
+ for (const key of path) {
25
+ if (!current) return undefined;
26
+ current = current[key];
27
+ }
28
+ return current;
29
+ };
30
+
31
+ // Gets or creates a CodeMirror editor for the given file id.
32
+ export const getOrCreateEditor = ({
33
+ fileId,
34
+ shareDBDoc,
35
+ filesPath,
36
+ localPresence,
37
+ docPresence,
38
+ theme,
39
+ }: {
40
+ fileId: FileId;
41
+ shareDBDoc: ShareDBDoc<any>;
42
+ filesPath: string[];
43
+ localPresence: any;
44
+ docPresence: any;
45
+ theme: any;
46
+ }) => {
47
+ const data = shareDBDoc.data;
48
+ const textPath = [...filesPath, fileId, 'text'];
49
+ const namePath = [...filesPath, fileId, 'name'];
50
+
51
+ const text = getAtPath(data, textPath);
52
+ const name = getAtPath(data, namePath);
53
+
54
+ const fileExtension = name.split('.').pop();
55
+
56
+ // TODO revisit if this is the right approach for dynamic themes
57
+ let themeSet = new Compartment();
58
+
59
+ const extensions = [];
60
+
61
+ // This plugin implements multiplayer editing,
62
+ // real-time synchronozation of changes across clients.
63
+ // Does not deal with showing others' cursors.
64
+ extensions.push(
65
+ json1Sync({
66
+ shareDBDoc,
67
+ path: textPath,
68
+ json1: json1Presence,
69
+ textUnicode,
70
+ }),
71
+ );
72
+
73
+ // Deals with broadcasting changes in cursor location and selection.
74
+ if (localPresence)
75
+ extensions.push(json1PresenceBroadcast({ path: textPath, localPresence }));
76
+
77
+ // Deals with receiving the broadcas from other clients and displaying them.
78
+ if (docPresence)
79
+ extensions.push(json1PresenceDisplay({ path: textPath, docPresence }));
80
+
81
+ extensions.push(basicSetup);
82
+ extensions.push(themeSet.of(theme));
83
+
84
+ // TODO handle dynamic changing of the file extension.
85
+ if (
86
+ fileExtension === 'js' ||
87
+ fileExtension === 'json' ||
88
+ fileExtension === 'jsx' ||
89
+ fileExtension === 'ts'
90
+ ) {
91
+ extensions.push(javascript());
92
+ } else if (fileExtension === 'html') {
93
+ extensions.push(html());
94
+ } else if (fileExtension === 'css') {
95
+ extensions.push(css());
96
+ } else if (fileExtension === 'md') {
97
+ extensions.push(markdown());
98
+ }
99
+
100
+ let editor = editorCache.get(fileId);
101
+ if (!editor) {
102
+ editor = new EditorView({
103
+ state: EditorState.create({
104
+ doc: text,
105
+ extensions,
106
+ }),
107
+ });
108
+
109
+ // Populate the cache.
110
+ editorCache.set(fileId, editor);
111
+ }
112
+ return editor;
113
+ };
@@ -0,0 +1,50 @@
1
+ import { useRef, useLayoutEffect } from 'react';
2
+ import { FileId } from '../../types';
3
+ import { defaultTheme, themesByLabel } from '../themes';
4
+ import { getOrCreateEditor } from './getOrCreateEditor';
5
+ import './style.scss';
6
+
7
+ export const CodeEditor = ({
8
+ activeFileId,
9
+ shareDBDoc,
10
+ localPresence,
11
+ docPresence,
12
+ theme = defaultTheme,
13
+ filesPath = [],
14
+ }: {
15
+ activeFileId: FileId;
16
+ shareDBDoc: any;
17
+ localPresence?: any;
18
+ docPresence?: any;
19
+ theme?: string;
20
+
21
+ // The path of the files object in the ShareDB document.
22
+ // Defaults to the root of the document.
23
+ filesPath?: string[];
24
+ }) => {
25
+ const ref = useRef<HTMLDivElement>(null);
26
+
27
+ useLayoutEffect(() => {
28
+ if (!ref.current) return;
29
+ if (!shareDBDoc) return;
30
+
31
+ // `getOrCreateEditor` gets called
32
+ // every time the active file changes.
33
+ const editor = getOrCreateEditor({
34
+ fileId: activeFileId,
35
+ shareDBDoc,
36
+ filesPath,
37
+ localPresence,
38
+ docPresence,
39
+ // TODO refactor this, make dynamic themes work
40
+ theme: themesByLabel[theme],
41
+ });
42
+ ref.current.appendChild(editor.dom);
43
+
44
+ return () => {
45
+ ref.current.removeChild(editor.dom);
46
+ };
47
+ }, [shareDBDoc, activeFileId]);
48
+
49
+ return <div className="vz-code-editor" ref={ref} />;
50
+ };
@@ -58,7 +58,7 @@ export const json1PresenceDisplay = ({ path, docPresence }) => [
58
58
  }),
59
59
  // Without this argument, we get the following error:
60
60
  // Uncaught Error: Ranges must be added sorted by `from` position and `startSide`
61
- true
61
+ true,
62
62
  );
63
63
 
64
64
  // Somehow this triggers re-rendering of the Decorations.
@@ -74,7 +74,7 @@ export const json1PresenceDisplay = ({ path, docPresence }) => [
74
74
  },
75
75
  {
76
76
  decorations: (v) => v.decorations,
77
- }
77
+ },
78
78
  ),
79
79
  presenceTheme,
80
80
  ];
@@ -0,0 +1,11 @@
1
+ .vz-code-editor {
2
+ flex: 1;
3
+ display: flex;
4
+ font-family: var(--vzcode-font-family);
5
+ overflow-x: auto;
6
+
7
+ /* TODO QA this */
8
+ .cm-editor {
9
+ flex: 1;
10
+ }
11
+ }
@@ -0,0 +1,76 @@
1
+ import { useCallback } from 'react';
2
+ import Modal from 'react-bootstrap/Modal';
3
+ import Form from 'react-bootstrap/Form';
4
+ import Button from 'react-bootstrap/Button';
5
+ import Select from 'react-select';
6
+ import { themes } from './themes';
7
+
8
+ const saveTimes = [
9
+ { value: 1, label: '1 second' },
10
+ { value: 5, label: '5 seconds' },
11
+ { value: 10, label: '10 seconds' },
12
+ { value: 30, label: '30 seconds' },
13
+ ];
14
+
15
+ export const Settings = ({ show, onClose, setTheme }) => {
16
+ const handleChange = useCallback((selectedOption) => {
17
+ setTheme(selectedOption.label);
18
+ // TODO make this work.
19
+ // Sketch of algorithm:
20
+ // for each editor in the editor cache,
21
+ // update the theme
22
+ // editor.dispatch({
23
+ // effects: themeSet.reconfigure(selectedOption.value),
24
+ // });
25
+ }, []);
26
+
27
+ const handleSaveTimeChange = useCallback((selectedOption) => {
28
+ const time = [{ value: selectedOption.value }];
29
+ fetch('/saveTime', {
30
+ method: 'POST',
31
+ headers: {
32
+ 'Content-Type': 'application/json',
33
+ },
34
+ body: JSON.stringify(time),
35
+ });
36
+ }, []);
37
+
38
+ return show ? (
39
+ <Modal
40
+ className="vz-settings"
41
+ show={show}
42
+ onHide={onClose}
43
+ animation={false}
44
+ >
45
+ <Modal.Header closeButton>
46
+ <Modal.Title>Settings</Modal.Title>
47
+ </Modal.Header>
48
+ <Modal.Body>
49
+ <Form.Group className="mb-3" controlId="formFork">
50
+ <Form.Label>Username</Form.Label>
51
+ <Form.Control type="text" placeholder="Enter username" />
52
+ <Form.Text className="text-muted">
53
+ Enter a username to be displayed on your cursor
54
+ </Form.Text>
55
+ </Form.Group>
56
+ <Form.Group className="mb-3" controlId="formFork">
57
+ <Form.Label>Theme</Form.Label>
58
+ <Select options={themes} onChange={handleChange} />
59
+ <Form.Text className="text-muted">
60
+ Select a color theme for the editor
61
+ </Form.Text>
62
+ </Form.Group>
63
+ <Form.Group className="mb-3" controlId="formFork">
64
+ <Form.Label>Auto-Save Time</Form.Label>
65
+ <Select options={saveTimes} onChange={handleSaveTimeChange} />
66
+ <Form.Text className="text-muted">Select an auto save time</Form.Text>
67
+ </Form.Group>
68
+ </Modal.Body>
69
+ <Modal.Footer>
70
+ <Button variant="primary" onClick={onClose}>
71
+ Done
72
+ </Button>
73
+ </Modal.Footer>
74
+ </Modal>
75
+ ) : null;
76
+ };