online-compiler-widget 0.0.1

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 (67) hide show
  1. package/FileStorage/obj/FileStorage.csproj.EntityFrameworkCore.targets +28 -0
  2. package/README.md +1 -0
  3. package/eslint.config.js +26 -0
  4. package/index.html +13 -0
  5. package/openapitools.json +7 -0
  6. package/package.json +36 -0
  7. package/pnpm-workspace.yaml +2 -0
  8. package/public/vite.svg +1 -0
  9. package/src/App.css +49 -0
  10. package/src/App.tsx +84 -0
  11. package/src/api/.openapi-generator/FILES +25 -0
  12. package/src/api/.openapi-generator/VERSION +1 -0
  13. package/src/api/.openapi-generator-ignore +23 -0
  14. package/src/api/api.ts +1312 -0
  15. package/src/api/base.ts +62 -0
  16. package/src/api/common.ts +113 -0
  17. package/src/api/configuration.ts +121 -0
  18. package/src/api/docs/CompilationError.md +26 -0
  19. package/src/api/docs/CompileRequest.md +22 -0
  20. package/src/api/docs/CompileResult.md +28 -0
  21. package/src/api/docs/CompilerApi.md +263 -0
  22. package/src/api/docs/CreateFileDto.md +22 -0
  23. package/src/api/docs/CreateProjectRequest.md +20 -0
  24. package/src/api/docs/FileApi.md +274 -0
  25. package/src/api/docs/ProcessStatus.md +28 -0
  26. package/src/api/docs/ProjectApi.md +362 -0
  27. package/src/api/docs/ProjectInfo.md +24 -0
  28. package/src/api/docs/ProjectStats.md +28 -0
  29. package/src/api/docs/RenameFileDto.md +20 -0
  30. package/src/api/docs/RenameProjectRequest.md +20 -0
  31. package/src/api/docs/RunRequest.md +24 -0
  32. package/src/api/docs/RunResult.md +30 -0
  33. package/src/api/docs/RunningProjectInfo.md +26 -0
  34. package/src/api/docs/UpdateFileDto.md +20 -0
  35. package/src/api/git_push.sh +57 -0
  36. package/src/api/index.ts +18 -0
  37. package/src/assets/Badge.svg +17 -0
  38. package/src/assets/closeIcon.svg +20 -0
  39. package/src/assets/documentIcon.svg +11 -0
  40. package/src/assets/history.svg +11 -0
  41. package/src/assets/output.svg +12 -0
  42. package/src/assets/plus.svg +20 -0
  43. package/src/assets/react.svg +1 -0
  44. package/src/assets/save-icon.svg +11 -0
  45. package/src/assets/shield.svg +10 -0
  46. package/src/assets/start.svg +11 -0
  47. package/src/assets/stop.svg +11 -0
  48. package/src/components/CompilerWidget.module.scss +169 -0
  49. package/src/components/CompilerWidget.tsx +279 -0
  50. package/src/components/FileExplorer.module.scss +372 -0
  51. package/src/components/FileExplorer.tsx +285 -0
  52. package/src/components/MonacoEditorWrapper.module.scss +29 -0
  53. package/src/components/MonacoEditorWrapper.tsx +74 -0
  54. package/src/components/OutputPanel.module.scss +123 -0
  55. package/src/components/OutputPanel.tsx +53 -0
  56. package/src/components/RunContainer.module.scss +150 -0
  57. package/src/components/RunContainer.tsx +34 -0
  58. package/src/hooks/useCompiler.ts +228 -0
  59. package/src/hooks/useInitialNodes.ts +0 -0
  60. package/src/index.css +78 -0
  61. package/src/main.tsx +7 -0
  62. package/src/types/EditorDocument.ts +8 -0
  63. package/swagger.json +1020 -0
  64. package/tsconfig.app.json +29 -0
  65. package/tsconfig.json +7 -0
  66. package/tsconfig.node.json +26 -0
  67. package/vite.config.ts +8 -0
@@ -0,0 +1,285 @@
1
+ import React, { useState, useRef, useEffect, useCallback } from "react";
2
+ import cls from "./FileExplorer.module.scss";
3
+ import DocumentIcon from "../assets/documentIcon.svg?react";
4
+ import PlusIcon from "../assets/plus.svg?react";
5
+ import type { EditorDocument } from "../types/EditorDocument";
6
+
7
+ interface FileExplorerProps {
8
+ documents: EditorDocument[];
9
+ selectedId: string | null;
10
+ onSelect: (id: string) => void;
11
+ onAdd: (fileName?: string) => void;
12
+ onRename: (id: string, newName: string) => void;
13
+ onDelete: (id: string) => void;
14
+ }
15
+
16
+ // Компонент модального окна для создания файла
17
+ interface AddFileModalProps {
18
+ isOpen: boolean;
19
+ onClose: () => void;
20
+ onConfirm: (fileName: string) => void;
21
+ }
22
+
23
+ const AddFileModal: React.FC<AddFileModalProps> = ({ isOpen, onClose, onConfirm }) => {
24
+ const [fileName, setFileName] = useState("");
25
+ const [error, setError] = useState("");
26
+
27
+ if (!isOpen) return null;
28
+
29
+ const handleSubmit = (e: React.FormEvent) => {
30
+ e.preventDefault();
31
+
32
+ const trimmedName = fileName.trim();
33
+
34
+ if (!trimmedName) {
35
+ setError("Введите имя файла");
36
+ return;
37
+ }
38
+
39
+ // Проверяем расширение
40
+ if (!trimmedName.includes('.')) {
41
+ setError("Добавьте расширение файла (например, .cs, .js, .txt)");
42
+ return;
43
+ }
44
+
45
+ // Проверяем допустимые расширения
46
+ const validExtensions = ['.cs', '.js', '.txt'];
47
+ const hasValidExtension = validExtensions.some(ext => trimmedName.endsWith(ext));
48
+
49
+ if (!hasValidExtension) {
50
+ setError(`Используйте одно из расширений: ${validExtensions.join(', ')}`);
51
+ return;
52
+ }
53
+
54
+ onConfirm(trimmedName);
55
+ setFileName("");
56
+ setError("");
57
+ };
58
+
59
+ const handleClose = () => {
60
+ setFileName("");
61
+ setError("");
62
+ onClose();
63
+ };
64
+
65
+ return (
66
+ <div className={cls.modalOverlay} onClick={handleClose}>
67
+ <div className={cls.modalContent} onClick={(e) => e.stopPropagation()}>
68
+ <h3>Создать новый файл</h3>
69
+ <form onSubmit={handleSubmit}>
70
+ <div className={cls.formGroup}>
71
+ <label htmlFor="fileName">Имя файла с расширением:</label>
72
+ <input
73
+ id="fileName"
74
+ type="text"
75
+ value={fileName}
76
+ onChange={(e) => {
77
+ setFileName(e.target.value);
78
+ setError("");
79
+ }}
80
+ placeholder="Например: program.cs или script.js"
81
+ autoFocus
82
+ className={error ? cls.error : ""}
83
+ />
84
+ <div className={cls.helpText}>
85
+ Введите имя файла с расширением (.cs, .js, .txt)
86
+ </div>
87
+ {error && <div className={cls.errorMessage}>{error}</div>}
88
+ </div>
89
+ <div className={cls.modalActions}>
90
+ <button
91
+ type="button"
92
+ onClick={handleClose}
93
+ className={cls.cancelBtn}
94
+ >
95
+ Отмена
96
+ </button>
97
+ <button
98
+ type="submit"
99
+ className={cls.confirmBtn}
100
+ disabled={!fileName.trim()}
101
+ >
102
+ Создать
103
+ </button>
104
+ </div>
105
+ </form>
106
+ </div>
107
+ </div>
108
+ );
109
+ };
110
+
111
+ export const FileExplorer: React.FC<FileExplorerProps> = ({
112
+ documents,
113
+ selectedId,
114
+ onSelect,
115
+ onAdd,
116
+ onRename,
117
+ onDelete
118
+ }) => {
119
+ const [menuId, setMenuId] = useState<string | null>(null);
120
+ const [editingId, setEditingId] = useState<string | null>(null);
121
+ const [editName, setEditName] = useState("");
122
+ const [showAddModal, setShowAddModal] = useState(false);
123
+
124
+ const menuRef = useRef<HTMLDivElement | null>(null);
125
+ const inputRef = useRef<HTMLInputElement | null>(null);
126
+
127
+ // Закрытие меню при клике вне
128
+ useEffect(() => {
129
+ const handler = (e: MouseEvent) => {
130
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
131
+ setMenuId(null);
132
+ }
133
+ };
134
+
135
+ document.addEventListener("mousedown", handler);
136
+ return () => document.removeEventListener("mousedown", handler);
137
+ }, []);
138
+
139
+ // Фокус на input при начале редактирования
140
+ useEffect(() => {
141
+ if (editingId && inputRef.current) {
142
+ inputRef.current.focus();
143
+ inputRef.current.select();
144
+ }
145
+ }, [editingId]);
146
+
147
+ const handleStartRename = useCallback((id: string, currentName: string) => {
148
+ setMenuId(null);
149
+ setEditingId(id);
150
+ setEditName(currentName);
151
+ }, []);
152
+
153
+ const handleSaveRename = useCallback((id: string) => {
154
+ if (editName.trim() && editName.trim() !== documents.find(d => d.id === id)?.name) {
155
+ onRename(id, editName.trim());
156
+ }
157
+ setEditingId(null);
158
+ setEditName("");
159
+ }, [editName, onRename, documents]);
160
+
161
+ const handleKeyDown = useCallback((e: React.KeyboardEvent, id: string) => {
162
+ if (e.key === 'Enter') {
163
+ e.preventDefault();
164
+ handleSaveRename(id);
165
+ } else if (e.key === 'Escape') {
166
+ e.preventDefault();
167
+ setEditingId(null);
168
+ setEditName("");
169
+ }
170
+ }, [handleSaveRename]);
171
+
172
+ const handleBlur = useCallback((id: string) => {
173
+ setTimeout(() => {
174
+ handleSaveRename(id);
175
+ }, 100);
176
+ }, [handleSaveRename]);
177
+
178
+ const handleAddFile = (fileName: string) => {
179
+ onAdd(fileName);
180
+ setShowAddModal(false);
181
+ };
182
+
183
+ return (
184
+ <div className={cls.fileExplorer}>
185
+ <AddFileModal
186
+ isOpen={showAddModal}
187
+ onClose={() => setShowAddModal(false)}
188
+ onConfirm={handleAddFile}
189
+ />
190
+
191
+ <div className={cls.header}>
192
+ <DocumentIcon className={cls.documentIcon} />
193
+ <span>Файлы</span>
194
+ <button
195
+ onClick={() => setShowAddModal(true)}
196
+ className={cls.addButton}
197
+ title="Добавить файл"
198
+ >
199
+ <PlusIcon className={cls.plusIcon} />
200
+ </button>
201
+ </div>
202
+
203
+ <ul className={cls.fileList}>
204
+ {documents.map(doc => (
205
+ <li
206
+ key={doc.id}
207
+ className={`${cls.fileItem} ${
208
+ selectedId === doc.id ? cls.selected : ""
209
+ } ${doc.modified ? cls.modified : ""}`}
210
+ onClick={() => !editingId && onSelect(doc.id)}
211
+ >
212
+ <div className={cls.clickZone}>
213
+ <span className={cls.fileIcon}>📄</span>
214
+
215
+ {editingId === doc.id ? (
216
+ <div className={cls.editContainer}>
217
+ <input
218
+ ref={inputRef}
219
+ type="text"
220
+ className={cls.editInput}
221
+ value={editName}
222
+ onChange={(e) => setEditName(e.target.value)}
223
+ onKeyDown={(e) => handleKeyDown(e, doc.id)}
224
+ onBlur={() => handleBlur(doc.id)}
225
+ onClick={(e) => e.stopPropagation()}
226
+ onMouseDown={(e) => e.stopPropagation()}
227
+ />
228
+ </div>
229
+ ) : (
230
+ <div className={cls.nameContainer}>
231
+ <span className={cls.itemText}>{doc.name}</span>
232
+ {doc.modified && (
233
+ <span
234
+ className={cls.modifiedDot}
235
+ title="Файл изменен (не сохранен)"
236
+ >
237
+
238
+ </span>
239
+ )}
240
+ </div>
241
+ )}
242
+ </div>
243
+
244
+ {editingId !== doc.id && (
245
+ <button
246
+ className={cls.moreBtn}
247
+ onClick={(e) => {
248
+ e.stopPropagation();
249
+ setMenuId(menuId === doc.id ? null : doc.id);
250
+ }}
251
+ title="Действия с файлом"
252
+ >
253
+
254
+ </button>
255
+ )}
256
+
257
+ {menuId === doc.id && (
258
+ <div className={cls.contextMenu} ref={menuRef}>
259
+ <div
260
+ className={cls.menuItem}
261
+ onClick={(e) => {
262
+ e.stopPropagation();
263
+ handleStartRename(doc.id, doc.name);
264
+ }}
265
+ >
266
+ Переименовать
267
+ </div>
268
+ <div
269
+ className={cls.menuItem}
270
+ onClick={(e) => {
271
+ e.stopPropagation();
272
+ setMenuId(null);
273
+ onDelete(doc.id);
274
+ }}
275
+ >
276
+ Удалить
277
+ </div>
278
+ </div>
279
+ )}
280
+ </li>
281
+ ))}
282
+ </ul>
283
+ </div>
284
+ );
285
+ };
@@ -0,0 +1,29 @@
1
+ $editor-header-bg: #f9fafb;
2
+ $editor-border: #ddd;
3
+
4
+ .editorContainer {
5
+ flex: 1 1 auto;
6
+ display: flex;
7
+ flex-direction: column;
8
+ height: 100%;
9
+ overflow: hidden;
10
+
11
+ .editorHeader {
12
+ flex: 0 0 32px;
13
+ display: flex;
14
+ align-items: center;
15
+ padding: 0 12px;
16
+ background: $editor-header-bg;
17
+ border-bottom: 1px solid #eee;
18
+ font-weight: 500;
19
+ font-size: 13px;
20
+ color: #111;
21
+ }
22
+
23
+ .monaco-editor {
24
+ flex: 1 1 auto;
25
+ overflow: hidden !important;
26
+ }
27
+ }
28
+
29
+
@@ -0,0 +1,74 @@
1
+ // src/components/MonacoEditorWrapper.tsx
2
+ import React, { useRef, useEffect } from "react";
3
+ import Editor from "@monaco-editor/react";
4
+ import * as monaco from "monaco-editor";
5
+ import cls from "./MonacoEditorWrapper.module.scss";
6
+ import StartIcon from "../assets/start.svg?react";
7
+ import StopIcon from "../assets/stop.svg?react";
8
+
9
+ interface MonacoEditorWrapperProps {
10
+ code: string;
11
+ language: string; // "javascript" | "csharp"
12
+ onChange: (value: string) => void;
13
+ theme?: string;
14
+ }
15
+
16
+ export const MonacoEditorWrapper: React.FC<MonacoEditorWrapperProps> = ({
17
+ code,
18
+ language,
19
+ onChange,
20
+ theme = "vs-light",
21
+ }) => {
22
+ const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
23
+ const modelRef = useRef<monaco.editor.ITextModel | null>(null);
24
+
25
+ // Создаём модель при загрузке редактора
26
+ const handleEditorDidMount = (
27
+ editor: monaco.editor.IStandaloneCodeEditor
28
+ ) => {
29
+ editorRef.current = editor;
30
+ };
31
+
32
+ // 🔥 Создание / обновление модели при переключении документа
33
+ useEffect(() => {
34
+ if (!editorRef.current) return;
35
+
36
+ // удаляем старую модель
37
+ if (modelRef.current) {
38
+ modelRef.current?.dispose();
39
+ }
40
+
41
+ const newModel = monaco.editor.createModel(code, language);
42
+ modelRef.current = newModel;
43
+
44
+
45
+ editorRef.current?.focus();
46
+ }, [code, language]);
47
+
48
+ return (
49
+ <div className={cls.editorContainer}>
50
+ <div className={cls.editorHeader}>
51
+ <span>{language === "csharp" ? "C#" : "JS"}</span>
52
+ </div>
53
+
54
+ <Editor
55
+ height="100%"
56
+ language={language}
57
+ theme={theme}
58
+ onMount={handleEditorDidMount}
59
+ value={code} // контролируемое значение
60
+ onChange={(v) => onChange(v || "")}
61
+ options={{
62
+ minimap: { enabled: false },
63
+ fontSize: 10,
64
+ scrollBeyondLastLine: false,
65
+ wordWrap: "off",
66
+ lineNumbers: "on",
67
+ folding: true,
68
+ renderLineHighlight: "all",
69
+ tabSize: 4,
70
+ }}
71
+ />
72
+ </div>
73
+ );
74
+ };
@@ -0,0 +1,123 @@
1
+ $output-bg: #fafafa;
2
+ $output-border: #ddd;
3
+ $tab-active-bg: #e0f0ff;
4
+ $tab-active-border: #007bff;
5
+
6
+
7
+
8
+ .outputPanel {
9
+ flex: 0 0 300px; // ширина правой панели (можно менять через resize)
10
+ display: flex;
11
+ flex-direction: column;
12
+ height: 100%;
13
+ background: $output-bg;
14
+ border-left: 1px solid $output-border;
15
+ box-sizing: border-box;
16
+
17
+ .tabs {
18
+ display: flex;
19
+ background: #F8FAFC;
20
+ border-bottom: 1px solid #E2E8F0;
21
+
22
+ .tab {
23
+ width: 100%;
24
+ padding: 7px 12px;
25
+ display: flex;
26
+ align-items: center;
27
+ font-size: 8px;
28
+ cursor: pointer;
29
+ border: none;
30
+ transition: background 0.2s;
31
+ border-radius: 0px;
32
+ outline: none;
33
+ justify-content: center;
34
+ align-items: center;
35
+
36
+ &.active {
37
+ border: 0px;
38
+ background: #FFFFFF;
39
+ border-bottom: 1px solid $tab-active-border;
40
+ border-radius: 0px;
41
+ }
42
+
43
+ .icon {
44
+ margin-right: 2px;
45
+ height: 10px;
46
+ padding-top: 2px;
47
+ }
48
+ }
49
+ }
50
+
51
+ .content {
52
+ flex: 1 1 auto;
53
+ overflow-y: auto;
54
+ padding: 8px 12px;
55
+
56
+ background: #fff;
57
+ box-sizing: border-box;
58
+
59
+ pre {
60
+ background: #f8f8f8;
61
+ padding: 8px;
62
+
63
+ font-family: 'Courier New', Courier, monospace;
64
+ font-size: 13px;
65
+ line-height: 1.4;
66
+ white-space: pre-wrap;
67
+ word-wrap: break-word;
68
+ }
69
+
70
+ .output {
71
+ margin: 0;
72
+ padding: 0;
73
+ white-space: pre-wrap;
74
+ font-size: 12px;
75
+ line-height: 20px;
76
+ }
77
+
78
+ .placeholder {
79
+ display: flex;
80
+ flex-direction: column;
81
+ align-items: center;
82
+ justify-content: center;
83
+ height: 100%;
84
+ color: #999;
85
+ text-align: center;
86
+ gap: 6px;
87
+
88
+ .bigIcon{
89
+ height: 28px;
90
+ color: #CAD5E2;
91
+ }
92
+
93
+ span {
94
+ font-size: 28px;
95
+ opacity: 0.4;
96
+ }
97
+
98
+ p {
99
+ margin: 0;
100
+ font-family: Arial;
101
+ font-weight: 400;
102
+ font-size: 11px;
103
+ color: #62748E;
104
+ }
105
+
106
+ small {
107
+ font-family: Arial;
108
+ font-weight: 400;
109
+ font-size: 9px;
110
+ opacity: 0.7;
111
+ color: #90A1B9;
112
+ }
113
+ }
114
+
115
+ .historyItem {
116
+ margin-bottom: 8px;
117
+ padding: 6px;
118
+ background: #f0f0f0;
119
+
120
+ border-left: 3px solid $tab-active-border;
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,53 @@
1
+ // src/components/OutputPanel.tsx
2
+ import React, { useState } from 'react';
3
+ import cls from './OutputPanel.module.scss';
4
+ import HistoryIcon from "../assets/history.svg?react";
5
+ import OutputIcon from "../assets/output.svg?react";
6
+
7
+ interface OutputPanelProps {
8
+ output: string;
9
+ history: string[];
10
+ }
11
+
12
+ export const OutputPanel: React.FC<OutputPanelProps> = ({ output, history }) => {
13
+ const [activeTab, setActiveTab] = useState<'output' | 'history'>('output');
14
+
15
+ return (
16
+ <div className={cls.outputPanel}>
17
+ <div className={cls.tabs}>
18
+ <button
19
+ className={`${cls.tab} ${activeTab === 'output' ? cls.active : ''}`}
20
+ onClick={() => setActiveTab('output')}
21
+ >
22
+ <OutputIcon className={cls.icon}/>
23
+ <span>Вывод</span>
24
+ </button>
25
+ <button
26
+ className={`${cls.tab} ${activeTab === 'history' ? cls.active : ''}`}
27
+ onClick={() => setActiveTab('history')}
28
+ >
29
+ <HistoryIcon className={cls.icon}/>
30
+ <span>История</span>
31
+ </button>
32
+ </div>
33
+
34
+ <div className={cls.content}>
35
+ {!output && activeTab === 'output' && (
36
+ <div className={cls.placeholder}>
37
+ <OutputIcon className={cls.bigIcon}/>
38
+ <p>Нет результата кода</p>
39
+ <small>Запустите свой код, чтобы увидеть результат</small>
40
+ </div>
41
+ )}
42
+
43
+ <p className={cls.output}>{output}</p>
44
+
45
+ {activeTab === 'history' && (
46
+ <div className={cls.placeholder}>
47
+ <HistoryIcon className={cls.bigIcon}/>
48
+ <p>История пуста</p>
49
+ </div>)}
50
+ </div>
51
+ </div>
52
+ );
53
+ };