vzcode 0.4.0 → 0.6.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 (33) hide show
  1. package/README.md +47 -5
  2. package/dist/assets/index-41607816.js +89 -0
  3. package/dist/assets/index-8bac68c9.css +1 -0
  4. package/dist/index.html +16 -2
  5. package/package.json +29 -19
  6. package/src/{App.jsx → client/App.tsx} +83 -126
  7. package/src/client/Sidebar/DirectoryArrowSVG.jsx +5 -0
  8. package/src/client/Sidebar/DirectoryListing.tsx +76 -0
  9. package/src/client/Sidebar/FileListing.tsx +42 -0
  10. package/src/client/Sidebar/Item.jsx +43 -0
  11. package/src/client/Sidebar/Listing.tsx +44 -0
  12. package/src/client/Sidebar/index.tsx +80 -0
  13. package/src/client/Sidebar/styles.scss +84 -0
  14. package/src/client/Sidebar/useOpenDirectories.ts +37 -0
  15. package/src/{diff.js → client/diff.js} +1 -1
  16. package/src/client/featureFlags.js +3 -0
  17. package/src/client/getFileTree.ts +52 -0
  18. package/src/{getOrCreateEditor.js → client/getOrCreateEditor.js} +6 -6
  19. package/src/{json1PresenceDisplay.js → client/json1PresenceDisplay.js} +0 -1
  20. package/src/client/settings.jsx +97 -0
  21. package/src/client/sortFileTree.ts +19 -0
  22. package/src/client/style.scss +93 -0
  23. package/src/server/computeInitialDocument.js +69 -0
  24. package/src/server/featureFlags.js +4 -0
  25. package/{cli.js → src/server/index.js} +24 -30
  26. package/src/types.ts +57 -0
  27. package/dist/assets/index-48dbd5c3.js +0 -89
  28. package/dist/assets/index-8d2d4b3b.css +0 -1
  29. package/src/settings.jsx +0 -141
  30. package/src/style.css +0 -215
  31. /package/src/{CodeEditor.jsx → client/CodeEditor.jsx} +0 -0
  32. /package/src/{json1PresenceBroadcast.js → client/json1PresenceBroadcast.js} +0 -0
  33. /package/src/{main.jsx → client/main.jsx} +0 -0
@@ -0,0 +1 @@
1
+ .vz-sidebar{height:100%;width:200px;background:rgb(54,65,89);display:flex;flex-direction:column;justify-content:space-between}.vz-sidebar .full-box{padding:7px;display:flex;flex-direction:row;align-items:center;justify-content:space-between}.vz-sidebar .files{overflow:auto}.vz-sidebar .file-or-directory{display:flex;flex-direction:row;align-items:center;justify-content:space-between;cursor:pointer;margin:0 10px;color:#dbdde1;border-radius:5px}.vz-sidebar .link-name{margin-top:30px;margin-left:10px;color:#9aa0ac;font-size:12px;text-decoration:none}.vz-sidebar .new-btn{justify-content:right;margin-right:10px;cursor:pointer}.vz-sidebar .utils{display:flex}.vz-sidebar .utilities{margin-right:3px}.vz-sidebar .file-or-directory:hover,.vz-sidebar .settings:hover{background-color:#4a5469}.vz-sidebar .name{display:flex;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.vz-sidebar .indentation{margin-left:20px}.vz-sidebar .arrow-wrapper{margin-right:4px}.vz-sidebar .settings{height:30px;color:#dbdde1;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:14px 0;margin:10px;border-radius:5px}:root{--vzcode-font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:rgb(41,44,52)}::-webkit-scrollbar-thumb{background:rgb(37,37,38)}::-webkit-scrollbar-thumb:hover{background:#555}html,body,#root,.app{height:100%}.app{background-color:#292c34;font-family:Poppins,sans-serif;display:flex}.app .left{display:flex}.app .right{display:flex;flex:1;flex-direction:column;min-width:0}.app .right .tab-list{display:flex;background-color:#252526}.app .right .tab-list .tab{background:rgb(45,45,45);color:#fff;font-family:var(--vzcode-font-family);align-items:center;display:flex;padding-top:2px;padding-left:10px;padding-right:10px;border-top-left-radius:10px;border-top-right-radius:10px;margin-top:5px;margin-left:2px;margin-right:2px;cursor:pointer}.app .right .tab-list .tab .tab-close{margin-left:5px}.app .right .tab-list .tab .tab-close:hover{border-radius:30%;background-color:#252526}.app .right .tab-list .tab.active{background-color:#292c34}.app .right .editor{flex:1;display:flex;font-family:var(--vzcode-font-family);overflow-x:auto}.themes{color:#252526}
package/dist/index.html CHANGED
@@ -4,8 +4,22 @@
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-48dbd5c3.js"></script>
8
- <link rel="stylesheet" href="/assets/index-8d2d4b3b.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-41607816.js"></script>
22
+ <link rel="stylesheet" href="/assets/index-8bac68c9.css">
9
23
  </head>
10
24
  <body>
11
25
  <div id="root"></div>
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
- "bin": "cli.js",
7
+ "types": "src/types.ts",
8
+ "bin": "src/server/index.js",
8
9
  "files": [
9
10
  "dist/**",
10
11
  "src/**"
11
12
  ],
12
13
  "scripts": {
13
- "test": "npm run build; cd test; node ../cli.js",
14
+ "test": "vitest",
15
+ "test-interactive": "npm run build; cd test/sampleDirectories/kitchenSink; node ../../../src/server/index.js",
14
16
  "prettier": "prettier {*.*,**/*.*} --write",
17
+ "tsc": "tsc --noEmit",
15
18
  "dev": "vite",
16
19
  "build": "vite build",
17
20
  "build-release": "vite build --mode release",
@@ -43,32 +46,39 @@
43
46
  },
44
47
  "homepage": "https://github.com/vizhub-core/vzcode#readme",
45
48
  "dependencies": {
46
- "@codemirror/lang-css": "^6.1.1",
47
- "@codemirror/lang-html": "^6.4.2",
48
- "@codemirror/lang-javascript": "^6.1.4",
49
- "@codemirror/lang-markdown": "^6.1.0",
50
- "@codemirror/state": "^6.2.0",
51
- "@codemirror/theme-one-dark": "^6.1.1",
49
+ "@codemirror/lang-css": "^6.2.0",
50
+ "@codemirror/lang-html": "^6.4.4",
51
+ "@codemirror/lang-javascript": "^6.1.9",
52
+ "@codemirror/lang-markdown": "^6.1.1",
53
+ "@codemirror/state": "^6.2.1",
54
+ "@codemirror/theme-one-dark": "^6.1.2",
52
55
  "@teamwork/websocket-json-stream": "^2.0.0",
56
+ "@uiw/codemirror-themes-all": "^4.20.2",
53
57
  "codemirror": "^6.0.1",
54
58
  "codemirror-ot": "^4.0.0",
59
+ "d3-array": "^3.2.4",
55
60
  "diff-match-patch": "^1.0.5",
61
+ "dotenv": "^16.3.0",
56
62
  "express": "^4.18.2",
57
63
  "json0-ot-diff": "^1.1.2",
58
- "open": "^9.0.0",
64
+ "ngrok": "^4.3.3",
65
+ "open": "^9.1.0",
59
66
  "react": "^18.2.0",
60
- "react-bootstrap": "^2.7.2",
67
+ "react-bootstrap": "^2.7.4",
61
68
  "react-dom": "^18.2.0",
62
- "react-select": "^5.7.1",
63
- "sharedb": "^3.3.0",
64
- "sharedb-client-browser": "^4.2.0",
69
+ "react-select": "^5.7.3",
70
+ "sharedb": "^4.0.0",
71
+ "sharedb-client-browser": "^4.2.1",
65
72
  "ws": "^8.13.0"
66
73
  },
67
74
  "devDependencies": {
68
- "@types/react": "^18.0.28",
69
- "@types/react-dom": "^18.0.11",
70
- "@vitejs/plugin-react": "^3.1.0",
71
- "prettier": "^2.8.6",
72
- "vite": "^4.2.1"
75
+ "@types/react": "^18.2.12",
76
+ "@types/react-dom": "^18.2.5",
77
+ "@vitejs/plugin-react": "^4.0.0",
78
+ "prettier": "^2.8.8",
79
+ "sass": "^1.64.0",
80
+ "vite": "^4.3.9",
81
+ "vitest": "^0.32.2",
82
+ "typescript": "^5.1.6"
73
83
  }
74
84
  }
@@ -1,11 +1,14 @@
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
- import { json1Presence } from './ot';
3
+ import { json1Presence } from '../ot';
4
+ import { randomId } from '../randomId';
4
5
  import { CodeEditor } from './CodeEditor';
5
6
  import { diff } from './diff';
6
- import { randomId } from './randomId';
7
7
  import { Settings } from './settings';
8
- import './style.css';
8
+ import { Sidebar } from './Sidebar';
9
+ import './style.scss';
10
+ import { oneDark } from '@codemirror/theme-one-dark';
11
+ import { FileId } from '../types';
9
12
 
10
13
  // Register our custom JSON1 OT type that supports presence.
11
14
  // See https://github.com/vizhub-core/json1-presence
@@ -18,10 +21,6 @@ const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
18
21
  const socket = new WebSocket(wsProtocol + window.location.host + '/ws');
19
22
  const connection = new Connection(socket);
20
23
 
21
- // A feature flag to disable settings for release, until it's completed.
22
- // See https://vitejs.dev/guide/env-and-mode.html
23
- const disableSettings = import.meta.env.VITE_DISABLE_SETTINGS === 'true';
24
-
25
24
  function App() {
26
25
  // The ShareDB document.
27
26
  const [shareDBDoc, setShareDBDoc] = useState(null);
@@ -39,9 +38,6 @@ function App() {
39
38
  // updated on each change to decouple rendering from ShareDB.
40
39
  const [data, setData] = useState(null);
41
40
 
42
- // True if the file menu is open.
43
- const [isFileMenuOpen, setIsFileMenuOpen] = useState(false);
44
-
45
41
  // The id of the currently open file tab.
46
42
  const [activeFileId, setActiveFileId] = useState(null);
47
43
 
@@ -49,14 +45,10 @@ function App() {
49
45
  const [tabList, setTabList] = useState([]);
50
46
 
51
47
  // The current theme.
52
- const [theme, setTheme] = useState('OneDark');
53
-
54
- // The currently hovered over file, for showing
55
- // "utils", meaning buttons for rename and delete.
56
- const [utils, setUtils] = useState(false);
48
+ const [theme, setTheme] = useState(oneDark);
57
49
 
58
50
  // True to show the settings modal.
59
- const [settings, setSettings] = useState(false);
51
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false);
60
52
 
61
53
  // Set up the connection to ShareDB.
62
54
  useEffect(() => {
@@ -144,15 +136,15 @@ function App() {
144
136
  );
145
137
 
146
138
  // Called when a file in the sidebar is double-clicked.
147
- const renameFile = useCallback(
148
- (key) => {
139
+ const handleRenameFileClick = useCallback(
140
+ (fileId: FileId) => {
149
141
  const newName = prompt('Enter new name');
150
142
  if (newName) {
151
143
  const currentDocument = shareDBDoc.data;
152
144
  const nextDocument = {
153
145
  ...currentDocument,
154
- [key]: {
155
- ...currentDocument[key],
146
+ [fileId]: {
147
+ ...currentDocument[fileId],
156
148
  name: newName,
157
149
  },
158
150
  };
@@ -163,22 +155,32 @@ function App() {
163
155
  );
164
156
 
165
157
  const deleteFile = useCallback(
166
- (key) => {
158
+ (fileId: FileId) => {
167
159
  // Close the tab in case it's open.
168
160
  // TODO consistently use either "key" or "fileId" naming
169
- closeTab(key);
161
+ closeTab(fileId);
170
162
 
171
163
  const currentDocument = shareDBDoc.data;
172
164
  const nextDocument = { ...currentDocument };
173
- delete nextDocument[key];
165
+ delete nextDocument[fileId];
174
166
  shareDBDoc.submitOp(diff(currentDocument, nextDocument));
175
167
  },
176
168
  [shareDBDoc, closeTab]
177
169
  );
178
170
 
171
+ const handleFileClick = useCallback(
172
+ (fileId: FileId) => {
173
+ setActiveFileId(fileId);
174
+ if (!tabList.includes(fileId)) {
175
+ setTabList([...tabList, fileId]);
176
+ }
177
+ },
178
+ [tabList]
179
+ );
180
+
179
181
  // TODO prompt the user "Are you sure?"
180
182
  const handleDeleteFileClick = useCallback(
181
- (fileId) => (event) => {
183
+ (fileId: FileId, event: React.MouseEvent) => {
182
184
  // Stop propagation so that the outer listener doesn't fire,
183
185
  // which would try to open this file in a tab.
184
186
  event.stopPropagation();
@@ -188,6 +190,7 @@ function App() {
188
190
  );
189
191
 
190
192
  const createFile = useCallback(() => {
193
+ // TODO better UI, maybe Bootstrap modal? Maybe edit inline?
191
194
  const name = prompt('Enter new file name');
192
195
  if (!name) return;
193
196
  const currentDocument = shareDBDoc.data;
@@ -202,114 +205,68 @@ function App() {
202
205
  const tabValid = data && activeFileId;
203
206
 
204
207
  const handleSettingsClose = useCallback(() => {
205
- setSettings(false);
208
+ setIsSettingsOpen(false);
206
209
  }, []);
207
210
 
211
+ const fileNameSplit = (fileName) => {
212
+ const split = fileName.split('/');
213
+ //adding the folder that the is in to the tab name
214
+ if (split.length === 1) return split[split.length - 1];
215
+ return split[split.length - 2] + '/' + split[split.length - 1];
216
+ };
217
+
208
218
  return (
209
- <>
210
- <Settings
211
- show={settings}
212
- onClose={handleSettingsClose}
213
- setTheme={setTheme}
214
- />
215
- <div className="tab-list">
216
- {tabList.map((fileId) => (
217
- <div
218
- key={fileId}
219
- className={
220
- tabValid ? `tab${fileId === activeFileId ? ' active' : ''}` : null
221
- }
222
- onClick={() => {
223
- setActiveFileId(fileId);
224
- }}
225
- >
226
- {tabValid ? data[fileId].name : ''}
227
- <div
228
- className={activeFileId ? 'bx bx-x tab-close' : ''}
229
- onClick={handleCloseTabClick(fileId)}
230
- ></div>
231
- </div>
232
- ))}
219
+ <div className="app">
220
+ <div className="left">
221
+ <Sidebar
222
+ createFile={createFile}
223
+ files={data}
224
+ handleRenameFileClick={handleRenameFileClick}
225
+ handleDeleteFileClick={handleDeleteFileClick}
226
+ handleFileClick={handleFileClick}
227
+ setIsSettingsOpen={setIsSettingsOpen}
228
+ isSettingsOpen={isSettingsOpen}
229
+ />
230
+ <Settings
231
+ show={isSettingsOpen}
232
+ onClose={handleSettingsClose}
233
+ setTheme={setTheme}
234
+ />
233
235
  </div>
234
- <div className="sidebar show">
235
- <div className="nav-links">
236
- <div className="show-menu">
237
- <div className="sub-menu">
238
- <div className="files">
239
- <div className="full-Box">
240
- <div>
241
- <a className="link-name" href="#">
242
- Files
243
- </a>
244
- </div>
245
- <div>
246
- <i
247
- className="bx bxs-file-plus newBTN"
248
- style={{ color: '#dbdde1' }}
249
- onClick={createFile}
250
- ></i>
251
- </div>
252
- </div>
253
- </div>
254
- {data
255
- ? Object.keys(data).map((key) => (
256
- <div className="file" key={key}>
257
- <div
258
- className="full-Box"
259
- onClick={() => {
260
- setActiveFileId(key);
261
- if (!tabList.includes(key)) {
262
- setTabList([...tabList, key]);
263
- }
264
- }}
265
- onMouseEnter={() => {
266
- setUtils(key);
267
- }}
268
- onMouseLeave={() => {
269
- setUtils(null);
270
- }}
271
- >
272
- <div className={utils === key ? 'hover-name' : 'name'}>
273
- {data[key].name}
274
- </div>
275
- <div className={utils === key ? 'utils' : 'noUtils'}>
276
- <i
277
- className="bx bxs-edit utilities"
278
- style={{ color: '#abdafb' }}
279
- onClick={() => {
280
- renameFile(key);
281
- }}
282
- ></i>
283
- <i
284
- className="bx bx-trash"
285
- style={{ color: '#eb336c' }}
286
- onClick={handleDeleteFileClick(key)}
287
- ></i>
288
- </div>
289
- </div>
290
- </div>
291
- ))
292
- : null}
293
- </div>
294
- </div>
295
- {disableSettings ? null : (
296
- <div className="settings" onClick={() => setSettings(!settings)}>
297
- Settings
236
+ <div className="right">
237
+ <div className="tab-list">
238
+ {tabList.map((fileId) => (
239
+ <div
240
+ key={fileId}
241
+ className={
242
+ tabValid
243
+ ? `tab${fileId === activeFileId ? ' active' : ''}`
244
+ : null
245
+ }
246
+ onClick={() => {
247
+ setActiveFileId(fileId);
248
+ }}
249
+ >
250
+ {tabValid ? fileNameSplit(data[fileId].name) : ''}
251
+ <div
252
+ className={activeFileId ? 'bx bx-x tab-close' : ''}
253
+ onClick={handleCloseTabClick(fileId)}
254
+ ></div>
298
255
  </div>
299
- )}
256
+ ))}
300
257
  </div>
258
+ {data && activeFileId ? (
259
+ <CodeEditor
260
+ className="editor"
261
+ shareDBDoc={shareDBDoc}
262
+ localPresence={localPresence}
263
+ docPresence={docPresence}
264
+ activeFileId={activeFileId}
265
+ theme={theme}
266
+ />
267
+ ) : null}
301
268
  </div>
302
- {data && activeFileId ? (
303
- <CodeEditor
304
- className="editor"
305
- shareDBDoc={shareDBDoc}
306
- localPresence={localPresence}
307
- docPresence={docPresence}
308
- activeFileId={activeFileId}
309
- theme={theme}
310
- />
311
- ) : null}
312
- </>
269
+ </div>
313
270
  );
314
271
  }
315
272
 
@@ -0,0 +1,5 @@
1
+ export const DirectoryArrowSVG = ({ height = 10, fill = 'currentcolor' }) => (
2
+ <svg height={height} viewBox={'4 4 16 16'}>
3
+ <path fill={fill} d="M8 17l9-5-9-5v10z" />
4
+ </svg>
5
+ );
@@ -0,0 +1,76 @@
1
+ import { useCallback } from 'react';
2
+ import { Item } from './Item';
3
+ import { Listing } from './Listing';
4
+ import { DirectoryArrowSVG } from './DirectoryArrowSVG';
5
+ import { FileTree, FileTreeFile } from '../../types';
6
+
7
+ export const DirectoryListing = ({
8
+ name,
9
+ path,
10
+ children,
11
+ handleRenameFileClick,
12
+ handleDeleteFileClick,
13
+ handleFileClick,
14
+ openDirectories,
15
+ toggleDirectory,
16
+ }: {
17
+ name: string;
18
+ path: string;
19
+ children: Array<FileTree | FileTreeFile>;
20
+ handleRenameFileClick: (fileId: string) => void;
21
+ handleDeleteFileClick: (fileId: string, event: React.MouseEvent) => void;
22
+ handleFileClick: (fileId: string) => void;
23
+ openDirectories: { [path: string]: boolean };
24
+ toggleDirectory: (path: string) => void;
25
+ }) => {
26
+ const handleClick = useCallback(() => {
27
+ toggleDirectory(path);
28
+ }, [toggleDirectory]);
29
+
30
+ const handleDeleteClick = useCallback(() => {
31
+ console.log('TODO handleDeleteDirectoryClick');
32
+ }, []);
33
+
34
+ const handleRenameClick = useCallback(() => {
35
+ console.log('TODO handleRenameDirectoryClick');
36
+ }, []);
37
+
38
+ const isOpen = openDirectories[path];
39
+ return (
40
+ <>
41
+ <Item
42
+ name={name}
43
+ handleClick={handleClick}
44
+ handleDeleteClick={handleDeleteClick}
45
+ handleRenameClick={handleRenameClick}
46
+ >
47
+ <div
48
+ className="arrow-wrapper"
49
+ style={{ transform: `rotate(${isOpen ? 90 : 0}deg)` }}
50
+ >
51
+ <DirectoryArrowSVG />
52
+ </div>
53
+ {name}
54
+ </Item>
55
+ {children && isOpen ? (
56
+ <div className="indentation">
57
+ {children.map((entity) => {
58
+ const { fileId } = entity as FileTreeFile;
59
+ const { path } = entity as FileTree;
60
+ return (
61
+ <Listing
62
+ entity={entity}
63
+ key={fileId || path}
64
+ handleRenameFileClick={handleRenameFileClick}
65
+ handleDeleteFileClick={handleDeleteFileClick}
66
+ handleFileClick={handleFileClick}
67
+ openDirectories={openDirectories}
68
+ toggleDirectory={toggleDirectory}
69
+ />
70
+ );
71
+ })}
72
+ </div>
73
+ ) : null}
74
+ </>
75
+ );
76
+ };
@@ -0,0 +1,42 @@
1
+ import { useCallback } from 'react';
2
+ import { Item } from './Item';
3
+ import { FileId } from '../../types';
4
+
5
+ export const FileListing = ({
6
+ name,
7
+ fileId,
8
+ handleRenameFileClick,
9
+ handleDeleteFileClick,
10
+ handleFileClick,
11
+ }: {
12
+ name: string;
13
+ fileId: FileId;
14
+ handleRenameFileClick: (fileId: FileId) => void;
15
+ handleDeleteFileClick: (fileId: FileId, event: React.MouseEvent) => void;
16
+ handleFileClick: (fileId: FileId) => void;
17
+ }) => {
18
+ const handleClick = useCallback(() => {
19
+ handleFileClick(fileId);
20
+ }, [fileId, handleFileClick]);
21
+
22
+ const handleDeleteClick = useCallback(
23
+ (event) => {
24
+ handleDeleteFileClick(fileId, event);
25
+ },
26
+ [fileId]
27
+ );
28
+
29
+ const handleRenameClick = useCallback(() => {
30
+ handleRenameFileClick(fileId);
31
+ }, [fileId]);
32
+
33
+ return (
34
+ <Item
35
+ handleClick={handleClick}
36
+ handleDeleteClick={handleDeleteClick}
37
+ handleRenameClick={handleRenameClick}
38
+ >
39
+ {name}
40
+ </Item>
41
+ );
42
+ };
@@ -0,0 +1,43 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ export const Item = ({
4
+ children,
5
+ handleClick,
6
+ handleRenameClick,
7
+ handleDeleteClick,
8
+ }) => {
9
+ const [isHovered, setIsHovered] = useState(false);
10
+
11
+ const handleMouseEnter = useCallback(() => {
12
+ setIsHovered(true);
13
+ }, []);
14
+
15
+ const handleMouseLeave = useCallback(() => {
16
+ setIsHovered(false);
17
+ }, []);
18
+
19
+ return (
20
+ <div
21
+ className="full-box file-or-directory"
22
+ onClick={handleClick}
23
+ onMouseEnter={handleMouseEnter}
24
+ onMouseLeave={handleMouseLeave}
25
+ >
26
+ <div className="name">{children}</div>
27
+ {isHovered ? (
28
+ <div className="utils">
29
+ <i
30
+ className="bx bxs-edit utilities"
31
+ style={{ color: '#abdafb' }}
32
+ onClick={handleRenameClick}
33
+ ></i>
34
+ <i
35
+ className="bx bx-trash"
36
+ style={{ color: '#eb336c' }}
37
+ onClick={handleDeleteClick}
38
+ ></i>
39
+ </div>
40
+ ) : null}
41
+ </div>
42
+ );
43
+ };
@@ -0,0 +1,44 @@
1
+ import { DirectoryListing } from './DirectoryListing';
2
+ import { FileId, FileTree, FileTreeFile } from '../../types';
3
+ import { FileListing } from './FileListing';
4
+
5
+ // A "Listing" is a "FileListing" or a "DirectoryListing"
6
+ // that appears in the Sidebar.
7
+ export const Listing = ({
8
+ entity,
9
+ handleRenameFileClick,
10
+ handleDeleteFileClick,
11
+ handleFileClick,
12
+ openDirectories,
13
+ toggleDirectory,
14
+ }: {
15
+ entity: FileTree | FileTreeFile;
16
+ handleRenameFileClick: (fileId: FileId) => void;
17
+ handleDeleteFileClick: (fileId: FileId, event: React.MouseEvent) => void;
18
+ handleFileClick: (fileId: FileId) => void;
19
+ openDirectories: { [path: string]: boolean };
20
+ toggleDirectory: (path: string) => void;
21
+ }) => {
22
+ const { name, file, fileId } = entity as FileTreeFile;
23
+ const { path, children } = entity as FileTree;
24
+ return file ? (
25
+ <FileListing
26
+ fileId={fileId}
27
+ name={name}
28
+ handleRenameFileClick={handleRenameFileClick}
29
+ handleDeleteFileClick={handleDeleteFileClick}
30
+ handleFileClick={handleFileClick}
31
+ />
32
+ ) : (
33
+ <DirectoryListing
34
+ name={name}
35
+ path={path}
36
+ children={children}
37
+ handleRenameFileClick={handleRenameFileClick}
38
+ handleDeleteFileClick={handleDeleteFileClick}
39
+ handleFileClick={handleFileClick}
40
+ openDirectories={openDirectories}
41
+ toggleDirectory={toggleDirectory}
42
+ />
43
+ );
44
+ };