noho-platform 1.0.2 → 1.0.4
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.
- package/gitignore +4 -0
- package/noho-dashboard.html +670 -314
- package/package.json +7 -6
- package/test-lib.js +0 -56
package/noho-dashboard.html
CHANGED
|
@@ -3,41 +3,91 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>NOHO -
|
|
6
|
+
<title>NOHO Platform - لوحة التحكم</title>
|
|
7
7
|
<style>
|
|
8
8
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
9
|
|
|
10
10
|
:root {
|
|
11
11
|
--primary: #6366f1;
|
|
12
12
|
--primary-dark: #4f46e5;
|
|
13
|
+
--primary-light: #818cf8;
|
|
13
14
|
--bg: #0f172a;
|
|
14
15
|
--surface: #1e293b;
|
|
16
|
+
--surface-hover: #334155;
|
|
15
17
|
--text: #f8fafc;
|
|
16
18
|
--text-secondary: #94a3b8;
|
|
17
|
-
--border: #
|
|
19
|
+
--border: #475569;
|
|
18
20
|
--success: #10b981;
|
|
19
21
|
--error: #ef4444;
|
|
20
22
|
--warning: #f59e0b;
|
|
23
|
+
--info: #3b82f6;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
body {
|
|
24
|
-
font-family: 'Segoe UI',
|
|
27
|
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
25
28
|
background: var(--bg);
|
|
26
29
|
color: var(--text);
|
|
27
30
|
line-height: 1.6;
|
|
31
|
+
overflow-x: hidden;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
/* Loading Animation */
|
|
35
|
+
.loader {
|
|
36
|
+
position: fixed;
|
|
37
|
+
top: 0; left: 0; width: 100%; height: 100%;
|
|
38
|
+
background: var(--bg);
|
|
39
|
+
display: flex; justify-content: center; align-items: center;
|
|
40
|
+
z-index: 9999;
|
|
41
|
+
transition: opacity 0.5s;
|
|
42
|
+
}
|
|
43
|
+
.loader.hidden { opacity: 0; pointer-events: none; }
|
|
44
|
+
.loader-spinner {
|
|
45
|
+
width: 50px; height: 50px;
|
|
46
|
+
border: 3px solid var(--surface);
|
|
47
|
+
border-top-color: var(--primary);
|
|
48
|
+
border-radius: 50%;
|
|
49
|
+
animation: spin 1s linear infinite;
|
|
50
|
+
}
|
|
51
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
52
|
+
|
|
53
|
+
/* Connection Status */
|
|
54
|
+
.connection-status {
|
|
55
|
+
position: fixed;
|
|
56
|
+
top: 20px; left: 20px;
|
|
57
|
+
padding: 8px 16px;
|
|
58
|
+
border-radius: 20px;
|
|
59
|
+
font-size: 12px;
|
|
60
|
+
font-weight: bold;
|
|
61
|
+
z-index: 1000;
|
|
62
|
+
display: flex; align-items: center; gap: 8px;
|
|
63
|
+
transition: all 0.3s;
|
|
64
|
+
}
|
|
65
|
+
.connection-status.online {
|
|
66
|
+
background: rgba(16, 185, 129, 0.2);
|
|
67
|
+
color: var(--success);
|
|
68
|
+
border: 1px solid var(--success);
|
|
69
|
+
}
|
|
70
|
+
.connection-status.offline {
|
|
71
|
+
background: rgba(239, 68, 68, 0.2);
|
|
72
|
+
color: var(--error);
|
|
73
|
+
border: 1px solid var(--error);
|
|
74
|
+
}
|
|
75
|
+
.status-dot {
|
|
76
|
+
width: 8px; height: 8px;
|
|
77
|
+
border-radius: 50%;
|
|
78
|
+
background: currentColor;
|
|
79
|
+
animation: pulse 2s infinite;
|
|
34
80
|
}
|
|
81
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
35
82
|
|
|
36
83
|
header {
|
|
37
|
-
background:
|
|
84
|
+
background: rgba(30, 41, 59, 0.8);
|
|
85
|
+
backdrop-filter: blur(10px);
|
|
38
86
|
border-bottom: 1px solid var(--border);
|
|
39
87
|
padding: 20px 0;
|
|
40
|
-
|
|
88
|
+
position: sticky;
|
|
89
|
+
top: 0;
|
|
90
|
+
z-index: 100;
|
|
41
91
|
}
|
|
42
92
|
|
|
43
93
|
.header-content {
|
|
@@ -55,14 +105,18 @@
|
|
|
55
105
|
background: linear-gradient(135deg, var(--primary), #a855f7);
|
|
56
106
|
-webkit-background-clip: text;
|
|
57
107
|
-webkit-text-fill-color: transparent;
|
|
108
|
+
display: flex; align-items: center; gap: 10px;
|
|
109
|
+
}
|
|
110
|
+
.logo::before {
|
|
111
|
+
content: "◈";
|
|
112
|
+
font-size: 32px;
|
|
58
113
|
}
|
|
59
114
|
|
|
60
|
-
.
|
|
61
|
-
display: flex;
|
|
62
|
-
gap: 10px;
|
|
115
|
+
.user-menu {
|
|
116
|
+
display: flex; align-items: center; gap: 15px;
|
|
63
117
|
}
|
|
64
118
|
|
|
65
|
-
|
|
119
|
+
.btn {
|
|
66
120
|
background: var(--primary);
|
|
67
121
|
color: white;
|
|
68
122
|
border: none;
|
|
@@ -71,265 +125,430 @@
|
|
|
71
125
|
cursor: pointer;
|
|
72
126
|
font-size: 14px;
|
|
73
127
|
transition: all 0.3s;
|
|
128
|
+
display: inline-flex; align-items: center; gap: 8px;
|
|
74
129
|
}
|
|
75
|
-
|
|
76
|
-
button:hover {
|
|
130
|
+
.btn:hover {
|
|
77
131
|
background: var(--primary-dark);
|
|
78
132
|
transform: translateY(-2px);
|
|
133
|
+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
|
|
79
134
|
}
|
|
80
|
-
|
|
81
|
-
button.secondary {
|
|
135
|
+
.btn.secondary {
|
|
82
136
|
background: transparent;
|
|
83
137
|
border: 1px solid var(--border);
|
|
84
138
|
}
|
|
139
|
+
.btn.secondary:hover {
|
|
140
|
+
background: var(--surface);
|
|
141
|
+
border-color: var(--primary);
|
|
142
|
+
}
|
|
143
|
+
.btn.danger { background: var(--error); }
|
|
144
|
+
.btn.success { background: var(--success); }
|
|
145
|
+
.btn.small { padding: 6px 12px; font-size: 12px; }
|
|
85
146
|
|
|
86
|
-
|
|
87
|
-
|
|
147
|
+
.container {
|
|
148
|
+
max-width: 1200px;
|
|
149
|
+
margin: 0 auto;
|
|
150
|
+
padding: 30px 20px;
|
|
88
151
|
}
|
|
89
152
|
|
|
90
|
-
|
|
153
|
+
/* Stats Cards */
|
|
154
|
+
.stats-grid {
|
|
91
155
|
display: grid;
|
|
92
|
-
grid-template-columns: repeat(auto-fit, minmax(
|
|
156
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
93
157
|
gap: 20px;
|
|
94
158
|
margin-bottom: 30px;
|
|
95
159
|
}
|
|
96
160
|
|
|
161
|
+
.stat-card {
|
|
162
|
+
background: var(--surface);
|
|
163
|
+
border: 1px solid var(--border);
|
|
164
|
+
border-radius: 16px;
|
|
165
|
+
padding: 24px;
|
|
166
|
+
position: relative;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
transition: transform 0.3s;
|
|
169
|
+
}
|
|
170
|
+
.stat-card:hover {
|
|
171
|
+
transform: translateY(-5px);
|
|
172
|
+
border-color: var(--primary);
|
|
173
|
+
}
|
|
174
|
+
.stat-card::before {
|
|
175
|
+
content: "";
|
|
176
|
+
position: absolute;
|
|
177
|
+
top: 0; left: 0; right: 0; height: 3px;
|
|
178
|
+
background: linear-gradient(90deg, var(--primary), var(--primary-light));
|
|
179
|
+
transform: scaleX(0);
|
|
180
|
+
transition: transform 0.3s;
|
|
181
|
+
}
|
|
182
|
+
.stat-card:hover::before { transform: scaleX(1); }
|
|
183
|
+
|
|
184
|
+
.stat-icon {
|
|
185
|
+
font-size: 32px;
|
|
186
|
+
margin-bottom: 10px;
|
|
187
|
+
}
|
|
188
|
+
.stat-value {
|
|
189
|
+
font-size: 36px;
|
|
190
|
+
font-weight: bold;
|
|
191
|
+
color: var(--primary);
|
|
192
|
+
line-height: 1;
|
|
193
|
+
}
|
|
194
|
+
.stat-label {
|
|
195
|
+
color: var(--text-secondary);
|
|
196
|
+
font-size: 14px;
|
|
197
|
+
margin-top: 5px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Cards */
|
|
97
201
|
.card {
|
|
98
202
|
background: var(--surface);
|
|
99
203
|
border: 1px solid var(--border);
|
|
100
|
-
border-radius:
|
|
204
|
+
border-radius: 16px;
|
|
101
205
|
padding: 24px;
|
|
206
|
+
margin-bottom: 24px;
|
|
102
207
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
|
208
|
+
animation: slideUp 0.5s ease-out;
|
|
209
|
+
}
|
|
210
|
+
@keyframes slideUp {
|
|
211
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
212
|
+
to { opacity: 1; transform: translateY(0); }
|
|
103
213
|
}
|
|
104
214
|
|
|
105
|
-
.card
|
|
106
|
-
|
|
107
|
-
|
|
215
|
+
.card-header {
|
|
216
|
+
display: flex;
|
|
217
|
+
justify-content: space-between;
|
|
218
|
+
align-items: center;
|
|
219
|
+
margin-bottom: 20px;
|
|
220
|
+
padding-bottom: 15px;
|
|
221
|
+
border-bottom: 1px solid var(--border);
|
|
222
|
+
}
|
|
223
|
+
.card-title {
|
|
108
224
|
font-size: 20px;
|
|
225
|
+
font-weight: bold;
|
|
226
|
+
display: flex; align-items: center; gap: 10px;
|
|
109
227
|
}
|
|
110
228
|
|
|
229
|
+
/* Form Elements */
|
|
111
230
|
.form-group {
|
|
112
|
-
margin-bottom:
|
|
231
|
+
margin-bottom: 20px;
|
|
113
232
|
}
|
|
114
|
-
|
|
115
233
|
label {
|
|
116
234
|
display: block;
|
|
117
235
|
margin-bottom: 8px;
|
|
118
236
|
color: var(--text-secondary);
|
|
119
237
|
font-size: 14px;
|
|
238
|
+
font-weight: 500;
|
|
120
239
|
}
|
|
121
|
-
|
|
122
240
|
input, textarea, select {
|
|
123
241
|
width: 100%;
|
|
124
|
-
padding: 12px;
|
|
242
|
+
padding: 12px 16px;
|
|
125
243
|
background: var(--bg);
|
|
126
244
|
border: 1px solid var(--border);
|
|
127
245
|
border-radius: 8px;
|
|
128
246
|
color: var(--text);
|
|
129
247
|
font-size: 14px;
|
|
130
|
-
|
|
248
|
+
transition: all 0.3s;
|
|
249
|
+
}
|
|
250
|
+
input:focus, textarea:focus, select:focus {
|
|
251
|
+
outline: none;
|
|
252
|
+
border-color: var(--primary);
|
|
253
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
|
131
254
|
}
|
|
132
|
-
|
|
133
255
|
textarea {
|
|
134
256
|
min-height: 200px;
|
|
135
|
-
font-family: 'Courier New', monospace;
|
|
257
|
+
font-family: 'Fira Code', 'Courier New', monospace;
|
|
136
258
|
direction: ltr;
|
|
259
|
+
resize: vertical;
|
|
137
260
|
}
|
|
138
261
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
262
|
+
/* Code Editor Style */
|
|
263
|
+
.code-editor {
|
|
264
|
+
position: relative;
|
|
265
|
+
background: #0d1117;
|
|
266
|
+
border: 1px solid #30363d;
|
|
267
|
+
border-radius: 8px;
|
|
268
|
+
overflow: hidden;
|
|
269
|
+
}
|
|
270
|
+
.code-header {
|
|
271
|
+
background: #161b22;
|
|
272
|
+
padding: 8px 16px;
|
|
273
|
+
display: flex;
|
|
274
|
+
gap: 8px;
|
|
275
|
+
align-items: center;
|
|
276
|
+
border-bottom: 1px solid #30363d;
|
|
144
277
|
}
|
|
278
|
+
.code-dot {
|
|
279
|
+
width: 12px; height: 12px;
|
|
280
|
+
border-radius: 50%;
|
|
281
|
+
}
|
|
282
|
+
.code-dot.red { background: #ff5f56; }
|
|
283
|
+
.code-dot.yellow { background: #ffbd2e; }
|
|
284
|
+
.code-dot.green { background: #27c93f; }
|
|
145
285
|
|
|
146
|
-
|
|
286
|
+
/* Tabs */
|
|
287
|
+
.tabs {
|
|
288
|
+
display: flex;
|
|
289
|
+
gap: 5px;
|
|
290
|
+
margin-bottom: 20px;
|
|
147
291
|
background: var(--bg);
|
|
148
|
-
padding:
|
|
292
|
+
padding: 5px;
|
|
149
293
|
border-radius: 8px;
|
|
150
|
-
|
|
151
|
-
border: 1px solid var(--border);
|
|
294
|
+
width: fit-content;
|
|
152
295
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
296
|
+
.tab {
|
|
297
|
+
padding: 10px 20px;
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
border-radius: 6px;
|
|
300
|
+
transition: all 0.3s;
|
|
301
|
+
border: none;
|
|
302
|
+
background: transparent;
|
|
303
|
+
color: var(--text-secondary);
|
|
304
|
+
font-weight: 500;
|
|
305
|
+
}
|
|
306
|
+
.tab.active {
|
|
307
|
+
background: var(--primary);
|
|
308
|
+
color: white;
|
|
309
|
+
}
|
|
310
|
+
.tab:hover:not(.active) {
|
|
311
|
+
color: var(--text);
|
|
312
|
+
background: var(--surface-hover);
|
|
158
313
|
}
|
|
159
314
|
|
|
160
|
-
.
|
|
161
|
-
|
|
162
|
-
|
|
315
|
+
.tab-content {
|
|
316
|
+
display: none;
|
|
317
|
+
animation: fadeIn 0.3s;
|
|
163
318
|
}
|
|
319
|
+
.tab-content.active { display: block; }
|
|
320
|
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
164
321
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
border-radius: 8px;
|
|
322
|
+
/* API Key Display */
|
|
323
|
+
.api-key-box {
|
|
324
|
+
background: linear-gradient(135deg, #1e293b, #0f172a);
|
|
169
325
|
border: 1px solid var(--border);
|
|
170
|
-
|
|
326
|
+
border-radius: 12px;
|
|
327
|
+
padding: 20px;
|
|
171
328
|
display: flex;
|
|
172
329
|
justify-content: space-between;
|
|
173
330
|
align-items: center;
|
|
174
|
-
|
|
331
|
+
gap: 20px;
|
|
332
|
+
font-family: 'Fira Code', monospace;
|
|
175
333
|
word-break: break-all;
|
|
176
334
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
position: fixed;
|
|
181
|
-
top: 0;
|
|
182
|
-
left: 0;
|
|
183
|
-
width: 100%;
|
|
184
|
-
height: 100%;
|
|
185
|
-
background: rgba(0,0,0,0.8);
|
|
186
|
-
z-index: 1000;
|
|
187
|
-
justify-content: center;
|
|
188
|
-
align-items: center;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
.modal.active {
|
|
192
|
-
display: flex;
|
|
335
|
+
.api-key-text {
|
|
336
|
+
font-size: 16px;
|
|
337
|
+
letter-spacing: 1px;
|
|
193
338
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
padding: 30px;
|
|
198
|
-
border-radius: 16px;
|
|
199
|
-
max-width: 500px;
|
|
200
|
-
width: 90%;
|
|
201
|
-
max-height: 90vh;
|
|
202
|
-
overflow-y: auto;
|
|
339
|
+
.api-key-hidden {
|
|
340
|
+
filter: blur(4px);
|
|
341
|
+
user-select: none;
|
|
203
342
|
}
|
|
204
343
|
|
|
344
|
+
/* Pages List */
|
|
205
345
|
.pages-list {
|
|
206
346
|
list-style: none;
|
|
347
|
+
display: grid;
|
|
348
|
+
gap: 12px;
|
|
207
349
|
}
|
|
208
|
-
|
|
209
350
|
.page-item {
|
|
210
351
|
background: var(--bg);
|
|
211
352
|
border: 1px solid var(--border);
|
|
212
|
-
border-radius:
|
|
353
|
+
border-radius: 12px;
|
|
213
354
|
padding: 16px;
|
|
214
|
-
margin-bottom: 12px;
|
|
215
355
|
display: flex;
|
|
216
356
|
justify-content: space-between;
|
|
217
357
|
align-items: center;
|
|
358
|
+
transition: all 0.3s;
|
|
359
|
+
animation: slideIn 0.3s ease-out;
|
|
360
|
+
}
|
|
361
|
+
@keyframes slideIn {
|
|
362
|
+
from { opacity: 0; transform: translateX(-20px); }
|
|
363
|
+
to { opacity: 1; transform: translateX(0); }
|
|
364
|
+
}
|
|
365
|
+
.page-item:hover {
|
|
366
|
+
border-color: var(--primary);
|
|
367
|
+
transform: translateX(5px);
|
|
368
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
|
218
369
|
}
|
|
219
|
-
|
|
220
370
|
.page-info h3 {
|
|
221
|
-
margin-bottom: 4px;
|
|
222
371
|
font-size: 16px;
|
|
372
|
+
margin-bottom: 4px;
|
|
373
|
+
color: var(--primary-light);
|
|
223
374
|
}
|
|
224
|
-
|
|
225
375
|
.page-meta {
|
|
226
376
|
font-size: 12px;
|
|
227
377
|
color: var(--text-secondary);
|
|
378
|
+
display: flex; gap: 10px; align-items: center;
|
|
228
379
|
}
|
|
229
|
-
|
|
230
380
|
.badge {
|
|
231
381
|
display: inline-block;
|
|
232
382
|
padding: 4px 8px;
|
|
233
383
|
border-radius: 4px;
|
|
234
|
-
font-size:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
.badge.warning {
|
|
240
|
-
background: var(--warning);
|
|
384
|
+
font-size: 11px;
|
|
385
|
+
font-weight: bold;
|
|
386
|
+
text-transform: uppercase;
|
|
241
387
|
}
|
|
388
|
+
.badge.public { background: rgba(16, 185, 129, 0.2); color: var(--success); }
|
|
389
|
+
.badge.private { background: rgba(239, 68, 68, 0.2); color: var(--error); }
|
|
242
390
|
|
|
243
|
-
|
|
391
|
+
/* Analysis Result */
|
|
392
|
+
.analysis-box {
|
|
244
393
|
background: var(--bg);
|
|
245
|
-
border-radius:
|
|
246
|
-
padding:
|
|
247
|
-
margin-top:
|
|
394
|
+
border-radius: 12px;
|
|
395
|
+
padding: 20px;
|
|
396
|
+
margin-top: 20px;
|
|
248
397
|
border-right: 4px solid var(--success);
|
|
249
398
|
}
|
|
250
|
-
|
|
251
|
-
.analysis-result.has-warnings {
|
|
399
|
+
.analysis-box.has-warnings {
|
|
252
400
|
border-right-color: var(--warning);
|
|
253
401
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
color: var(--warning);
|
|
257
|
-
margin: 4px 0;
|
|
258
|
-
font-size: 14px;
|
|
402
|
+
.analysis-box.error {
|
|
403
|
+
border-right-color: var(--error);
|
|
259
404
|
}
|
|
260
405
|
|
|
261
|
-
|
|
406
|
+
/* Toast Notifications */
|
|
407
|
+
.toast-container {
|
|
262
408
|
position: fixed;
|
|
263
409
|
bottom: 20px;
|
|
264
410
|
left: 20px;
|
|
411
|
+
z-index: 2000;
|
|
412
|
+
display: flex;
|
|
413
|
+
flex-direction: column;
|
|
414
|
+
gap: 10px;
|
|
415
|
+
}
|
|
416
|
+
.toast {
|
|
265
417
|
background: var(--surface);
|
|
266
418
|
border: 1px solid var(--border);
|
|
267
419
|
padding: 16px 24px;
|
|
268
|
-
border-radius:
|
|
420
|
+
border-radius: 12px;
|
|
269
421
|
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.5);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
422
|
+
display: flex;
|
|
423
|
+
align-items: center;
|
|
424
|
+
gap: 12px;
|
|
425
|
+
min-width: 300px;
|
|
426
|
+
animation: slideInLeft 0.3s ease-out;
|
|
427
|
+
border-right: 4px solid var(--info);
|
|
274
428
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
transform:
|
|
278
|
-
opacity: 1;
|
|
429
|
+
@keyframes slideInLeft {
|
|
430
|
+
from { transform: translateX(-100%); opacity: 0; }
|
|
431
|
+
to { transform: translateX(0); opacity: 1; }
|
|
279
432
|
}
|
|
433
|
+
.toast.success { border-right-color: var(--success); }
|
|
434
|
+
.toast.error { border-right-color: var(--error); }
|
|
435
|
+
.toast.warning { border-right-color: var(--warning); }
|
|
280
436
|
|
|
281
|
-
|
|
282
|
-
.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
.
|
|
437
|
+
/* Live Activity */
|
|
438
|
+
.activity-feed {
|
|
439
|
+
max-height: 300px;
|
|
440
|
+
overflow-y: auto;
|
|
441
|
+
}
|
|
442
|
+
.activity-item {
|
|
287
443
|
display: flex;
|
|
288
|
-
|
|
289
|
-
|
|
444
|
+
align-items: center;
|
|
445
|
+
gap: 12px;
|
|
446
|
+
padding: 12px;
|
|
290
447
|
border-bottom: 1px solid var(--border);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
.tab {
|
|
295
|
-
padding: 8px 16px;
|
|
296
|
-
cursor: pointer;
|
|
297
|
-
border-radius: 6px;
|
|
298
|
-
transition: all 0.2s;
|
|
448
|
+
font-size: 14px;
|
|
449
|
+
animation: fadeIn 0.3s;
|
|
299
450
|
}
|
|
300
|
-
|
|
301
|
-
.
|
|
302
|
-
|
|
303
|
-
|
|
451
|
+
.activity-item:last-child { border-bottom: none; }
|
|
452
|
+
.activity-time {
|
|
453
|
+
color: var(--text-secondary);
|
|
454
|
+
font-size: 12px;
|
|
455
|
+
margin-right: auto;
|
|
304
456
|
}
|
|
305
457
|
|
|
306
|
-
|
|
458
|
+
/* Modal */
|
|
459
|
+
.modal-overlay {
|
|
307
460
|
display: none;
|
|
461
|
+
position: fixed;
|
|
462
|
+
top: 0; left: 0; width: 100%; height: 100%;
|
|
463
|
+
background: rgba(0,0,0,0.8);
|
|
464
|
+
z-index: 1000;
|
|
465
|
+
justify-content: center;
|
|
466
|
+
align-items: center;
|
|
467
|
+
backdrop-filter: blur(5px);
|
|
468
|
+
}
|
|
469
|
+
.modal-overlay.active { display: flex; animation: fadeIn 0.3s; }
|
|
470
|
+
.modal-content {
|
|
471
|
+
background: var(--surface);
|
|
472
|
+
border: 1px solid var(--border);
|
|
473
|
+
border-radius: 20px;
|
|
474
|
+
padding: 30px;
|
|
475
|
+
max-width: 500px;
|
|
476
|
+
width: 90%;
|
|
477
|
+
max-height: 90vh;
|
|
478
|
+
overflow-y: auto;
|
|
479
|
+
animation: scaleIn 0.3s;
|
|
480
|
+
}
|
|
481
|
+
@keyframes scaleIn {
|
|
482
|
+
from { transform: scale(0.9); opacity: 0; }
|
|
483
|
+
to { transform: scale(1); opacity: 1; }
|
|
308
484
|
}
|
|
309
485
|
|
|
310
|
-
|
|
311
|
-
|
|
486
|
+
/* Welcome Screen */
|
|
487
|
+
.hero {
|
|
488
|
+
text-align: center;
|
|
489
|
+
padding: 80px 20px;
|
|
490
|
+
background: radial-gradient(circle at center, rgba(99,102,241,0.1) 0%, transparent 70%);
|
|
491
|
+
border-radius: 24px;
|
|
492
|
+
margin-bottom: 40px;
|
|
493
|
+
}
|
|
494
|
+
.hero h1 {
|
|
495
|
+
font-size: 48px;
|
|
496
|
+
margin-bottom: 20px;
|
|
497
|
+
background: linear-gradient(135deg, var(--primary), #a855f7);
|
|
498
|
+
-webkit-background-clip: text;
|
|
499
|
+
-webkit-text-fill-color: transparent;
|
|
500
|
+
}
|
|
501
|
+
.hero p {
|
|
502
|
+
color: var(--text-secondary);
|
|
503
|
+
font-size: 18px;
|
|
504
|
+
max-width: 600px;
|
|
505
|
+
margin: 0 auto 30px;
|
|
312
506
|
}
|
|
313
507
|
|
|
508
|
+
/* Responsive */
|
|
314
509
|
@media (max-width: 768px) {
|
|
315
|
-
.grid { grid-template-columns: 1fr; }
|
|
316
|
-
.
|
|
317
|
-
.
|
|
510
|
+
.stats-grid { grid-template-columns: 1fr; }
|
|
511
|
+
.header-content { flex-direction: column; gap: 15px; }
|
|
512
|
+
.hero h1 { font-size: 36px; }
|
|
318
513
|
}
|
|
514
|
+
|
|
515
|
+
.hidden { display: none !important; }
|
|
319
516
|
</style>
|
|
320
517
|
</head>
|
|
321
518
|
<body>
|
|
322
519
|
|
|
520
|
+
<!-- Loading Screen -->
|
|
521
|
+
<div class="loader" id="loader">
|
|
522
|
+
<div class="loader-spinner"></div>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
<!-- Connection Status -->
|
|
526
|
+
<div class="connection-status offline" id="connectionStatus">
|
|
527
|
+
<span class="status-dot"></span>
|
|
528
|
+
<span id="connectionText">غير متصل</span>
|
|
529
|
+
</div>
|
|
530
|
+
|
|
531
|
+
<!-- Toast Container -->
|
|
532
|
+
<div class="toast-container" id="toastContainer"></div>
|
|
533
|
+
|
|
323
534
|
<header>
|
|
324
535
|
<div class="header-content">
|
|
325
536
|
<div class="logo">NOHO Platform</div>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
<button class="secondary" onclick="showModal('
|
|
537
|
+
|
|
538
|
+
<div class="user-menu" id="guestMenu">
|
|
539
|
+
<button class="btn secondary" onclick="showModal('loginModal')">
|
|
540
|
+
<span>🔑</span> تسجيل الدخول
|
|
541
|
+
</button>
|
|
542
|
+
<button class="btn" onclick="showModal('registerModal')">
|
|
543
|
+
<span>✨</span> إنشاء حساب
|
|
544
|
+
</button>
|
|
329
545
|
</div>
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
<
|
|
546
|
+
|
|
547
|
+
<div class="user-menu hidden" id="userMenu">
|
|
548
|
+
<span id="usernameDisplay" style="font-weight: bold;"></span>
|
|
549
|
+
<button class="btn secondary small" onclick="logout()">
|
|
550
|
+
<span>🚪</span> خروج
|
|
551
|
+
</button>
|
|
333
552
|
</div>
|
|
334
553
|
</div>
|
|
335
554
|
</header>
|
|
@@ -337,173 +556,296 @@
|
|
|
337
556
|
<div class="container">
|
|
338
557
|
<!-- Guest View -->
|
|
339
558
|
<div id="guestView">
|
|
340
|
-
<div class="
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
559
|
+
<div class="hero">
|
|
560
|
+
<h1>🚀 NOHO Platform</h1>
|
|
561
|
+
<p>منصة متكاملة لإنشاء صفحات ويب ذكية مع تحليل AI، ربط API، واستضافة فورية. بناء وتشغيل صفحاتك الديناميكية في ثوانٍ!</p>
|
|
562
|
+
<button class="btn" style="font-size: 18px; padding: 15px 40px;" onclick="showModal('registerModal')">
|
|
563
|
+
ابدأ الآن مجاناً
|
|
564
|
+
</button>
|
|
565
|
+
</div>
|
|
566
|
+
|
|
567
|
+
<div class="stats-grid">
|
|
568
|
+
<div class="stat-card">
|
|
569
|
+
<div class="stat-icon">⚡</div>
|
|
570
|
+
<div class="stat-value" id="publicPages">0</div>
|
|
571
|
+
<div class="stat-label">صفحة عامة</div>
|
|
572
|
+
</div>
|
|
573
|
+
<div class="stat-card">
|
|
574
|
+
<div class="stat-icon">👥</div>
|
|
575
|
+
<div class="stat-value" id="publicUsers">0</div>
|
|
576
|
+
<div class="stat-label">مستخدم نشط</div>
|
|
577
|
+
</div>
|
|
578
|
+
<div class="stat-card">
|
|
579
|
+
<div class="stat-icon">🤖</div>
|
|
580
|
+
<div class="stat-value">GPT-4</div>
|
|
581
|
+
<div class="stat-label">ذكاء اصطناعي</div>
|
|
350
582
|
</div>
|
|
351
583
|
</div>
|
|
352
584
|
</div>
|
|
353
585
|
|
|
354
586
|
<!-- Dashboard View -->
|
|
355
587
|
<div id="dashboardView" class="hidden">
|
|
356
|
-
<div class="stats">
|
|
357
|
-
<div class="stat-
|
|
588
|
+
<div class="stats-grid">
|
|
589
|
+
<div class="stat-card">
|
|
590
|
+
<div class="stat-icon">📄</div>
|
|
358
591
|
<div class="stat-value" id="statPages">0</div>
|
|
359
|
-
<div class="stat-label"
|
|
592
|
+
<div class="stat-label">صفحاتي</div>
|
|
360
593
|
</div>
|
|
361
|
-
<div class="stat-
|
|
594
|
+
<div class="stat-card">
|
|
595
|
+
<div class="stat-icon">👁️</div>
|
|
362
596
|
<div class="stat-value" id="statViews">0</div>
|
|
363
|
-
<div class="stat-label"
|
|
597
|
+
<div class="stat-label">إجمالي المشاهدات</div>
|
|
364
598
|
</div>
|
|
365
|
-
<div class="stat-
|
|
599
|
+
<div class="stat-card">
|
|
600
|
+
<div class="stat-icon">📊</div>
|
|
366
601
|
<div class="stat-value" id="statRequests">0</div>
|
|
367
602
|
<div class="stat-label">الطلبات</div>
|
|
368
603
|
</div>
|
|
369
604
|
</div>
|
|
370
605
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
<div class="card
|
|
374
|
-
<
|
|
375
|
-
<
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
</div>
|
|
386
|
-
<div style="background: var(--bg); padding: 16px; border-radius: 8px; direction: ltr; text-align: left; overflow-x: auto;">
|
|
387
|
-
<code style="color: var(--text-secondary);">
|
|
388
|
-
curl -X POST http://localhost:5000/api/pages \<br>
|
|
389
|
-
-H "X-API-Key: <span class="key-placeholder">YOUR_KEY</span>" \<br>
|
|
390
|
-
-d '{"route":"/hello","code":"res.send(\"Hello\")"}'
|
|
391
|
-
</code>
|
|
606
|
+
<!-- API Key Section -->
|
|
607
|
+
<div class="card">
|
|
608
|
+
<div class="card-header">
|
|
609
|
+
<div class="card-title">🔑 مفتاح API</div>
|
|
610
|
+
<button class="btn small secondary" onclick="regenerateKey()">تجديد</button>
|
|
611
|
+
</div>
|
|
612
|
+
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
|
613
|
+
استخدم هذا المفتاح للوصول إلى API من أي لغة برمجة
|
|
614
|
+
</p>
|
|
615
|
+
<div class="api-key-box">
|
|
616
|
+
<span class="api-key-text api-key-hidden" id="apiKeyDisplay">••••••••••••••••••••••••••</span>
|
|
617
|
+
<div style="display: flex; gap: 8px;">
|
|
618
|
+
<button class="btn small secondary" onclick="toggleApiKey()">👁️ إظهار</button>
|
|
619
|
+
<button class="btn small" onclick="copyApiKey()">📋 نسخ</button>
|
|
392
620
|
</div>
|
|
393
621
|
</div>
|
|
622
|
+
<div style="margin-top: 16px; background: var(--bg); padding: 16px; border-radius: 8px; direction: ltr; font-family: monospace; font-size: 13px; overflow-x: auto;">
|
|
623
|
+
<code style="color: var(--text-secondary);">
|
|
624
|
+
curl -X POST http://localhost:5000/api/pages \<br>
|
|
625
|
+
-H "X-API-Key: <span style="color: var(--primary);">YOUR_KEY_HERE</span>" \<br>
|
|
626
|
+
-d '{"route":"/hello","code":"res.send(\"Hello World\")"}'
|
|
627
|
+
</code>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
394
630
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
631
|
+
<!-- Create Page -->
|
|
632
|
+
<div class="card">
|
|
633
|
+
<div class="card-header">
|
|
634
|
+
<div class="card-title">✨ إنشاء صفحة جديدة</div>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
<div class="tabs">
|
|
638
|
+
<button class="tab active" onclick="switchTab('codeTab', this)">📝 كود مخصص</button>
|
|
639
|
+
<button class="tab" onclick="switchTab('aiTab', this)">🤖 توليد بالAI</button>
|
|
640
|
+
</div>
|
|
403
641
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
642
|
+
<div id="codeTab" class="tab-content active">
|
|
643
|
+
<div class="form-group">
|
|
644
|
+
<label>📍 مسار الصفحة (Route)</label>
|
|
645
|
+
<input type="text" id="pageRoute" placeholder="/my-awesome-page" dir="ltr">
|
|
646
|
+
</div>
|
|
647
|
+
|
|
648
|
+
<div class="form-group">
|
|
649
|
+
<label>💻 كود JavaScript</label>
|
|
650
|
+
<div class="code-editor">
|
|
651
|
+
<div class="code-header">
|
|
652
|
+
<div class="code-dot red"></div>
|
|
653
|
+
<div class="code-dot yellow"></div>
|
|
654
|
+
<div class="code-dot green"></div>
|
|
655
|
+
<span style="color: var(--text-secondary); font-size: 12px; margin-right: 10px;">main.js</span>
|
|
656
|
+
</div>
|
|
657
|
+
<textarea id="pageCode" placeholder="// اكتب كودك هنا...
|
|
413
658
|
res.send(`
|
|
414
|
-
|
|
415
|
-
<
|
|
659
|
+
<!DOCTYPE html>
|
|
660
|
+
<html>
|
|
661
|
+
<head><title>صفحتي</title></head>
|
|
662
|
+
<body>
|
|
663
|
+
<h1>مرحباً بك في NOHO!</h1>
|
|
664
|
+
<p>الوقت: ${new Date().toLocaleString('ar-EG')}</p>
|
|
665
|
+
</body>
|
|
666
|
+
</html>
|
|
416
667
|
`);" dir="ltr"></textarea>
|
|
417
668
|
</div>
|
|
669
|
+
</div>
|
|
418
670
|
|
|
419
|
-
|
|
420
|
-
<button onclick="
|
|
421
|
-
|
|
422
|
-
<div id="analysisResult"></div>
|
|
671
|
+
<div style="display: flex; gap: 10px;">
|
|
672
|
+
<button class="btn secondary" onclick="analyzeCode()">🔍 تحليل الكود</button>
|
|
673
|
+
<button class="btn success" onclick="createPage()">🚀 نشر الصفحة</button>
|
|
423
674
|
</div>
|
|
675
|
+
|
|
676
|
+
<div id="analysisResult"></div>
|
|
677
|
+
</div>
|
|
424
678
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
</div>
|
|
430
|
-
<button onclick="generateWithAI()">✨ توليد الكود</button>
|
|
431
|
-
<div id="aiResult" style="margin-top: 20px;"></div>
|
|
679
|
+
<div id="aiTab" class="tab-content">
|
|
680
|
+
<div class="form-group">
|
|
681
|
+
<label>📝 وصف الصفحة المطلوبة</label>
|
|
682
|
+
<input type="text" id="aiPrompt" placeholder="مثال: صفحة عرض منتجات عصرية مع صور وأسعار">
|
|
432
683
|
</div>
|
|
684
|
+
<button class="btn" onclick="generateWithAI()">✨ توليد الكود الآن</button>
|
|
685
|
+
<div id="aiResult" style="margin-top: 20px;"></div>
|
|
433
686
|
</div>
|
|
687
|
+
</div>
|
|
434
688
|
|
|
689
|
+
<div class="grid" style="display: grid; grid-template-columns: 2fr 1fr; gap: 24px;">
|
|
435
690
|
<!-- My Pages -->
|
|
436
|
-
<div class="card"
|
|
437
|
-
<
|
|
691
|
+
<div class="card">
|
|
692
|
+
<div class="card-header">
|
|
693
|
+
<div class="card-title">📄 صفحاتي</div>
|
|
694
|
+
<button class="btn small secondary" onclick="loadPages()">🔄 تحديث</button>
|
|
695
|
+
</div>
|
|
438
696
|
<ul class="pages-list" id="pagesList">
|
|
439
697
|
<li style="text-align: center; color: var(--text-secondary); padding: 40px;">
|
|
440
698
|
لا توجد صفحات بعد
|
|
441
699
|
</li>
|
|
442
700
|
</ul>
|
|
443
701
|
</div>
|
|
702
|
+
|
|
703
|
+
<!-- Live Activity -->
|
|
704
|
+
<div class="card">
|
|
705
|
+
<div class="card-header">
|
|
706
|
+
<div class="card-title">📡 النشاط المباشر</div>
|
|
707
|
+
</div>
|
|
708
|
+
<div class="activity-feed" id="activityFeed">
|
|
709
|
+
<div class="activity-item">
|
|
710
|
+
<span>🟢</span>
|
|
711
|
+
<span>متصل بالسيرفر</span>
|
|
712
|
+
<span class="activity-time">الآن</span>
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
444
716
|
</div>
|
|
445
717
|
</div>
|
|
446
718
|
</div>
|
|
447
719
|
|
|
448
720
|
<!-- Login Modal -->
|
|
449
|
-
<div class="modal" id="loginModal">
|
|
721
|
+
<div class="modal-overlay" id="loginModal">
|
|
450
722
|
<div class="modal-content">
|
|
451
|
-
<h2 style="margin-bottom: 20px;"
|
|
723
|
+
<h2 style="margin-bottom: 20px;">🔑 تسجيل الدخول</h2>
|
|
452
724
|
<div class="form-group">
|
|
453
|
-
<label
|
|
454
|
-
<input type="email" id="loginEmail" dir="ltr">
|
|
725
|
+
<label>📧 البريد الإلكتروني</label>
|
|
726
|
+
<input type="email" id="loginEmail" dir="ltr" placeholder="example@mail.com">
|
|
455
727
|
</div>
|
|
456
728
|
<div class="form-group">
|
|
457
|
-
<label
|
|
458
|
-
<input type="password" id="loginPassword">
|
|
729
|
+
<label>🔒 كلمة المرور</label>
|
|
730
|
+
<input type="password" id="loginPassword" placeholder="********">
|
|
459
731
|
</div>
|
|
460
|
-
<button
|
|
461
|
-
<button class="secondary"
|
|
732
|
+
<button class="btn" style="width: 100%;" onclick="login()">دخول</button>
|
|
733
|
+
<button class="btn secondary" style="width: 100%; margin-top: 10px;" onclick="hideModal('loginModal')">إلغاء</button>
|
|
462
734
|
</div>
|
|
463
735
|
</div>
|
|
464
736
|
|
|
465
737
|
<!-- Register Modal -->
|
|
466
|
-
<div class="modal" id="registerModal">
|
|
738
|
+
<div class="modal-overlay" id="registerModal">
|
|
467
739
|
<div class="modal-content">
|
|
468
|
-
<h2 style="margin-bottom: 20px;"
|
|
740
|
+
<h2 style="margin-bottom: 20px;">✨ إنشاء حساب جديد</h2>
|
|
469
741
|
<div class="form-group">
|
|
470
|
-
<label
|
|
471
|
-
<input type="text" id="regUsername" placeholder="your-name"
|
|
742
|
+
<label>👤 اسم المستخدم (سيظهر في الرابط)</label>
|
|
743
|
+
<input type="text" id="regUsername" dir="ltr" placeholder="your-name">
|
|
472
744
|
</div>
|
|
473
745
|
<div class="form-group">
|
|
474
|
-
<label
|
|
475
|
-
<input type="email" id="regEmail" dir="ltr">
|
|
746
|
+
<label>📧 البريد الإلكتروني</label>
|
|
747
|
+
<input type="email" id="regEmail" dir="ltr" placeholder="example@mail.com">
|
|
476
748
|
</div>
|
|
477
749
|
<div class="form-group">
|
|
478
|
-
<label
|
|
479
|
-
<input type="password" id="regPassword">
|
|
750
|
+
<label>🔒 كلمة المرور (8 أحرف على الأقل)</label>
|
|
751
|
+
<input type="password" id="regPassword" placeholder="********">
|
|
480
752
|
</div>
|
|
481
|
-
<button
|
|
482
|
-
<button class="secondary"
|
|
753
|
+
<button class="btn" style="width: 100%;" onclick="register()">إنشاء الحساب</button>
|
|
754
|
+
<button class="btn secondary" style="width: 100%; margin-top: 10px;" onclick="hideModal('registerModal')">إلغاء</button>
|
|
483
755
|
</div>
|
|
484
756
|
</div>
|
|
485
757
|
|
|
486
|
-
<!-- API Key
|
|
487
|
-
<div class="modal" id="apiKeyModal">
|
|
758
|
+
<!-- API Key Modal -->
|
|
759
|
+
<div class="modal-overlay" id="apiKeyModal">
|
|
488
760
|
<div class="modal-content">
|
|
489
761
|
<h2 style="margin-bottom: 20px; color: var(--success);">✅ تم إنشاء الحساب!</h2>
|
|
490
762
|
<p style="margin-bottom: 16px;">احفظ مفتاح API هذا جيداً، لن يُعرض مرة أخرى:</p>
|
|
491
|
-
<div class="api-key-
|
|
492
|
-
<span id="newApiKey" style="font-size: 16px;"></span>
|
|
763
|
+
<div class="api-key-box" style="background: rgba(16, 185, 129, 0.1); border-color: var(--success);">
|
|
764
|
+
<span id="newApiKey" style="font-size: 16px; font-weight: bold;"></span>
|
|
493
765
|
</div>
|
|
494
|
-
<button
|
|
495
|
-
<button
|
|
766
|
+
<button class="btn" style="width: 100%; margin-bottom: 10px;" onclick="copyNewApiKey()">📋 نسخ المفتاح</button>
|
|
767
|
+
<button class="btn secondary" style="width: 100%;" onclick="closeApiKeyModal()">متابعة للوحة التحكم</button>
|
|
496
768
|
</div>
|
|
497
769
|
</div>
|
|
498
770
|
|
|
499
|
-
<div class="toast" id="toast"></div>
|
|
500
|
-
|
|
501
771
|
<script>
|
|
502
|
-
// API URL - يعمل تلقائياً على نفس السيرفر
|
|
503
772
|
const API_URL = window.location.origin;
|
|
504
773
|
let currentUser = null;
|
|
505
774
|
let apiKeyVisible = false;
|
|
506
775
|
let storedApiKey = '';
|
|
776
|
+
let ws = null;
|
|
777
|
+
|
|
778
|
+
// إخفاء اللودر
|
|
779
|
+
window.addEventListener('load', () => {
|
|
780
|
+
setTimeout(() => {
|
|
781
|
+
document.getElementById('loader').classList.add('hidden');
|
|
782
|
+
}, 500);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// WebSocket Connection
|
|
786
|
+
function connectWebSocket() {
|
|
787
|
+
const wsUrl = `ws://${window.location.host}`;
|
|
788
|
+
ws = new WebSocket(wsUrl);
|
|
789
|
+
|
|
790
|
+
ws.onopen = () => {
|
|
791
|
+
updateConnectionStatus(true);
|
|
792
|
+
showToast('متصل بالسيرفر', 'success');
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
ws.onclose = () => {
|
|
796
|
+
updateConnectionStatus(false);
|
|
797
|
+
setTimeout(connectWebSocket, 3000); // إعادة المحاولة
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
ws.onmessage = (event) => {
|
|
801
|
+
const data = JSON.parse(event.data);
|
|
802
|
+
handleWebSocketMessage(data);
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
ws.onerror = (error) => {
|
|
806
|
+
console.error('WebSocket Error:', error);
|
|
807
|
+
updateConnectionStatus(false);
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function updateConnectionStatus(connected) {
|
|
812
|
+
const status = document.getElementById('connectionStatus');
|
|
813
|
+
const text = document.getElementById('connectionText');
|
|
814
|
+
|
|
815
|
+
if (connected) {
|
|
816
|
+
status.className = 'connection-status online';
|
|
817
|
+
text.textContent = 'متصل';
|
|
818
|
+
} else {
|
|
819
|
+
status.className = 'connection-status offline';
|
|
820
|
+
text.textContent = 'غير متصل';
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function handleWebSocketMessage(data) {
|
|
825
|
+
if (data.type === 'new_page') {
|
|
826
|
+
addActivity(`🆕 صفحة جديدة: ${data.route} بواسطة ${data.username}`);
|
|
827
|
+
if (currentUser && data.username !== currentUser.username) {
|
|
828
|
+
showToast(`🆕 ${data.username} أنشأ صفحة جديدة!`, 'info');
|
|
829
|
+
}
|
|
830
|
+
} else if (data.type === 'user_online') {
|
|
831
|
+
addActivity(`🟢 ${data.username} دخل الآن`);
|
|
832
|
+
} else if (data.type === 'user_offline') {
|
|
833
|
+
addActivity(`🔴 ${data.username} خرج`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function addActivity(message) {
|
|
838
|
+
const feed = document.getElementById('activityFeed');
|
|
839
|
+
const time = new Date().toLocaleTimeString('ar-EG', {hour: '2-digit', minute: '2-digit'});
|
|
840
|
+
const item = document.createElement('div');
|
|
841
|
+
item.className = 'activity-item';
|
|
842
|
+
item.innerHTML = `
|
|
843
|
+
<span>${message}</span>
|
|
844
|
+
<span class="activity-time">${time}</span>
|
|
845
|
+
`;
|
|
846
|
+
feed.insertBefore(item, feed.firstChild);
|
|
847
|
+
if (feed.children.length > 20) feed.removeChild(feed.lastChild);
|
|
848
|
+
}
|
|
507
849
|
|
|
508
850
|
// Auth Functions
|
|
509
851
|
async function register() {
|
|
@@ -512,12 +854,12 @@ async function register() {
|
|
|
512
854
|
const password = document.getElementById('regPassword').value;
|
|
513
855
|
|
|
514
856
|
if (!username || !email || !password) {
|
|
515
|
-
showToast('أكمل جميع الحقول', 'error');
|
|
857
|
+
showToast('❌ أكمل جميع الحقول', 'error');
|
|
516
858
|
return;
|
|
517
859
|
}
|
|
518
860
|
|
|
519
861
|
if (password.length < 8) {
|
|
520
|
-
showToast('كلمة المرور يجب أن تكون 8 أحرف على الأقل', 'error');
|
|
862
|
+
showToast('❌ كلمة المرور يجب أن تكون 8 أحرف على الأقل', 'error');
|
|
521
863
|
return;
|
|
522
864
|
}
|
|
523
865
|
|
|
@@ -535,11 +877,10 @@ async function register() {
|
|
|
535
877
|
hideModal('registerModal');
|
|
536
878
|
showModal('apiKeyModal');
|
|
537
879
|
} else {
|
|
538
|
-
showToast(data.error || 'خطأ في التسجيل', 'error');
|
|
880
|
+
showToast(data.error || '❌ خطأ في التسجيل', 'error');
|
|
539
881
|
}
|
|
540
882
|
} catch (e) {
|
|
541
|
-
showToast('خطأ في الاتصال
|
|
542
|
-
console.error(e);
|
|
883
|
+
showToast('❌ خطأ في الاتصال', 'error');
|
|
543
884
|
}
|
|
544
885
|
}
|
|
545
886
|
|
|
@@ -548,7 +889,7 @@ async function login() {
|
|
|
548
889
|
const password = document.getElementById('loginPassword').value;
|
|
549
890
|
|
|
550
891
|
if (!email || !password) {
|
|
551
|
-
showToast('أدخل البريد وكلمة المرور', 'error');
|
|
892
|
+
showToast('❌ أدخل البريد وكلمة المرور', 'error');
|
|
552
893
|
return;
|
|
553
894
|
}
|
|
554
895
|
|
|
@@ -565,25 +906,29 @@ async function login() {
|
|
|
565
906
|
currentUser = data.user;
|
|
566
907
|
hideModal('loginModal');
|
|
567
908
|
loadDashboard();
|
|
568
|
-
showToast(
|
|
909
|
+
showToast(`✅ أهلاً ${data.user.username}!`, 'success');
|
|
910
|
+
|
|
911
|
+
// WebSocket Auth
|
|
912
|
+
if (ws && ws.readyState === 1) {
|
|
913
|
+
ws.send(JSON.stringify({ type: 'auth', token: data.token }));
|
|
914
|
+
}
|
|
569
915
|
} else {
|
|
570
|
-
showToast(data.error || 'بيانات غير صحيحة', 'error');
|
|
916
|
+
showToast(data.error || '❌ بيانات غير صحيحة', 'error');
|
|
571
917
|
}
|
|
572
918
|
} catch (e) {
|
|
573
|
-
showToast('خطأ في الاتصال', 'error');
|
|
574
|
-
console.error(e);
|
|
919
|
+
showToast('❌ خطأ في الاتصال', 'error');
|
|
575
920
|
}
|
|
576
921
|
}
|
|
577
922
|
|
|
578
923
|
function logout() {
|
|
579
924
|
localStorage.removeItem('noho_token');
|
|
580
925
|
currentUser = null;
|
|
581
|
-
|
|
926
|
+
showToast('👋 تم تسجيل الخروج', 'info');
|
|
927
|
+
setTimeout(() => location.reload(), 500);
|
|
582
928
|
}
|
|
583
929
|
|
|
584
930
|
function closeApiKeyModal() {
|
|
585
931
|
hideModal('apiKeyModal');
|
|
586
|
-
// تسجيل الدخول تلقائياً بعد التسجيل
|
|
587
932
|
const email = document.getElementById('regEmail').value;
|
|
588
933
|
const password = document.getElementById('regPassword').value;
|
|
589
934
|
document.getElementById('loginEmail').value = email;
|
|
@@ -605,28 +950,25 @@ async function loadDashboard() {
|
|
|
605
950
|
if (data.success) {
|
|
606
951
|
currentUser = data.user;
|
|
607
952
|
|
|
608
|
-
// تحديث الواجهة
|
|
609
953
|
document.getElementById('guestView').classList.add('hidden');
|
|
610
954
|
document.getElementById('dashboardView').classList.remove('hidden');
|
|
611
|
-
document.getElementById('
|
|
612
|
-
document.getElementById('
|
|
613
|
-
document.getElementById('usernameDisplay').textContent = data.user.username
|
|
955
|
+
document.getElementById('guestMenu').classList.add('hidden');
|
|
956
|
+
document.getElementById('userMenu').classList.remove('hidden');
|
|
957
|
+
document.getElementById('usernameDisplay').textContent = `👤 ${data.user.username}`;
|
|
614
958
|
|
|
615
|
-
// تحديث الإحصائيات
|
|
616
959
|
document.getElementById('statPages').textContent = data.stats.totalPages;
|
|
617
|
-
document.getElementById('statViews').textContent = data.stats.pages.reduce((a,b) => a + b.views, 0);
|
|
960
|
+
document.getElementById('statViews').textContent = data.stats.pages.reduce((a,b) => a + (b.views || 0), 0);
|
|
618
961
|
document.getElementById('statRequests').textContent = data.stats.requests;
|
|
619
962
|
|
|
620
|
-
// حفظ مفتاح API
|
|
621
963
|
document.getElementById('apiKeyDisplay').dataset.fullKey = data.user.apiKey;
|
|
964
|
+
document.getElementById('apiKeyDisplay').textContent = '•'.repeat(30);
|
|
622
965
|
|
|
623
|
-
// تحميل الصفحات
|
|
624
966
|
loadPages();
|
|
625
967
|
} else {
|
|
626
968
|
localStorage.removeItem('noho_token');
|
|
627
969
|
}
|
|
628
970
|
} catch (e) {
|
|
629
|
-
console.error('
|
|
971
|
+
console.error('Error:', e);
|
|
630
972
|
}
|
|
631
973
|
}
|
|
632
974
|
|
|
@@ -643,41 +985,41 @@ async function loadPages() {
|
|
|
643
985
|
|
|
644
986
|
const list = document.getElementById('pagesList');
|
|
645
987
|
if (!data.pages || data.pages.length === 0) {
|
|
646
|
-
list.innerHTML = '<li style="text-align: center; color: var(--text-secondary); padding: 40px;"
|
|
988
|
+
list.innerHTML = '<li style="text-align: center; color: var(--text-secondary); padding: 40px;">📭 لا توجد صفحات بعد</li>';
|
|
647
989
|
return;
|
|
648
990
|
}
|
|
649
991
|
|
|
650
992
|
list.innerHTML = data.pages.map(page => `
|
|
651
993
|
<li class="page-item">
|
|
652
994
|
<div class="page-info">
|
|
653
|
-
<h3
|
|
995
|
+
<h3>🔗 ${page.route}</h3>
|
|
654
996
|
<div class="page-meta">
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
${page.public ? '<span class="badge">عام</span>' : '<span class="badge warning">خاص</span>'}
|
|
997
|
+
👁️ ${page.views || 0} مشاهدة | 📅 ${new Date(page.createdAt).toLocaleDateString('ar-SA')}
|
|
998
|
+
${page.public ? '<span class="badge public">عام</span>' : '<span class="badge private">خاص</span>'}
|
|
658
999
|
</div>
|
|
659
1000
|
</div>
|
|
660
|
-
<div>
|
|
1001
|
+
<div style="display: flex; gap: 8px;">
|
|
661
1002
|
<a href="${API_URL}${page.route}" target="_blank">
|
|
662
|
-
<button class="secondary"
|
|
1003
|
+
<button class="btn small secondary">🌐 زيارة</button>
|
|
663
1004
|
</a>
|
|
664
|
-
<button class="danger" onclick="deletePage('${page.id}')"
|
|
1005
|
+
<button class="btn small danger" onclick="deletePage('${page.id}')">🗑️ حذف</button>
|
|
665
1006
|
</div>
|
|
666
1007
|
</li>
|
|
667
1008
|
`).join('');
|
|
668
1009
|
} catch (e) {
|
|
669
|
-
|
|
1010
|
+
showToast('❌ خطأ في تحميل الصفحات', 'error');
|
|
670
1011
|
}
|
|
671
1012
|
}
|
|
672
1013
|
|
|
673
|
-
// Page Functions
|
|
674
1014
|
async function analyzeCode() {
|
|
675
1015
|
const code = document.getElementById('pageCode').value;
|
|
676
|
-
if (!code) return showToast('أدخل الكود أولاً', 'error');
|
|
1016
|
+
if (!code) return showToast('❌ أدخل الكود أولاً', 'error');
|
|
677
1017
|
|
|
678
1018
|
const token = localStorage.getItem('noho_token');
|
|
679
|
-
if (!token) return showToast('سجل دخول أولاً', 'error');
|
|
1019
|
+
if (!token) return showToast('❌ سجل دخول أولاً', 'error');
|
|
680
1020
|
|
|
1021
|
+
showToast('🔍 جاري التحليل...', 'info');
|
|
1022
|
+
|
|
681
1023
|
try {
|
|
682
1024
|
const res = await fetch(`${API_URL}/api/ai/analyze`, {
|
|
683
1025
|
method: 'POST',
|
|
@@ -692,17 +1034,19 @@ async function analyzeCode() {
|
|
|
692
1034
|
const resultDiv = document.getElementById('analysisResult');
|
|
693
1035
|
|
|
694
1036
|
if (data.success) {
|
|
1037
|
+
const hasWarnings = data.warnings && data.warnings.length > 0;
|
|
695
1038
|
resultDiv.innerHTML = `
|
|
696
|
-
<div class="analysis-
|
|
697
|
-
<h4
|
|
698
|
-
${data.warnings ? data.warnings.map(w => `<div
|
|
699
|
-
${data.changes ? data.changes.map(c => `<div style="color: var(--success); margin: 4px 0;"
|
|
700
|
-
${(!data.warnings || data.warnings.length === 0) ? '<div style="color: var(--success);"
|
|
1039
|
+
<div class="analysis-box ${hasWarnings ? 'has-warnings' : ''}">
|
|
1040
|
+
<h4 style="margin-bottom: 10px;">🔍 نتيجة التحليل</h4>
|
|
1041
|
+
${data.warnings ? data.warnings.map(w => `<div style="color: var(--warning); margin: 4px 0;">⚠️ ${w}</div>`).join('') : ''}
|
|
1042
|
+
${data.changes ? data.changes.map(c => `<div style="color: var(--success); margin: 4px 0;">✅ ${c}</div>`).join('') : ''}
|
|
1043
|
+
${(!data.warnings || data.warnings.length === 0) ? '<div style="color: var(--success);">✅ الكود آمن ولا يوجد تحذيرات</div>' : ''}
|
|
701
1044
|
</div>
|
|
702
1045
|
`;
|
|
1046
|
+
showToast('✅ تم التحليل', 'success');
|
|
703
1047
|
}
|
|
704
1048
|
} catch (e) {
|
|
705
|
-
showToast('خطأ في التحليل', 'error');
|
|
1049
|
+
showToast('❌ خطأ في التحليل', 'error');
|
|
706
1050
|
}
|
|
707
1051
|
}
|
|
708
1052
|
|
|
@@ -711,8 +1055,8 @@ async function createPage() {
|
|
|
711
1055
|
const code = document.getElementById('pageCode').value;
|
|
712
1056
|
const token = localStorage.getItem('noho_token');
|
|
713
1057
|
|
|
714
|
-
if (!route || !code) return showToast('أكمل جميع الحقول', 'error');
|
|
715
|
-
if (!token) return showToast('سجل دخول أولاً', 'error');
|
|
1058
|
+
if (!route || !code) return showToast('❌ أكمل جميع الحقول', 'error');
|
|
1059
|
+
if (!token) return showToast('❌ سجل دخول أولاً', 'error');
|
|
716
1060
|
|
|
717
1061
|
try {
|
|
718
1062
|
const res = await fetch(`${API_URL}/api/pages`, {
|
|
@@ -726,28 +1070,36 @@ async function createPage() {
|
|
|
726
1070
|
|
|
727
1071
|
const data = await res.json();
|
|
728
1072
|
if (data.success) {
|
|
729
|
-
showToast('تم إنشاء
|
|
1073
|
+
showToast('✅ تم إنشاء الصفحة بنجاح!', 'success');
|
|
730
1074
|
document.getElementById('pageRoute').value = '';
|
|
731
1075
|
document.getElementById('pageCode').value = '';
|
|
732
1076
|
document.getElementById('analysisResult').innerHTML = '';
|
|
733
1077
|
loadPages();
|
|
1078
|
+
|
|
1079
|
+
// WebSocket broadcast
|
|
1080
|
+
if (ws && ws.readyState === 1) {
|
|
1081
|
+
ws.send(JSON.stringify({
|
|
1082
|
+
type: 'page_created',
|
|
1083
|
+
route: data.page.route,
|
|
1084
|
+
username: currentUser.username
|
|
1085
|
+
}));
|
|
1086
|
+
}
|
|
734
1087
|
} else {
|
|
735
|
-
showToast(data.error || 'خطأ في الإنشاء', 'error');
|
|
1088
|
+
showToast(data.error || '❌ خطأ في الإنشاء', 'error');
|
|
736
1089
|
}
|
|
737
1090
|
} catch (e) {
|
|
738
|
-
showToast('خطأ في الاتصال', 'error');
|
|
739
|
-
console.error(e);
|
|
1091
|
+
showToast('❌ خطأ في الاتصال', 'error');
|
|
740
1092
|
}
|
|
741
1093
|
}
|
|
742
1094
|
|
|
743
1095
|
async function generateWithAI() {
|
|
744
1096
|
const prompt = document.getElementById('aiPrompt').value.trim();
|
|
745
|
-
if (!prompt) return showToast('أدخل وصفاً', 'error');
|
|
1097
|
+
if (!prompt) return showToast('❌ أدخل وصفاً', 'error');
|
|
746
1098
|
|
|
747
1099
|
const token = localStorage.getItem('noho_token');
|
|
748
|
-
if (!token) return showToast('سجل دخول أولاً', 'error');
|
|
1100
|
+
if (!token) return showToast('❌ سجل دخول أولاً', 'error');
|
|
749
1101
|
|
|
750
|
-
showToast('جاري التوليد...', '
|
|
1102
|
+
showToast('🤖 جاري التوليد... قد يستغرق 10-20 ثانية', 'info');
|
|
751
1103
|
|
|
752
1104
|
try {
|
|
753
1105
|
const res = await fetch(`${API_URL}/api/ai/generate`, {
|
|
@@ -762,18 +1114,18 @@ async function generateWithAI() {
|
|
|
762
1114
|
const data = await res.json();
|
|
763
1115
|
if (data.success) {
|
|
764
1116
|
document.getElementById('pageCode').value = data.code;
|
|
765
|
-
|
|
1117
|
+
showToast('✅ تم توليد الكود! راجعه في تبويب "كود مخصص"', 'success');
|
|
766
1118
|
switchTab('codeTab', document.querySelectorAll('.tab')[0]);
|
|
767
1119
|
} else {
|
|
768
|
-
showToast(data.error || 'فشل التوليد', 'error');
|
|
1120
|
+
showToast(data.error || '❌ فشل التوليد', 'error');
|
|
769
1121
|
}
|
|
770
1122
|
} catch (e) {
|
|
771
|
-
showToast('خطأ في الاتصال', 'error');
|
|
1123
|
+
showToast('❌ خطأ في الاتصال', 'error');
|
|
772
1124
|
}
|
|
773
1125
|
}
|
|
774
1126
|
|
|
775
1127
|
async function deletePage(pageId) {
|
|
776
|
-
if (!confirm('هل أنت متأكد من
|
|
1128
|
+
if (!confirm('⚠️ هل أنت متأكد من حذف هذه الصفحة؟')) return;
|
|
777
1129
|
|
|
778
1130
|
const token = localStorage.getItem('noho_token');
|
|
779
1131
|
if (!token) return;
|
|
@@ -785,21 +1137,20 @@ async function deletePage(pageId) {
|
|
|
785
1137
|
});
|
|
786
1138
|
|
|
787
1139
|
if (res.ok) {
|
|
788
|
-
showToast('تم الحذف', 'success');
|
|
1140
|
+
showToast('🗑️ تم الحذف', 'success');
|
|
789
1141
|
loadPages();
|
|
790
1142
|
}
|
|
791
1143
|
} catch (e) {
|
|
792
|
-
showToast('خطأ في الحذف', 'error');
|
|
1144
|
+
showToast('❌ خطأ في الحذف', 'error');
|
|
793
1145
|
}
|
|
794
1146
|
}
|
|
795
1147
|
|
|
796
|
-
// API Key Functions
|
|
797
1148
|
function toggleApiKey() {
|
|
798
1149
|
const display = document.getElementById('apiKeyDisplay');
|
|
799
1150
|
const fullKey = display.dataset.fullKey;
|
|
800
1151
|
|
|
801
1152
|
if (apiKeyVisible) {
|
|
802
|
-
display.textContent = '
|
|
1153
|
+
display.textContent = '•'.repeat(30);
|
|
803
1154
|
apiKeyVisible = false;
|
|
804
1155
|
} else {
|
|
805
1156
|
display.textContent = fullKey || 'غير متوفر';
|
|
@@ -809,21 +1160,21 @@ function toggleApiKey() {
|
|
|
809
1160
|
|
|
810
1161
|
function copyApiKey() {
|
|
811
1162
|
const key = document.getElementById('apiKeyDisplay').dataset.fullKey;
|
|
812
|
-
if (!key) return showToast('لا يوجد مفتاح', 'error');
|
|
1163
|
+
if (!key) return showToast('❌ لا يوجد مفتاح', 'error');
|
|
813
1164
|
|
|
814
1165
|
navigator.clipboard.writeText(key).then(() => {
|
|
815
|
-
showToast('تم النسخ!', 'success');
|
|
1166
|
+
showToast('📋 تم النسخ!', 'success');
|
|
816
1167
|
});
|
|
817
1168
|
}
|
|
818
1169
|
|
|
819
1170
|
function copyNewApiKey() {
|
|
820
1171
|
navigator.clipboard.writeText(storedApiKey).then(() => {
|
|
821
|
-
showToast('تم النسخ!', 'success');
|
|
1172
|
+
showToast('📋 تم النسخ!', 'success');
|
|
822
1173
|
});
|
|
823
1174
|
}
|
|
824
1175
|
|
|
825
1176
|
async function regenerateKey() {
|
|
826
|
-
if (!confirm('سيتم إنشاء مفتاح جديد والقديم سيتوقف؟')) return;
|
|
1177
|
+
if (!confirm('⚠️ سيتم إنشاء مفتاح جديد والقديم سيتوقف؟')) return;
|
|
827
1178
|
|
|
828
1179
|
const token = localStorage.getItem('noho_token');
|
|
829
1180
|
if (!token) return;
|
|
@@ -836,11 +1187,11 @@ async function regenerateKey() {
|
|
|
836
1187
|
|
|
837
1188
|
const data = await res.json();
|
|
838
1189
|
if (data.success) {
|
|
839
|
-
showToast('تم تجديد المفتاح', 'success');
|
|
1190
|
+
showToast('✅ تم تجديد المفتاح', 'success');
|
|
840
1191
|
loadDashboard();
|
|
841
1192
|
}
|
|
842
1193
|
} catch (e) {
|
|
843
|
-
showToast('خطأ في التجديد', 'error');
|
|
1194
|
+
showToast('❌ خطأ في التجديد', 'error');
|
|
844
1195
|
}
|
|
845
1196
|
}
|
|
846
1197
|
|
|
@@ -854,25 +1205,30 @@ function hideModal(id) {
|
|
|
854
1205
|
}
|
|
855
1206
|
|
|
856
1207
|
function switchTab(tabId, tabElement) {
|
|
857
|
-
// إزالة active من جميع التبويبات
|
|
858
1208
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
859
1209
|
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
860
1210
|
|
|
861
|
-
// إضافة active للمحدد
|
|
862
1211
|
if (tabElement) tabElement.classList.add('active');
|
|
863
1212
|
document.getElementById(tabId).classList.add('active');
|
|
864
1213
|
}
|
|
865
1214
|
|
|
866
|
-
function showToast(message, type = '
|
|
867
|
-
const
|
|
868
|
-
toast
|
|
869
|
-
toast.className = `toast
|
|
870
|
-
|
|
1215
|
+
function showToast(message, type = 'info') {
|
|
1216
|
+
const container = document.getElementById('toastContainer');
|
|
1217
|
+
const toast = document.createElement('div');
|
|
1218
|
+
toast.className = `toast ${type}`;
|
|
1219
|
+
toast.innerHTML = `<span>${message}</span>`;
|
|
1220
|
+
container.appendChild(toast);
|
|
1221
|
+
|
|
1222
|
+
setTimeout(() => {
|
|
1223
|
+
toast.style.animation = 'slideInLeft 0.3s reverse';
|
|
1224
|
+
setTimeout(() => toast.remove(), 300);
|
|
1225
|
+
}, 3000);
|
|
871
1226
|
}
|
|
872
1227
|
|
|
873
1228
|
// Init
|
|
874
1229
|
window.onload = () => {
|
|
875
|
-
|
|
1230
|
+
connectWebSocket();
|
|
1231
|
+
|
|
876
1232
|
const token = localStorage.getItem('noho_token');
|
|
877
1233
|
if (token) {
|
|
878
1234
|
loadDashboard();
|
|
@@ -881,7 +1237,7 @@ window.onload = () => {
|
|
|
881
1237
|
|
|
882
1238
|
// Close modals on outside click
|
|
883
1239
|
window.onclick = (e) => {
|
|
884
|
-
if (e.target.classList.contains('modal')) {
|
|
1240
|
+
if (e.target.classList.contains('modal-overlay')) {
|
|
885
1241
|
e.target.classList.remove('active');
|
|
886
1242
|
}
|
|
887
1243
|
};
|