@package-uploader/ui 1.0.14 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-C2Iuqreu.js +71 -0
- package/dist/assets/index-CooIw87J.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/api/client.ts +129 -3
- package/src/components/CourseStructureStep.tsx +211 -0
- package/src/components/UploadModal.tsx +371 -148
- package/src/index.css +275 -1
- package/src/main.tsx +28 -1
- package/dist/assets/index-8HHKr2PX.css +0 -1
- package/dist/assets/index-Cur4iArP.js +0 -71
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { api } from '../api/client';
|
|
2
|
+
import { api, type CourseClassMappings } from '../api/client';
|
|
3
3
|
import DropZone from './DropZone';
|
|
4
|
+
import CourseStructureStep, { type CourseStructure } from './CourseStructureStep';
|
|
4
5
|
|
|
5
6
|
interface FileItem {
|
|
6
7
|
file: File;
|
|
@@ -10,6 +11,8 @@ interface FileItem {
|
|
|
10
11
|
error?: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
type Step = 'drop' | 'structure' | 'options';
|
|
15
|
+
|
|
13
16
|
interface UploadModalProps {
|
|
14
17
|
folderId: number;
|
|
15
18
|
folderName: string;
|
|
@@ -25,6 +28,12 @@ export default function UploadModal({
|
|
|
25
28
|
}: UploadModalProps) {
|
|
26
29
|
const [files, setFiles] = useState<FileItem[]>([]);
|
|
27
30
|
const [uploading, setUploading] = useState(false);
|
|
31
|
+
const [step, setStep] = useState<Step>('drop');
|
|
32
|
+
|
|
33
|
+
// Course structure (parsed from Rise ZIP)
|
|
34
|
+
const [courseStructure, setCourseStructure] = useState<CourseStructure | null>(null);
|
|
35
|
+
const [classMappings, setClassMappings] = useState<CourseClassMappings>({});
|
|
36
|
+
const [parsingStructure, setParsingStructure] = useState(false);
|
|
28
37
|
|
|
29
38
|
// LMS Thin Pack options
|
|
30
39
|
const [createThinPack, setCreateThinPack] = useState(true); // Default ON
|
|
@@ -34,21 +43,52 @@ export default function UploadModal({
|
|
|
34
43
|
const [createSharedLink, setCreateSharedLink] = useState(false); // Default OFF
|
|
35
44
|
const [sharedLinkName, setSharedLinkName] = useState('');
|
|
36
45
|
|
|
46
|
+
// Skin option (optional)
|
|
47
|
+
const [skinName, setSkinName] = useState('');
|
|
48
|
+
|
|
49
|
+
// Wrap & Download mode (skip CDS upload)
|
|
50
|
+
const [wrapOnly, setWrapOnly] = useState(false);
|
|
51
|
+
|
|
37
52
|
// Generate slug from name: replace spaces with underscores
|
|
38
53
|
function generateSlug(name: string): string {
|
|
39
54
|
return name.trim().replace(/\s+/g, '_');
|
|
40
55
|
}
|
|
41
56
|
|
|
42
|
-
function handleFilesAccepted(newFiles: File[]) {
|
|
57
|
+
async function handleFilesAccepted(newFiles: File[]) {
|
|
43
58
|
const fileItems: FileItem[] = newFiles.map((file) => ({
|
|
44
59
|
file,
|
|
45
60
|
status: 'pending',
|
|
46
61
|
}));
|
|
47
62
|
setFiles((prev) => [...prev, ...fileItems]);
|
|
63
|
+
|
|
64
|
+
// Auto-parse structure for single file uploads
|
|
65
|
+
const allFiles = [...files, ...fileItems];
|
|
66
|
+
if (allFiles.length === 1 || (files.length === 0 && newFiles.length === 1)) {
|
|
67
|
+
const targetFile = newFiles.length === 1 ? newFiles[0] : allFiles[0].file;
|
|
68
|
+
setParsingStructure(true);
|
|
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
|
+
}
|
|
48
80
|
}
|
|
49
81
|
|
|
50
82
|
function handleRemoveFile(index: number) {
|
|
51
|
-
setFiles((prev) =>
|
|
83
|
+
setFiles((prev) => {
|
|
84
|
+
const next = prev.filter((_, i) => i !== index);
|
|
85
|
+
// Reset structure if we removed the only file or now have multiple
|
|
86
|
+
if (next.length !== 1) {
|
|
87
|
+
setCourseStructure(null);
|
|
88
|
+
setClassMappings({});
|
|
89
|
+
}
|
|
90
|
+
return next;
|
|
91
|
+
});
|
|
52
92
|
}
|
|
53
93
|
|
|
54
94
|
function formatSize(bytes: number): string {
|
|
@@ -57,12 +97,26 @@ export default function UploadModal({
|
|
|
57
97
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
58
98
|
}
|
|
59
99
|
|
|
100
|
+
function handleNextFromDrop() {
|
|
101
|
+
if (courseStructure && files.length === 1) {
|
|
102
|
+
setStep('structure');
|
|
103
|
+
} else {
|
|
104
|
+
setStep('options');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
60
108
|
async function handleUpload() {
|
|
61
109
|
const pendingFiles = files.filter((f) => f.status === 'pending');
|
|
62
110
|
if (pendingFiles.length === 0) return;
|
|
63
111
|
|
|
64
112
|
setUploading(true);
|
|
65
113
|
|
|
114
|
+
// Build class mappings option (only if any assignments exist)
|
|
115
|
+
const hasClassAssignments =
|
|
116
|
+
classMappings.course ||
|
|
117
|
+
Object.keys(classMappings.lessons || {}).length > 0 ||
|
|
118
|
+
Object.keys(classMappings.blocks || {}).length > 0;
|
|
119
|
+
|
|
66
120
|
for (let i = 0; i < files.length; i++) {
|
|
67
121
|
if (files[i].status !== 'pending') continue;
|
|
68
122
|
|
|
@@ -72,56 +126,82 @@ export default function UploadModal({
|
|
|
72
126
|
);
|
|
73
127
|
|
|
74
128
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (createThinPack) {
|
|
81
|
-
const name = thinPackName.trim() || `${result.documentName} - LMS Thin Pack`;
|
|
82
|
-
try {
|
|
83
|
-
await api.createSharedLink(result.documentId, {
|
|
84
|
-
name,
|
|
85
|
-
token: generateSlug(name),
|
|
86
|
-
isPublic: true,
|
|
87
|
-
isForThinPackage: true,
|
|
88
|
-
});
|
|
89
|
-
} catch (err) {
|
|
90
|
-
console.error('Failed to create thin pack:', err);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
129
|
+
if (wrapOnly) {
|
|
130
|
+
// Wrap & Download mode: wrap with PA-Patcher and download, no CDS upload
|
|
131
|
+
const wrapOptions: { skin?: string; classMappings?: CourseClassMappings } = {};
|
|
132
|
+
if (skinName.trim()) wrapOptions.skin = skinName.trim();
|
|
133
|
+
if (hasClassAssignments) wrapOptions.classMappings = classMappings;
|
|
93
134
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
await api.createSharedLink(result.documentId, {
|
|
99
|
-
name,
|
|
100
|
-
token: generateSlug(name),
|
|
101
|
-
isPublic: false,
|
|
102
|
-
isForThinPackage: false,
|
|
103
|
-
});
|
|
104
|
-
} catch (err) {
|
|
105
|
-
console.error('Failed to create shared link:', err);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
135
|
+
await api.wrapAndDownload(
|
|
136
|
+
files[i].file,
|
|
137
|
+
Object.keys(wrapOptions).length > 0 ? wrapOptions : undefined
|
|
138
|
+
);
|
|
108
139
|
|
|
109
140
|
setFiles((prev) =>
|
|
110
141
|
prev.map((f, idx) =>
|
|
111
|
-
idx === i
|
|
112
|
-
? {
|
|
113
|
-
...f,
|
|
114
|
-
status: 'success',
|
|
115
|
-
documentId: result.documentId ?? undefined,
|
|
116
|
-
documentName: result.documentName,
|
|
117
|
-
}
|
|
118
|
-
: f
|
|
142
|
+
idx === i ? { ...f, status: 'success' } : f
|
|
119
143
|
)
|
|
120
144
|
);
|
|
121
|
-
|
|
122
|
-
onUploadComplete(result.documentId);
|
|
123
145
|
} else {
|
|
124
|
-
|
|
146
|
+
// Normal upload mode
|
|
147
|
+
const uploadOptions: { title?: string; skin?: string; classMappings?: CourseClassMappings } = {};
|
|
148
|
+
if (skinName.trim()) uploadOptions.skin = skinName.trim();
|
|
149
|
+
if (hasClassAssignments) uploadOptions.classMappings = classMappings;
|
|
150
|
+
|
|
151
|
+
const result = await api.uploadCourse(
|
|
152
|
+
files[i].file,
|
|
153
|
+
folderId,
|
|
154
|
+
Object.keys(uploadOptions).length > 0 ? uploadOptions : undefined
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (result.success && result.documentId) {
|
|
158
|
+
// Create LMS Thin Pack if enabled
|
|
159
|
+
if (createThinPack) {
|
|
160
|
+
const name = thinPackName.trim() || `${result.documentName} - LMS Thin Pack`;
|
|
161
|
+
try {
|
|
162
|
+
await api.createSharedLink(result.documentId, {
|
|
163
|
+
name,
|
|
164
|
+
token: generateSlug(name),
|
|
165
|
+
isPublic: true,
|
|
166
|
+
isForThinPackage: true,
|
|
167
|
+
});
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error('Failed to create thin pack:', err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Create Shared Link if enabled
|
|
174
|
+
if (createSharedLink) {
|
|
175
|
+
const name = sharedLinkName.trim() || `${result.documentName} - Private`;
|
|
176
|
+
try {
|
|
177
|
+
await api.createSharedLink(result.documentId, {
|
|
178
|
+
name,
|
|
179
|
+
token: generateSlug(name),
|
|
180
|
+
isPublic: false,
|
|
181
|
+
isForThinPackage: false,
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error('Failed to create shared link:', err);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
setFiles((prev) =>
|
|
189
|
+
prev.map((f, idx) =>
|
|
190
|
+
idx === i
|
|
191
|
+
? {
|
|
192
|
+
...f,
|
|
193
|
+
status: 'success',
|
|
194
|
+
documentId: result.documentId ?? undefined,
|
|
195
|
+
documentName: result.documentName,
|
|
196
|
+
}
|
|
197
|
+
: f
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
onUploadComplete(result.documentId);
|
|
202
|
+
} else {
|
|
203
|
+
throw new Error(result.errors?.join(', ') || 'Upload failed');
|
|
204
|
+
}
|
|
125
205
|
}
|
|
126
206
|
} catch (err) {
|
|
127
207
|
setFiles((prev) =>
|
|
@@ -130,7 +210,7 @@ export default function UploadModal({
|
|
|
130
210
|
? {
|
|
131
211
|
...f,
|
|
132
212
|
status: 'error',
|
|
133
|
-
error: err instanceof Error ? err.message : 'Upload failed',
|
|
213
|
+
error: err instanceof Error ? err.message : wrapOnly ? 'Wrap failed' : 'Upload failed',
|
|
134
214
|
}
|
|
135
215
|
: f
|
|
136
216
|
)
|
|
@@ -156,132 +236,275 @@ export default function UploadModal({
|
|
|
156
236
|
<div className="modal-overlay" onClick={onClose}>
|
|
157
237
|
<div className="modal upload-modal" onClick={(e) => e.stopPropagation()}>
|
|
158
238
|
<div className="modal-header">
|
|
159
|
-
<h3 className="modal-title">
|
|
239
|
+
<h3 className="modal-title">
|
|
240
|
+
{wrapOnly ? 'Wrap & Download' : `Upload to: ${folderName}`}
|
|
241
|
+
{step === 'structure' && ' — Customize'}
|
|
242
|
+
{step === 'options' && ' — Options'}
|
|
243
|
+
</h3>
|
|
160
244
|
<button className="modal-close" onClick={onClose}>
|
|
161
245
|
x
|
|
162
246
|
</button>
|
|
163
247
|
</div>
|
|
164
248
|
|
|
165
249
|
<div className="upload-modal-content">
|
|
166
|
-
{/*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
250
|
+
{/* Step 1: Drop Zone */}
|
|
251
|
+
{step === 'drop' && (
|
|
252
|
+
<>
|
|
253
|
+
<DropZone onFilesAccepted={handleFilesAccepted} disabled={uploading} />
|
|
254
|
+
|
|
255
|
+
{/* File List */}
|
|
256
|
+
{hasFiles && (
|
|
257
|
+
<div className="upload-file-list">
|
|
258
|
+
<h4>Files ({files.length})</h4>
|
|
259
|
+
{files.map((item, index) => (
|
|
260
|
+
<div key={index} className="upload-file-item">
|
|
261
|
+
<div className="upload-file-info">
|
|
262
|
+
<span className="upload-file-name">{item.file.name}</span>
|
|
263
|
+
<span className="upload-file-size">{formatSize(item.file.size)}</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div className="upload-file-status">
|
|
266
|
+
{item.status === 'pending' && (
|
|
267
|
+
<button
|
|
268
|
+
className="btn btn-sm btn-secondary"
|
|
269
|
+
onClick={() => handleRemoveFile(index)}
|
|
270
|
+
>
|
|
271
|
+
Remove
|
|
272
|
+
</button>
|
|
273
|
+
)}
|
|
274
|
+
{item.status === 'uploading' && (
|
|
275
|
+
<>
|
|
276
|
+
<div className="spinner" />
|
|
277
|
+
<span className="status-uploading">{wrapOnly ? 'Wrapping...' : 'Uploading...'}</span>
|
|
278
|
+
</>
|
|
279
|
+
)}
|
|
280
|
+
{item.status === 'success' && (
|
|
281
|
+
<span className="status-success">{wrapOnly ? 'Downloaded' : 'Uploaded'}</span>
|
|
282
|
+
)}
|
|
283
|
+
{item.status === 'error' && (
|
|
284
|
+
<span className="status-error" title={item.error}>
|
|
285
|
+
Failed
|
|
286
|
+
</span>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
))}
|
|
206
291
|
</div>
|
|
207
|
-
)
|
|
208
|
-
|
|
292
|
+
)}
|
|
293
|
+
|
|
294
|
+
{/* Parsing indicator */}
|
|
295
|
+
{parsingStructure && (
|
|
296
|
+
<div className="structure-parsing-indicator">
|
|
297
|
+
<div className="spinner" />
|
|
298
|
+
<span>Analyzing course structure...</span>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{/* Structure detected badge */}
|
|
303
|
+
{courseStructure && !parsingStructure && (
|
|
304
|
+
<div className="structure-detected-badge">
|
|
305
|
+
Rise course detected — {courseStructure.lessons.length} lessons,{' '}
|
|
306
|
+
{courseStructure.lessons.reduce((sum, l) => sum + l.blocks.length, 0)} blocks.
|
|
307
|
+
You can customize classes in the next step.
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
|
|
311
|
+
{/* Multi-file note */}
|
|
312
|
+
{files.length > 1 && (
|
|
313
|
+
<div className="structure-multi-note">
|
|
314
|
+
Course structure customization is available for single-file uploads only.
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
</>
|
|
209
318
|
)}
|
|
210
319
|
|
|
211
|
-
{/*
|
|
212
|
-
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
{createThinPack && (
|
|
227
|
-
<div className="upload-link-name-input">
|
|
320
|
+
{/* Step 2: Course Structure */}
|
|
321
|
+
{step === 'structure' && courseStructure && (
|
|
322
|
+
<CourseStructureStep
|
|
323
|
+
structure={courseStructure}
|
|
324
|
+
classMappings={classMappings}
|
|
325
|
+
onChange={setClassMappings}
|
|
326
|
+
/>
|
|
327
|
+
)}
|
|
328
|
+
|
|
329
|
+
{/* Step 3: Options */}
|
|
330
|
+
{step === 'options' && (
|
|
331
|
+
<div className="upload-link-options">
|
|
332
|
+
{/* Wrap & Download toggle */}
|
|
333
|
+
<div className="upload-link-option">
|
|
334
|
+
<label className="upload-link-toggle">
|
|
228
335
|
<input
|
|
229
|
-
type="
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
onChange={(e) => setThinPackName(e.target.value)}
|
|
336
|
+
type="checkbox"
|
|
337
|
+
checked={wrapOnly}
|
|
338
|
+
onChange={(e) => setWrapOnly(e.target.checked)}
|
|
233
339
|
disabled={uploading}
|
|
234
340
|
/>
|
|
235
|
-
<span
|
|
236
|
-
</
|
|
341
|
+
<span>Wrap & Download only (skip CDS upload)</span>
|
|
342
|
+
</label>
|
|
343
|
+
{wrapOnly && (
|
|
344
|
+
<span className="slug-preview">Wraps with PA-Patcher and downloads the ZIP directly</span>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
{/* CDS upload options (hidden in wrap-only mode) */}
|
|
349
|
+
{!wrapOnly && (
|
|
350
|
+
<>
|
|
351
|
+
<h4>After upload, create:</h4>
|
|
352
|
+
|
|
353
|
+
{/* LMS Thin Pack */}
|
|
354
|
+
<div className="upload-link-option">
|
|
355
|
+
<label className="upload-link-toggle">
|
|
356
|
+
<input
|
|
357
|
+
type="checkbox"
|
|
358
|
+
checked={createThinPack}
|
|
359
|
+
onChange={(e) => setCreateThinPack(e.target.checked)}
|
|
360
|
+
disabled={uploading}
|
|
361
|
+
/>
|
|
362
|
+
<span>LMS Thin Pack (public)</span>
|
|
363
|
+
</label>
|
|
364
|
+
{createThinPack && (
|
|
365
|
+
<div className="upload-link-name-input">
|
|
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>
|
|
377
|
+
|
|
378
|
+
{/* Shared Link */}
|
|
379
|
+
<div className="upload-link-option">
|
|
380
|
+
<label className="upload-link-toggle">
|
|
381
|
+
<input
|
|
382
|
+
type="checkbox"
|
|
383
|
+
checked={createSharedLink}
|
|
384
|
+
onChange={(e) => setCreateSharedLink(e.target.checked)}
|
|
385
|
+
disabled={uploading}
|
|
386
|
+
/>
|
|
387
|
+
<span>Shared Link (private/SSO)</span>
|
|
388
|
+
</label>
|
|
389
|
+
{createSharedLink && (
|
|
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
|
+
</>
|
|
237
403
|
)}
|
|
238
|
-
</div>
|
|
239
404
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
checked={createSharedLink}
|
|
246
|
-
onChange={(e) => setCreateSharedLink(e.target.checked)}
|
|
247
|
-
disabled={uploading}
|
|
248
|
-
/>
|
|
249
|
-
<span>Shared Link (private/SSO)</span>
|
|
250
|
-
</label>
|
|
251
|
-
{createSharedLink && (
|
|
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>
|
|
252
410
|
<div className="upload-link-name-input">
|
|
253
411
|
<input
|
|
254
412
|
type="text"
|
|
255
|
-
placeholder="
|
|
256
|
-
value={
|
|
257
|
-
onChange={(e) =>
|
|
413
|
+
placeholder="e.g. marketing"
|
|
414
|
+
value={skinName}
|
|
415
|
+
onChange={(e) => setSkinName(e.target.value)}
|
|
258
416
|
disabled={uploading}
|
|
259
417
|
/>
|
|
260
|
-
<span className="slug-preview">
|
|
418
|
+
<span className="slug-preview">CSS class for custom skin styling</span>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
{/* Class mappings summary (if any assigned) */}
|
|
423
|
+
{(classMappings.course ||
|
|
424
|
+
Object.keys(classMappings.lessons || {}).length > 0 ||
|
|
425
|
+
Object.keys(classMappings.blocks || {}).length > 0) && (
|
|
426
|
+
<div className="structure-summary-badge">
|
|
427
|
+
Class assignments:{' '}
|
|
428
|
+
{[
|
|
429
|
+
classMappings.course ? '1 course' : '',
|
|
430
|
+
Object.keys(classMappings.lessons || {}).length > 0
|
|
431
|
+
? `${Object.keys(classMappings.lessons || {}).length} lesson(s)`
|
|
432
|
+
: '',
|
|
433
|
+
Object.keys(classMappings.blocks || {}).length > 0
|
|
434
|
+
? `${Object.keys(classMappings.blocks || {}).length} block(s)`
|
|
435
|
+
: '',
|
|
436
|
+
]
|
|
437
|
+
.filter(Boolean)
|
|
438
|
+
.join(', ')}
|
|
261
439
|
</div>
|
|
262
440
|
)}
|
|
263
441
|
</div>
|
|
264
|
-
|
|
442
|
+
)}
|
|
265
443
|
</div>
|
|
266
444
|
|
|
267
445
|
<div className="modal-footer">
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
446
|
+
{step === 'drop' && (
|
|
447
|
+
<>
|
|
448
|
+
<button className="btn btn-secondary" onClick={onClose} disabled={uploading}>
|
|
449
|
+
Cancel
|
|
450
|
+
</button>
|
|
451
|
+
<button
|
|
452
|
+
className="btn btn-primary"
|
|
453
|
+
onClick={handleNextFromDrop}
|
|
454
|
+
disabled={uploading || pendingCount === 0 || parsingStructure}
|
|
455
|
+
>
|
|
456
|
+
Next
|
|
457
|
+
</button>
|
|
458
|
+
</>
|
|
459
|
+
)}
|
|
460
|
+
|
|
461
|
+
{step === 'structure' && (
|
|
462
|
+
<>
|
|
463
|
+
<button className="btn btn-secondary" onClick={() => setStep('drop')}>
|
|
464
|
+
Back
|
|
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
|
+
)}
|
|
285
508
|
</div>
|
|
286
509
|
</div>
|
|
287
510
|
</div>
|