@package-uploader/ui 1.1.2 → 1.1.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/dist/assets/index-CAZIUAbb.js +71 -0
- package/dist/assets/{index-CcXisJMx.css → index-DHoRoGws.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/api/client.ts +10 -0
- package/src/components/CourseStructureStep.tsx +315 -140
- package/src/components/UploadModal.tsx +253 -34
- package/src/index.css +207 -95
- package/dist/assets/index-95fEc7BA.js +0 -71
- package/src/components/BlockGroupingPanel.tsx +0 -264
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
2
|
import { api, type CourseClassMappings } from '../api/client';
|
|
3
3
|
import DropZone from './DropZone';
|
|
4
4
|
import CourseStructureStep, { type CourseStructure } from './CourseStructureStep';
|
|
@@ -31,9 +31,17 @@ export default function UploadModal({
|
|
|
31
31
|
const [courseStructure, setCourseStructure] = useState<CourseStructure | null>(null);
|
|
32
32
|
const [classMappings, setClassMappings] = useState<CourseClassMappings>({});
|
|
33
33
|
const [parsingStructure, setParsingStructure] = useState(false);
|
|
34
|
-
const [structurePanelOpen, setStructurePanelOpen] = useState(false);
|
|
35
34
|
const [parseFailed, setParseFailed] = useState(false);
|
|
36
35
|
|
|
36
|
+
// Preview state
|
|
37
|
+
const [previewToken, setPreviewToken] = useState<string | null>(null);
|
|
38
|
+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
39
|
+
const [previewLoading, setPreviewLoading] = useState(false);
|
|
40
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
41
|
+
|
|
42
|
+
// Options panel (collapsed by default in preview mode)
|
|
43
|
+
const [optionsOpen, setOptionsOpen] = useState(false);
|
|
44
|
+
|
|
37
45
|
// LMS Thin Pack options
|
|
38
46
|
const [createThinPack, setCreateThinPack] = useState(true);
|
|
39
47
|
const [thinPackName, setThinPackName] = useState('');
|
|
@@ -48,6 +56,35 @@ export default function UploadModal({
|
|
|
48
56
|
// Wrap & Download mode
|
|
49
57
|
const [wrapOnly, setWrapOnly] = useState(false);
|
|
50
58
|
|
|
59
|
+
const isPreviewMode = !!previewToken && !!previewUrl;
|
|
60
|
+
|
|
61
|
+
// Sync classMappings to iframe via postMessage
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!isPreviewMode || !iframeRef.current?.contentWindow) return;
|
|
64
|
+
iframeRef.current.contentWindow.postMessage(
|
|
65
|
+
{ type: 'pa-class-mappings', classMappings },
|
|
66
|
+
'*'
|
|
67
|
+
);
|
|
68
|
+
}, [classMappings, isPreviewMode]);
|
|
69
|
+
|
|
70
|
+
// Navigate iframe to a lesson when user clicks lesson in tree
|
|
71
|
+
const handleNavigateLesson = useCallback((lessonId: string) => {
|
|
72
|
+
if (!iframeRef.current?.contentWindow) return;
|
|
73
|
+
iframeRef.current.contentWindow.postMessage(
|
|
74
|
+
{ type: 'pa-navigate', lessonId },
|
|
75
|
+
'*'
|
|
76
|
+
);
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
// Cleanup preview on unmount
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
return () => {
|
|
82
|
+
if (previewToken) {
|
|
83
|
+
api.stopPreview(previewToken).catch(() => {});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}, [previewToken]);
|
|
87
|
+
|
|
51
88
|
function generateSlug(name: string): string {
|
|
52
89
|
return name.trim().replace(/\s+/g, '_');
|
|
53
90
|
}
|
|
@@ -59,11 +96,15 @@ export default function UploadModal({
|
|
|
59
96
|
}));
|
|
60
97
|
setFiles((prev) => [...prev, ...fileItems]);
|
|
61
98
|
|
|
62
|
-
// Reset structure state when files change
|
|
99
|
+
// Reset structure + preview state when files change
|
|
63
100
|
setCourseStructure(null);
|
|
64
101
|
setClassMappings({});
|
|
65
|
-
setStructurePanelOpen(false);
|
|
66
102
|
setParseFailed(false);
|
|
103
|
+
if (previewToken) {
|
|
104
|
+
api.stopPreview(previewToken).catch(() => {});
|
|
105
|
+
setPreviewToken(null);
|
|
106
|
+
setPreviewUrl(null);
|
|
107
|
+
}
|
|
67
108
|
}
|
|
68
109
|
|
|
69
110
|
function handleRemoveFile(index: number) {
|
|
@@ -72,17 +113,25 @@ export default function UploadModal({
|
|
|
72
113
|
if (next.length !== 1) {
|
|
73
114
|
setCourseStructure(null);
|
|
74
115
|
setClassMappings({});
|
|
75
|
-
setStructurePanelOpen(false);
|
|
76
116
|
setParseFailed(false);
|
|
117
|
+
if (previewToken) {
|
|
118
|
+
api.stopPreview(previewToken).catch(() => {});
|
|
119
|
+
setPreviewToken(null);
|
|
120
|
+
setPreviewUrl(null);
|
|
121
|
+
}
|
|
77
122
|
}
|
|
78
123
|
return next;
|
|
79
124
|
});
|
|
80
125
|
}
|
|
81
126
|
|
|
82
127
|
async function handleCustomizeClick() {
|
|
83
|
-
// Already
|
|
84
|
-
if (
|
|
85
|
-
|
|
128
|
+
// Already in preview mode — toggle back to normal
|
|
129
|
+
if (isPreviewMode) {
|
|
130
|
+
if (previewToken) {
|
|
131
|
+
api.stopPreview(previewToken).catch(() => {});
|
|
132
|
+
}
|
|
133
|
+
setPreviewToken(null);
|
|
134
|
+
setPreviewUrl(null);
|
|
86
135
|
return;
|
|
87
136
|
}
|
|
88
137
|
|
|
@@ -93,19 +142,53 @@ export default function UploadModal({
|
|
|
93
142
|
if (files.length !== 1) return;
|
|
94
143
|
|
|
95
144
|
setParsingStructure(true);
|
|
145
|
+
setPreviewLoading(true);
|
|
146
|
+
|
|
96
147
|
try {
|
|
97
|
-
|
|
148
|
+
// Parse structure and start preview in parallel
|
|
149
|
+
const [structure, preview] = await Promise.all([
|
|
150
|
+
courseStructure
|
|
151
|
+
? Promise.resolve(courseStructure)
|
|
152
|
+
: api.parseCourseStructure(files[0].file),
|
|
153
|
+
api.startPreview(files[0].file),
|
|
154
|
+
]);
|
|
155
|
+
|
|
98
156
|
if (structure) {
|
|
99
157
|
setCourseStructure(structure);
|
|
100
|
-
setStructurePanelOpen(true);
|
|
101
158
|
setParseFailed(false);
|
|
159
|
+
setPreviewToken(preview.token);
|
|
160
|
+
setPreviewUrl(preview.url);
|
|
102
161
|
} else {
|
|
103
162
|
setParseFailed(true);
|
|
163
|
+
// Cleanup the preview if structure parse failed
|
|
164
|
+
api.stopPreview(preview.token).catch(() => {});
|
|
104
165
|
}
|
|
105
166
|
} catch {
|
|
106
167
|
setParseFailed(true);
|
|
107
168
|
}
|
|
169
|
+
|
|
108
170
|
setParsingStructure(false);
|
|
171
|
+
setPreviewLoading(false);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function handleClose() {
|
|
175
|
+
if (previewToken) {
|
|
176
|
+
api.stopPreview(previewToken).catch(() => {});
|
|
177
|
+
}
|
|
178
|
+
onClose();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// When iframe loads, send current classMappings
|
|
182
|
+
function handleIframeLoad() {
|
|
183
|
+
// Small delay to let bridge script initialize
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
if (iframeRef.current?.contentWindow) {
|
|
186
|
+
iframeRef.current.contentWindow.postMessage(
|
|
187
|
+
{ type: 'pa-class-mappings', classMappings },
|
|
188
|
+
'*'
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}, 500);
|
|
109
192
|
}
|
|
110
193
|
|
|
111
194
|
function formatSize(bytes: number): string {
|
|
@@ -249,18 +332,169 @@ export default function UploadModal({
|
|
|
249
332
|
? 'Analyzing...'
|
|
250
333
|
: parseFailed
|
|
251
334
|
? 'Not a Rise course'
|
|
252
|
-
:
|
|
253
|
-
?
|
|
335
|
+
: isPreviewMode
|
|
336
|
+
? 'Close Preview'
|
|
254
337
|
: 'Customize Classes';
|
|
255
338
|
|
|
339
|
+
// Build the preview URL — needs the API base path prepended
|
|
340
|
+
const fullPreviewUrl = previewUrl
|
|
341
|
+
? (() => {
|
|
342
|
+
// Detect API base same way client.ts does
|
|
343
|
+
const scripts = document.querySelectorAll('script[src*="assets/index"]');
|
|
344
|
+
if (scripts.length > 0) {
|
|
345
|
+
const src = (scripts[0] as HTMLScriptElement).src;
|
|
346
|
+
const url = new URL(src);
|
|
347
|
+
const assetsIndex = url.pathname.indexOf('/assets/');
|
|
348
|
+
if (assetsIndex > 0) {
|
|
349
|
+
return url.pathname.substring(0, assetsIndex) + '/' + previewUrl.replace(/^\//, '');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return previewUrl;
|
|
353
|
+
})()
|
|
354
|
+
: null;
|
|
355
|
+
|
|
356
|
+
// --- Preview mode: full-screen split layout ---
|
|
357
|
+
if (isPreviewMode && courseStructure) {
|
|
358
|
+
return (
|
|
359
|
+
<div className="modal-overlay" onClick={handleClose}>
|
|
360
|
+
<div className="modal upload-modal preview-mode" onClick={(e) => e.stopPropagation()}>
|
|
361
|
+
<div className="modal-header">
|
|
362
|
+
<h3 className="modal-title">
|
|
363
|
+
Customize Classes — {courseStructure.courseTitle}
|
|
364
|
+
</h3>
|
|
365
|
+
{assignmentCount > 0 && (
|
|
366
|
+
<span className="course-structure-badge">
|
|
367
|
+
{assignmentCount} class{assignmentCount !== 1 ? 'es' : ''} assigned
|
|
368
|
+
</span>
|
|
369
|
+
)}
|
|
370
|
+
<button className="modal-close" onClick={handleClose}>
|
|
371
|
+
x
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div className="preview-split">
|
|
376
|
+
{/* Left: Course structure controls */}
|
|
377
|
+
<div className="preview-split-left">
|
|
378
|
+
<CourseStructureStep
|
|
379
|
+
structure={courseStructure}
|
|
380
|
+
classMappings={classMappings}
|
|
381
|
+
onChange={setClassMappings}
|
|
382
|
+
onNavigateLesson={handleNavigateLesson}
|
|
383
|
+
/>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
{/* Right: Live SCORM preview */}
|
|
387
|
+
<div className="preview-split-right">
|
|
388
|
+
{previewLoading ? (
|
|
389
|
+
<div className="preview-loading">
|
|
390
|
+
<div className="spinner" />
|
|
391
|
+
<span>Loading preview...</span>
|
|
392
|
+
</div>
|
|
393
|
+
) : (
|
|
394
|
+
<iframe
|
|
395
|
+
ref={iframeRef}
|
|
396
|
+
src={fullPreviewUrl || ''}
|
|
397
|
+
className="preview-iframe"
|
|
398
|
+
title="Course Preview"
|
|
399
|
+
onLoad={handleIframeLoad}
|
|
400
|
+
sandbox="allow-scripts allow-same-origin allow-popups"
|
|
401
|
+
/>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<div className="modal-footer preview-footer">
|
|
407
|
+
<button className="btn btn-secondary" onClick={handleClose} disabled={uploading}>
|
|
408
|
+
Cancel
|
|
409
|
+
</button>
|
|
410
|
+
|
|
411
|
+
<button
|
|
412
|
+
className={`btn btn-sm btn-secondary preview-options-toggle${optionsOpen ? ' open' : ''}`}
|
|
413
|
+
onClick={() => setOptionsOpen((prev) => !prev)}
|
|
414
|
+
>
|
|
415
|
+
Options {optionsOpen ? '\u25B2' : '\u25BC'}
|
|
416
|
+
</button>
|
|
417
|
+
|
|
418
|
+
{optionsOpen && (
|
|
419
|
+
<div className="preview-options-panel">
|
|
420
|
+
<label className="upload-link-toggle">
|
|
421
|
+
<input
|
|
422
|
+
type="checkbox"
|
|
423
|
+
checked={wrapOnly}
|
|
424
|
+
onChange={(e) => setWrapOnly(e.target.checked)}
|
|
425
|
+
disabled={uploading}
|
|
426
|
+
/>
|
|
427
|
+
<span>Wrap & Download only</span>
|
|
428
|
+
</label>
|
|
429
|
+
|
|
430
|
+
{!wrapOnly && (
|
|
431
|
+
<>
|
|
432
|
+
<label className="upload-link-toggle">
|
|
433
|
+
<input
|
|
434
|
+
type="checkbox"
|
|
435
|
+
checked={createThinPack}
|
|
436
|
+
onChange={(e) => setCreateThinPack(e.target.checked)}
|
|
437
|
+
disabled={uploading}
|
|
438
|
+
/>
|
|
439
|
+
<span>LMS Thin Pack</span>
|
|
440
|
+
</label>
|
|
441
|
+
<label className="upload-link-toggle">
|
|
442
|
+
<input
|
|
443
|
+
type="checkbox"
|
|
444
|
+
checked={createSharedLink}
|
|
445
|
+
onChange={(e) => setCreateSharedLink(e.target.checked)}
|
|
446
|
+
disabled={uploading}
|
|
447
|
+
/>
|
|
448
|
+
<span>Shared Link</span>
|
|
449
|
+
</label>
|
|
450
|
+
</>
|
|
451
|
+
)}
|
|
452
|
+
|
|
453
|
+
<div className="preview-option-skin">
|
|
454
|
+
<span className="preview-option-label">Skin:</span>
|
|
455
|
+
<input
|
|
456
|
+
type="text"
|
|
457
|
+
placeholder="e.g. marketing"
|
|
458
|
+
value={skinName}
|
|
459
|
+
onChange={(e) => setSkinName(e.target.value)}
|
|
460
|
+
disabled={uploading}
|
|
461
|
+
className="preview-option-input"
|
|
462
|
+
/>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
|
|
467
|
+
<button
|
|
468
|
+
className="btn btn-primary"
|
|
469
|
+
onClick={handleUpload}
|
|
470
|
+
disabled={uploading || pendingCount === 0}
|
|
471
|
+
>
|
|
472
|
+
{uploading ? (
|
|
473
|
+
<>
|
|
474
|
+
<div className="spinner" />
|
|
475
|
+
{wrapOnly ? 'Wrapping...' : 'Uploading...'}
|
|
476
|
+
</>
|
|
477
|
+
) : wrapOnly ? (
|
|
478
|
+
'Wrap & Download'
|
|
479
|
+
) : (
|
|
480
|
+
'Upload'
|
|
481
|
+
)}
|
|
482
|
+
</button>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// --- Normal mode: standard upload modal ---
|
|
256
490
|
return (
|
|
257
|
-
<div className="modal-overlay" onClick={
|
|
491
|
+
<div className="modal-overlay" onClick={handleClose}>
|
|
258
492
|
<div className="modal upload-modal" onClick={(e) => e.stopPropagation()}>
|
|
259
493
|
<div className="modal-header">
|
|
260
494
|
<h3 className="modal-title">
|
|
261
495
|
{wrapOnly ? 'Wrap & Download' : `Upload to: ${folderName}`}
|
|
262
496
|
</h3>
|
|
263
|
-
<button className="modal-close" onClick={
|
|
497
|
+
<button className="modal-close" onClick={handleClose}>
|
|
264
498
|
x
|
|
265
499
|
</button>
|
|
266
500
|
</div>
|
|
@@ -308,35 +542,20 @@ export default function UploadModal({
|
|
|
308
542
|
</div>
|
|
309
543
|
)}
|
|
310
544
|
|
|
311
|
-
{/* Customize Classes
|
|
545
|
+
{/* Customize Classes button */}
|
|
312
546
|
{isSingleFile && !uploading && (
|
|
313
547
|
<div className="structure-section">
|
|
314
548
|
<button
|
|
315
|
-
className={`structure-toggle-btn${
|
|
549
|
+
className={`structure-toggle-btn${parseFailed ? ' disabled' : ''}`}
|
|
316
550
|
onClick={handleCustomizeClick}
|
|
317
551
|
disabled={parsingStructure || parseFailed}
|
|
318
552
|
>
|
|
319
553
|
{parsingStructure && <div className="spinner" />}
|
|
320
554
|
<span>{customizeLabel}</span>
|
|
321
|
-
{
|
|
322
|
-
<span className="structure-toggle-arrow">
|
|
323
|
-
{structurePanelOpen ? '\u25B2' : '\u25BC'}
|
|
324
|
-
</span>
|
|
325
|
-
)}
|
|
326
|
-
{assignmentCount > 0 && !structurePanelOpen && (
|
|
555
|
+
{assignmentCount > 0 && (
|
|
327
556
|
<span className="course-structure-badge">{assignmentCount} assigned</span>
|
|
328
557
|
)}
|
|
329
558
|
</button>
|
|
330
|
-
|
|
331
|
-
{structurePanelOpen && courseStructure && (
|
|
332
|
-
<div className="structure-panel">
|
|
333
|
-
<CourseStructureStep
|
|
334
|
-
structure={courseStructure}
|
|
335
|
-
classMappings={classMappings}
|
|
336
|
-
onChange={setClassMappings}
|
|
337
|
-
/>
|
|
338
|
-
</div>
|
|
339
|
-
)}
|
|
340
559
|
</div>
|
|
341
560
|
)}
|
|
342
561
|
|
|
@@ -432,7 +651,7 @@ export default function UploadModal({
|
|
|
432
651
|
</div>
|
|
433
652
|
|
|
434
653
|
{/* Class assignments summary */}
|
|
435
|
-
{assignmentCount > 0 &&
|
|
654
|
+
{assignmentCount > 0 && (
|
|
436
655
|
<div className="structure-summary-badge">
|
|
437
656
|
Class assignments:{' '}
|
|
438
657
|
{[
|
|
@@ -454,7 +673,7 @@ export default function UploadModal({
|
|
|
454
673
|
</div>
|
|
455
674
|
|
|
456
675
|
<div className="modal-footer">
|
|
457
|
-
<button className="btn btn-secondary" onClick={
|
|
676
|
+
<button className="btn btn-secondary" onClick={handleClose} disabled={uploading}>
|
|
458
677
|
Cancel
|
|
459
678
|
</button>
|
|
460
679
|
<button
|