@package-uploader/ui 1.1.0 → 1.1.2
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-95fEc7BA.js +71 -0
- package/dist/assets/{index-CooIw87J.css → index-CcXisJMx.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/api/client.ts +9 -0
- package/src/components/BlockGroupingPanel.tsx +264 -0
- package/src/components/CourseStructureStep.tsx +58 -2
- package/src/components/UploadModal.tsx +249 -281
- package/src/hooks/useBlockGrouping.ts +184 -0
- package/src/index.css +382 -0
- package/dist/assets/index-C2Iuqreu.js +0 -71
|
@@ -11,8 +11,6 @@ interface FileItem {
|
|
|
11
11
|
error?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
type Step = 'drop' | 'structure' | 'options';
|
|
15
|
-
|
|
16
14
|
interface UploadModalProps {
|
|
17
15
|
folderId: number;
|
|
18
16
|
folderName: string;
|
|
@@ -28,106 +26,115 @@ export default function UploadModal({
|
|
|
28
26
|
}: UploadModalProps) {
|
|
29
27
|
const [files, setFiles] = useState<FileItem[]>([]);
|
|
30
28
|
const [uploading, setUploading] = useState(false);
|
|
31
|
-
const [step, setStep] = useState<Step>('drop');
|
|
32
29
|
|
|
33
|
-
// Course structure
|
|
30
|
+
// Course structure — on-demand, collapsible
|
|
34
31
|
const [courseStructure, setCourseStructure] = useState<CourseStructure | null>(null);
|
|
35
32
|
const [classMappings, setClassMappings] = useState<CourseClassMappings>({});
|
|
36
33
|
const [parsingStructure, setParsingStructure] = useState(false);
|
|
34
|
+
const [structurePanelOpen, setStructurePanelOpen] = useState(false);
|
|
35
|
+
const [parseFailed, setParseFailed] = useState(false);
|
|
37
36
|
|
|
38
37
|
// LMS Thin Pack options
|
|
39
|
-
const [createThinPack, setCreateThinPack] = useState(true);
|
|
38
|
+
const [createThinPack, setCreateThinPack] = useState(true);
|
|
40
39
|
const [thinPackName, setThinPackName] = useState('');
|
|
41
40
|
|
|
42
41
|
// Shared Link options
|
|
43
|
-
const [createSharedLink, setCreateSharedLink] = useState(false);
|
|
42
|
+
const [createSharedLink, setCreateSharedLink] = useState(false);
|
|
44
43
|
const [sharedLinkName, setSharedLinkName] = useState('');
|
|
45
44
|
|
|
46
|
-
// Skin option
|
|
45
|
+
// Skin option
|
|
47
46
|
const [skinName, setSkinName] = useState('');
|
|
48
47
|
|
|
49
|
-
// Wrap & Download mode
|
|
48
|
+
// Wrap & Download mode
|
|
50
49
|
const [wrapOnly, setWrapOnly] = useState(false);
|
|
51
50
|
|
|
52
|
-
// Generate slug from name: replace spaces with underscores
|
|
53
51
|
function generateSlug(name: string): string {
|
|
54
52
|
return name.trim().replace(/\s+/g, '_');
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
|
|
55
|
+
function handleFilesAccepted(newFiles: File[]) {
|
|
58
56
|
const fileItems: FileItem[] = newFiles.map((file) => ({
|
|
59
57
|
file,
|
|
60
58
|
status: 'pending',
|
|
61
59
|
}));
|
|
62
60
|
setFiles((prev) => [...prev, ...fileItems]);
|
|
63
61
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const structure = await api.parseCourseStructure(targetFile);
|
|
71
|
-
setCourseStructure(structure);
|
|
72
|
-
} catch {
|
|
73
|
-
setCourseStructure(null);
|
|
74
|
-
}
|
|
75
|
-
setParsingStructure(false);
|
|
76
|
-
} else {
|
|
77
|
-
// Multi-file: no structure parsing
|
|
78
|
-
setCourseStructure(null);
|
|
79
|
-
}
|
|
62
|
+
// Reset structure state when files change
|
|
63
|
+
setCourseStructure(null);
|
|
64
|
+
setClassMappings({});
|
|
65
|
+
setStructurePanelOpen(false);
|
|
66
|
+
setParseFailed(false);
|
|
80
67
|
}
|
|
81
68
|
|
|
82
69
|
function handleRemoveFile(index: number) {
|
|
83
70
|
setFiles((prev) => {
|
|
84
71
|
const next = prev.filter((_, i) => i !== index);
|
|
85
|
-
// Reset structure if we removed the only file or now have multiple
|
|
86
72
|
if (next.length !== 1) {
|
|
87
73
|
setCourseStructure(null);
|
|
88
74
|
setClassMappings({});
|
|
75
|
+
setStructurePanelOpen(false);
|
|
76
|
+
setParseFailed(false);
|
|
89
77
|
}
|
|
90
78
|
return next;
|
|
91
79
|
});
|
|
92
80
|
}
|
|
93
81
|
|
|
82
|
+
async function handleCustomizeClick() {
|
|
83
|
+
// Already parsed — just toggle
|
|
84
|
+
if (courseStructure) {
|
|
85
|
+
setStructurePanelOpen((prev) => !prev);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Already failed — don't retry
|
|
90
|
+
if (parseFailed) return;
|
|
91
|
+
|
|
92
|
+
// Parse on first click
|
|
93
|
+
if (files.length !== 1) return;
|
|
94
|
+
|
|
95
|
+
setParsingStructure(true);
|
|
96
|
+
try {
|
|
97
|
+
const structure = await api.parseCourseStructure(files[0].file);
|
|
98
|
+
if (structure) {
|
|
99
|
+
setCourseStructure(structure);
|
|
100
|
+
setStructurePanelOpen(true);
|
|
101
|
+
setParseFailed(false);
|
|
102
|
+
} else {
|
|
103
|
+
setParseFailed(true);
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
setParseFailed(true);
|
|
107
|
+
}
|
|
108
|
+
setParsingStructure(false);
|
|
109
|
+
}
|
|
110
|
+
|
|
94
111
|
function formatSize(bytes: number): string {
|
|
95
112
|
if (bytes < 1024) return `${bytes} B`;
|
|
96
113
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
97
114
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
98
115
|
}
|
|
99
116
|
|
|
100
|
-
function handleNextFromDrop() {
|
|
101
|
-
if (courseStructure && files.length === 1) {
|
|
102
|
-
setStep('structure');
|
|
103
|
-
} else {
|
|
104
|
-
setStep('options');
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
117
|
async function handleUpload() {
|
|
109
118
|
const pendingFiles = files.filter((f) => f.status === 'pending');
|
|
110
119
|
if (pendingFiles.length === 0) return;
|
|
111
120
|
|
|
112
121
|
setUploading(true);
|
|
113
122
|
|
|
114
|
-
// Build class mappings option (only if any assignments exist)
|
|
115
123
|
const hasClassAssignments =
|
|
116
124
|
classMappings.course ||
|
|
117
125
|
Object.keys(classMappings.lessons || {}).length > 0 ||
|
|
118
|
-
Object.keys(classMappings.blocks || {}).length > 0
|
|
126
|
+
Object.keys(classMappings.blocks || {}).length > 0 ||
|
|
127
|
+
(classMappings.blockGroups && classMappings.blockGroups.length > 0);
|
|
119
128
|
|
|
120
129
|
for (let i = 0; i < files.length; i++) {
|
|
121
130
|
if (files[i].status !== 'pending') continue;
|
|
122
131
|
|
|
123
|
-
// Mark as uploading
|
|
124
132
|
setFiles((prev) =>
|
|
125
133
|
prev.map((f, idx) => (idx === i ? { ...f, status: 'uploading' } : f))
|
|
126
134
|
);
|
|
127
135
|
|
|
128
136
|
try {
|
|
129
137
|
if (wrapOnly) {
|
|
130
|
-
// Wrap & Download mode: wrap with PA-Patcher and download, no CDS upload
|
|
131
138
|
const wrapOptions: { skin?: string; classMappings?: CourseClassMappings } = {};
|
|
132
139
|
if (skinName.trim()) wrapOptions.skin = skinName.trim();
|
|
133
140
|
if (hasClassAssignments) wrapOptions.classMappings = classMappings;
|
|
@@ -143,7 +150,6 @@ export default function UploadModal({
|
|
|
143
150
|
)
|
|
144
151
|
);
|
|
145
152
|
} else {
|
|
146
|
-
// Normal upload mode
|
|
147
153
|
const uploadOptions: { title?: string; skin?: string; classMappings?: CourseClassMappings } = {};
|
|
148
154
|
if (skinName.trim()) uploadOptions.skin = skinName.trim();
|
|
149
155
|
if (hasClassAssignments) uploadOptions.classMappings = classMappings;
|
|
@@ -155,7 +161,6 @@ export default function UploadModal({
|
|
|
155
161
|
);
|
|
156
162
|
|
|
157
163
|
if (result.success && result.documentId) {
|
|
158
|
-
// Create LMS Thin Pack if enabled
|
|
159
164
|
if (createThinPack) {
|
|
160
165
|
const name = thinPackName.trim() || `${result.documentName} - LMS Thin Pack`;
|
|
161
166
|
try {
|
|
@@ -170,7 +175,6 @@ export default function UploadModal({
|
|
|
170
175
|
}
|
|
171
176
|
}
|
|
172
177
|
|
|
173
|
-
// Create Shared Link if enabled
|
|
174
178
|
if (createSharedLink) {
|
|
175
179
|
const name = sharedLinkName.trim() || `${result.documentName} - Private`;
|
|
176
180
|
try {
|
|
@@ -223,8 +227,8 @@ export default function UploadModal({
|
|
|
223
227
|
|
|
224
228
|
const pendingCount = files.filter((f) => f.status === 'pending').length;
|
|
225
229
|
const hasFiles = files.length > 0;
|
|
230
|
+
const isSingleFile = files.length === 1;
|
|
226
231
|
|
|
227
|
-
// Preview slugs
|
|
228
232
|
const thinPackSlugPreview = thinPackName.trim()
|
|
229
233
|
? generateSlug(thinPackName)
|
|
230
234
|
: '(will use document name)';
|
|
@@ -232,14 +236,29 @@ export default function UploadModal({
|
|
|
232
236
|
? generateSlug(sharedLinkName)
|
|
233
237
|
: '(will use document name)';
|
|
234
238
|
|
|
239
|
+
// Class assignment count for summary
|
|
240
|
+
const groupCount = (classMappings.blockGroups || []).length;
|
|
241
|
+
const assignmentCount =
|
|
242
|
+
(classMappings.course ? 1 : 0) +
|
|
243
|
+
Object.keys(classMappings.lessons || {}).length +
|
|
244
|
+
Object.keys(classMappings.blocks || {}).length +
|
|
245
|
+
groupCount;
|
|
246
|
+
|
|
247
|
+
// Customize button label
|
|
248
|
+
const customizeLabel = parsingStructure
|
|
249
|
+
? 'Analyzing...'
|
|
250
|
+
: parseFailed
|
|
251
|
+
? 'Not a Rise course'
|
|
252
|
+
: courseStructure
|
|
253
|
+
? `Customize Classes (${courseStructure.lessons.length} lessons)`
|
|
254
|
+
: 'Customize Classes';
|
|
255
|
+
|
|
235
256
|
return (
|
|
236
257
|
<div className="modal-overlay" onClick={onClose}>
|
|
237
258
|
<div className="modal upload-modal" onClick={(e) => e.stopPropagation()}>
|
|
238
259
|
<div className="modal-header">
|
|
239
260
|
<h3 className="modal-title">
|
|
240
261
|
{wrapOnly ? 'Wrap & Download' : `Upload to: ${folderName}`}
|
|
241
|
-
{step === 'structure' && ' — Customize'}
|
|
242
|
-
{step === 'options' && ' — Options'}
|
|
243
262
|
</h3>
|
|
244
263
|
<button className="modal-close" onClick={onClose}>
|
|
245
264
|
x
|
|
@@ -247,264 +266,213 @@ export default function UploadModal({
|
|
|
247
266
|
</div>
|
|
248
267
|
|
|
249
268
|
<div className="upload-modal-content">
|
|
250
|
-
{/*
|
|
251
|
-
{
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
)}
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
))}
|
|
291
|
-
</div>
|
|
292
|
-
)}
|
|
293
|
-
|
|
294
|
-
{/* Parsing indicator */}
|
|
295
|
-
{parsingStructure && (
|
|
296
|
-
<div className="structure-parsing-indicator">
|
|
297
|
-
<div className="spinner" />
|
|
298
|
-
<span>Analyzing course structure...</span>
|
|
269
|
+
{/* Dropzone */}
|
|
270
|
+
<DropZone onFilesAccepted={handleFilesAccepted} disabled={uploading} />
|
|
271
|
+
|
|
272
|
+
{/* File List */}
|
|
273
|
+
{hasFiles && (
|
|
274
|
+
<div className="upload-file-list">
|
|
275
|
+
<h4>Files ({files.length})</h4>
|
|
276
|
+
{files.map((item, index) => (
|
|
277
|
+
<div key={index} className="upload-file-item">
|
|
278
|
+
<div className="upload-file-info">
|
|
279
|
+
<span className="upload-file-name">{item.file.name}</span>
|
|
280
|
+
<span className="upload-file-size">{formatSize(item.file.size)}</span>
|
|
281
|
+
</div>
|
|
282
|
+
<div className="upload-file-status">
|
|
283
|
+
{item.status === 'pending' && (
|
|
284
|
+
<button
|
|
285
|
+
className="btn btn-sm btn-secondary"
|
|
286
|
+
onClick={() => handleRemoveFile(index)}
|
|
287
|
+
>
|
|
288
|
+
Remove
|
|
289
|
+
</button>
|
|
290
|
+
)}
|
|
291
|
+
{item.status === 'uploading' && (
|
|
292
|
+
<>
|
|
293
|
+
<div className="spinner" />
|
|
294
|
+
<span className="status-uploading">{wrapOnly ? 'Wrapping...' : 'Uploading...'}</span>
|
|
295
|
+
</>
|
|
296
|
+
)}
|
|
297
|
+
{item.status === 'success' && (
|
|
298
|
+
<span className="status-success">{wrapOnly ? 'Downloaded' : 'Uploaded'}</span>
|
|
299
|
+
)}
|
|
300
|
+
{item.status === 'error' && (
|
|
301
|
+
<span className="status-error" title={item.error}>
|
|
302
|
+
Failed
|
|
303
|
+
</span>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
299
306
|
</div>
|
|
300
|
-
)}
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
301
310
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
311
|
+
{/* Customize Classes — on-demand toggle */}
|
|
312
|
+
{isSingleFile && !uploading && (
|
|
313
|
+
<div className="structure-section">
|
|
314
|
+
<button
|
|
315
|
+
className={`structure-toggle-btn${structurePanelOpen ? ' open' : ''}${parseFailed ? ' disabled' : ''}`}
|
|
316
|
+
onClick={handleCustomizeClick}
|
|
317
|
+
disabled={parsingStructure || parseFailed}
|
|
318
|
+
>
|
|
319
|
+
{parsingStructure && <div className="spinner" />}
|
|
320
|
+
<span>{customizeLabel}</span>
|
|
321
|
+
{!parsingStructure && !parseFailed && (
|
|
322
|
+
<span className="structure-toggle-arrow">
|
|
323
|
+
{structurePanelOpen ? '\u25B2' : '\u25BC'}
|
|
324
|
+
</span>
|
|
325
|
+
)}
|
|
326
|
+
{assignmentCount > 0 && !structurePanelOpen && (
|
|
327
|
+
<span className="course-structure-badge">{assignmentCount} assigned</span>
|
|
328
|
+
)}
|
|
329
|
+
</button>
|
|
310
330
|
|
|
311
|
-
{
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
331
|
+
{structurePanelOpen && courseStructure && (
|
|
332
|
+
<div className="structure-panel">
|
|
333
|
+
<CourseStructureStep
|
|
334
|
+
structure={courseStructure}
|
|
335
|
+
classMappings={classMappings}
|
|
336
|
+
onChange={setClassMappings}
|
|
337
|
+
/>
|
|
315
338
|
</div>
|
|
316
339
|
)}
|
|
317
|
-
|
|
318
|
-
)}
|
|
319
|
-
|
|
320
|
-
{/* Step 2: Course Structure */}
|
|
321
|
-
{step === 'structure' && courseStructure && (
|
|
322
|
-
<CourseStructureStep
|
|
323
|
-
structure={courseStructure}
|
|
324
|
-
classMappings={classMappings}
|
|
325
|
-
onChange={setClassMappings}
|
|
326
|
-
/>
|
|
340
|
+
</div>
|
|
327
341
|
)}
|
|
328
342
|
|
|
329
|
-
{/*
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
<
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
</div>
|
|
347
|
-
|
|
348
|
-
{/* CDS upload options (hidden in wrap-only mode) */}
|
|
349
|
-
{!wrapOnly && (
|
|
350
|
-
<>
|
|
351
|
-
<h4>After upload, create:</h4>
|
|
343
|
+
{/* Options */}
|
|
344
|
+
<div className="upload-link-options">
|
|
345
|
+
{/* Wrap & Download toggle */}
|
|
346
|
+
<div className="upload-link-option">
|
|
347
|
+
<label className="upload-link-toggle">
|
|
348
|
+
<input
|
|
349
|
+
type="checkbox"
|
|
350
|
+
checked={wrapOnly}
|
|
351
|
+
onChange={(e) => setWrapOnly(e.target.checked)}
|
|
352
|
+
disabled={uploading}
|
|
353
|
+
/>
|
|
354
|
+
<span>Wrap & Download only (skip CDS upload)</span>
|
|
355
|
+
</label>
|
|
356
|
+
{wrapOnly && (
|
|
357
|
+
<span className="slug-preview">Wraps with PA-Patcher and downloads the ZIP directly</span>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
352
360
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
361
|
+
{/* CDS upload options */}
|
|
362
|
+
{!wrapOnly && (
|
|
363
|
+
<>
|
|
364
|
+
<h4>After upload, create:</h4>
|
|
365
|
+
|
|
366
|
+
<div className="upload-link-option">
|
|
367
|
+
<label className="upload-link-toggle">
|
|
368
|
+
<input
|
|
369
|
+
type="checkbox"
|
|
370
|
+
checked={createThinPack}
|
|
371
|
+
onChange={(e) => setCreateThinPack(e.target.checked)}
|
|
372
|
+
disabled={uploading}
|
|
373
|
+
/>
|
|
374
|
+
<span>LMS Thin Pack (public)</span>
|
|
375
|
+
</label>
|
|
376
|
+
{createThinPack && (
|
|
377
|
+
<div className="upload-link-name-input">
|
|
356
378
|
<input
|
|
357
|
-
type="
|
|
358
|
-
|
|
359
|
-
|
|
379
|
+
type="text"
|
|
380
|
+
placeholder="Link name (uses document name if empty)"
|
|
381
|
+
value={thinPackName}
|
|
382
|
+
onChange={(e) => setThinPackName(e.target.value)}
|
|
360
383
|
disabled={uploading}
|
|
361
384
|
/>
|
|
362
|
-
<span>
|
|
363
|
-
</
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
<input
|
|
367
|
-
type="text"
|
|
368
|
-
placeholder="Link name (uses document name if empty)"
|
|
369
|
-
value={thinPackName}
|
|
370
|
-
onChange={(e) => setThinPackName(e.target.value)}
|
|
371
|
-
disabled={uploading}
|
|
372
|
-
/>
|
|
373
|
-
<span className="slug-preview">Slug: {thinPackSlugPreview}</span>
|
|
374
|
-
</div>
|
|
375
|
-
)}
|
|
376
|
-
</div>
|
|
385
|
+
<span className="slug-preview">Slug: {thinPackSlugPreview}</span>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
377
389
|
|
|
378
|
-
|
|
379
|
-
<
|
|
380
|
-
<
|
|
390
|
+
<div className="upload-link-option">
|
|
391
|
+
<label className="upload-link-toggle">
|
|
392
|
+
<input
|
|
393
|
+
type="checkbox"
|
|
394
|
+
checked={createSharedLink}
|
|
395
|
+
onChange={(e) => setCreateSharedLink(e.target.checked)}
|
|
396
|
+
disabled={uploading}
|
|
397
|
+
/>
|
|
398
|
+
<span>Shared Link (private/SSO)</span>
|
|
399
|
+
</label>
|
|
400
|
+
{createSharedLink && (
|
|
401
|
+
<div className="upload-link-name-input">
|
|
381
402
|
<input
|
|
382
|
-
type="
|
|
383
|
-
|
|
384
|
-
|
|
403
|
+
type="text"
|
|
404
|
+
placeholder="Link name (uses document name if empty)"
|
|
405
|
+
value={sharedLinkName}
|
|
406
|
+
onChange={(e) => setSharedLinkName(e.target.value)}
|
|
385
407
|
disabled={uploading}
|
|
386
408
|
/>
|
|
387
|
-
<span>
|
|
388
|
-
</
|
|
389
|
-
|
|
390
|
-
<div className="upload-link-name-input">
|
|
391
|
-
<input
|
|
392
|
-
type="text"
|
|
393
|
-
placeholder="Link name (uses document name if empty)"
|
|
394
|
-
value={sharedLinkName}
|
|
395
|
-
onChange={(e) => setSharedLinkName(e.target.value)}
|
|
396
|
-
disabled={uploading}
|
|
397
|
-
/>
|
|
398
|
-
<span className="slug-preview">Slug: {sharedLinkSlugPreview}</span>
|
|
399
|
-
</div>
|
|
400
|
-
)}
|
|
401
|
-
</div>
|
|
402
|
-
</>
|
|
403
|
-
)}
|
|
404
|
-
|
|
405
|
-
{/* Skin Option */}
|
|
406
|
-
<div className="upload-link-option" style={{ marginTop: '12px' }}>
|
|
407
|
-
<label className="upload-link-toggle">
|
|
408
|
-
<span>Skin (optional)</span>
|
|
409
|
-
</label>
|
|
410
|
-
<div className="upload-link-name-input">
|
|
411
|
-
<input
|
|
412
|
-
type="text"
|
|
413
|
-
placeholder="e.g. marketing"
|
|
414
|
-
value={skinName}
|
|
415
|
-
onChange={(e) => setSkinName(e.target.value)}
|
|
416
|
-
disabled={uploading}
|
|
417
|
-
/>
|
|
418
|
-
<span className="slug-preview">CSS class for custom skin styling</span>
|
|
409
|
+
<span className="slug-preview">Slug: {sharedLinkSlugPreview}</span>
|
|
410
|
+
</div>
|
|
411
|
+
)}
|
|
419
412
|
</div>
|
|
413
|
+
</>
|
|
414
|
+
)}
|
|
415
|
+
|
|
416
|
+
{/* Skin */}
|
|
417
|
+
<div className="upload-link-option" style={{ marginTop: '12px' }}>
|
|
418
|
+
<label className="upload-link-toggle">
|
|
419
|
+
<span>Skin (optional)</span>
|
|
420
|
+
</label>
|
|
421
|
+
<div className="upload-link-name-input">
|
|
422
|
+
<input
|
|
423
|
+
type="text"
|
|
424
|
+
placeholder="e.g. marketing"
|
|
425
|
+
value={skinName}
|
|
426
|
+
onChange={(e) => setSkinName(e.target.value)}
|
|
427
|
+
disabled={uploading}
|
|
428
|
+
/>
|
|
429
|
+
<span className="slug-preview">CSS class for custom skin styling</span>
|
|
420
430
|
</div>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
{/* Class assignments summary */}
|
|
435
|
+
{assignmentCount > 0 && !structurePanelOpen && (
|
|
436
|
+
<div className="structure-summary-badge">
|
|
437
|
+
Class assignments:{' '}
|
|
438
|
+
{[
|
|
439
|
+
classMappings.course ? '1 course' : '',
|
|
440
|
+
Object.keys(classMappings.lessons || {}).length > 0
|
|
441
|
+
? `${Object.keys(classMappings.lessons || {}).length} lesson(s)`
|
|
442
|
+
: '',
|
|
443
|
+
Object.keys(classMappings.blocks || {}).length > 0
|
|
444
|
+
? `${Object.keys(classMappings.blocks || {}).length} block(s)`
|
|
445
|
+
: '',
|
|
446
|
+
groupCount > 0
|
|
447
|
+
? `${groupCount} group(s)`
|
|
448
|
+
: '',
|
|
449
|
+
]
|
|
450
|
+
.filter(Boolean)
|
|
451
|
+
.join(', ')}
|
|
441
452
|
</div>
|
|
442
453
|
)}
|
|
443
454
|
</div>
|
|
444
455
|
|
|
445
456
|
<div className="modal-footer">
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
</button>
|
|
466
|
-
<button
|
|
467
|
-
className="btn btn-secondary"
|
|
468
|
-
onClick={() => {
|
|
469
|
-
setClassMappings({});
|
|
470
|
-
setStep('options');
|
|
471
|
-
}}
|
|
472
|
-
>
|
|
473
|
-
Skip
|
|
474
|
-
</button>
|
|
475
|
-
<button className="btn btn-primary" onClick={() => setStep('options')}>
|
|
476
|
-
Next
|
|
477
|
-
</button>
|
|
478
|
-
</>
|
|
479
|
-
)}
|
|
480
|
-
|
|
481
|
-
{step === 'options' && (
|
|
482
|
-
<>
|
|
483
|
-
<button
|
|
484
|
-
className="btn btn-secondary"
|
|
485
|
-
onClick={() => setStep(courseStructure && files.length === 1 ? 'structure' : 'drop')}
|
|
486
|
-
disabled={uploading}
|
|
487
|
-
>
|
|
488
|
-
Back
|
|
489
|
-
</button>
|
|
490
|
-
<button
|
|
491
|
-
className="btn btn-primary"
|
|
492
|
-
onClick={handleUpload}
|
|
493
|
-
disabled={uploading || pendingCount === 0}
|
|
494
|
-
>
|
|
495
|
-
{uploading ? (
|
|
496
|
-
<>
|
|
497
|
-
<div className="spinner" />
|
|
498
|
-
{wrapOnly ? 'Wrapping...' : 'Uploading...'}
|
|
499
|
-
</>
|
|
500
|
-
) : wrapOnly ? (
|
|
501
|
-
`Wrap & Download ${pendingCount} file${pendingCount !== 1 ? 's' : ''}`
|
|
502
|
-
) : (
|
|
503
|
-
`Upload ${pendingCount} file${pendingCount !== 1 ? 's' : ''}`
|
|
504
|
-
)}
|
|
505
|
-
</button>
|
|
506
|
-
</>
|
|
507
|
-
)}
|
|
457
|
+
<button className="btn btn-secondary" onClick={onClose} disabled={uploading}>
|
|
458
|
+
Cancel
|
|
459
|
+
</button>
|
|
460
|
+
<button
|
|
461
|
+
className="btn btn-primary"
|
|
462
|
+
onClick={handleUpload}
|
|
463
|
+
disabled={uploading || pendingCount === 0}
|
|
464
|
+
>
|
|
465
|
+
{uploading ? (
|
|
466
|
+
<>
|
|
467
|
+
<div className="spinner" />
|
|
468
|
+
{wrapOnly ? 'Wrapping...' : 'Uploading...'}
|
|
469
|
+
</>
|
|
470
|
+
) : wrapOnly ? (
|
|
471
|
+
`Wrap & Download ${pendingCount} file${pendingCount !== 1 ? 's' : ''}`
|
|
472
|
+
) : (
|
|
473
|
+
`Upload ${pendingCount} file${pendingCount !== 1 ? 's' : ''}`
|
|
474
|
+
)}
|
|
475
|
+
</button>
|
|
508
476
|
</div>
|
|
509
477
|
</div>
|
|
510
478
|
</div>
|