@package-uploader/ui 1.0.14 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-C19M6liw.css +1 -0
- package/dist/assets/index-Ca5beg0c.js +71 -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 +286 -101
- package/src/index.css +352 -1
- package/src/main.tsx +28 -1
- package/dist/assets/index-8HHKr2PX.css +0 -1
- package/dist/assets/index-Cur4iArP.js +0 -71
package/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Package Uploader</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Ca5beg0c.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C19M6liw.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/api/client.ts
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
// API client for communicating with the backend
|
|
2
|
-
//
|
|
3
|
-
|
|
2
|
+
// Detects API URL at runtime based on where the app is mounted
|
|
3
|
+
|
|
4
|
+
function detectApiBase(): string {
|
|
5
|
+
// If explicitly set via env var at build time, use that
|
|
6
|
+
if (import.meta.env.VITE_API_URL) {
|
|
7
|
+
return import.meta.env.VITE_API_URL;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Otherwise detect from the script location or current path
|
|
11
|
+
const scripts = document.querySelectorAll('script[src*="assets/index"]');
|
|
12
|
+
if (scripts.length > 0) {
|
|
13
|
+
const src = (scripts[0] as HTMLScriptElement).src;
|
|
14
|
+
const url = new URL(src);
|
|
15
|
+
const assetsIndex = url.pathname.indexOf('/assets/');
|
|
16
|
+
if (assetsIndex > 0) {
|
|
17
|
+
return url.pathname.substring(0, assetsIndex) + '/api';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Fallback: detect from current path
|
|
22
|
+
const path = window.location.pathname;
|
|
23
|
+
if (path.endsWith('/')) {
|
|
24
|
+
return path.slice(0, -1) + '/api';
|
|
25
|
+
}
|
|
26
|
+
const lastSlash = path.lastIndexOf('/');
|
|
27
|
+
return (lastSlash > 0 ? path.substring(0, lastSlash) : '') + '/api';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const API_BASE = detectApiBase();
|
|
4
31
|
|
|
5
32
|
export interface Folder {
|
|
6
33
|
id: number;
|
|
@@ -93,6 +120,12 @@ export interface AppConfig {
|
|
|
93
120
|
user: UserProfile | null;
|
|
94
121
|
}
|
|
95
122
|
|
|
123
|
+
export interface CourseClassMappings {
|
|
124
|
+
course?: string;
|
|
125
|
+
lessons?: Record<string, string>;
|
|
126
|
+
blocks?: Record<string, string>;
|
|
127
|
+
}
|
|
128
|
+
|
|
96
129
|
async function request<T>(
|
|
97
130
|
method: string,
|
|
98
131
|
path: string,
|
|
@@ -174,15 +207,72 @@ export const api = {
|
|
|
174
207
|
getDocumentVersions: (documentId: number) =>
|
|
175
208
|
request<DocumentVersion[]>('GET', `/documents/${documentId}/versions`),
|
|
176
209
|
|
|
210
|
+
// Parse course structure (for customization step)
|
|
211
|
+
parseCourseStructure: async (file: File) => {
|
|
212
|
+
const formData = new FormData();
|
|
213
|
+
formData.append('file', file);
|
|
214
|
+
try {
|
|
215
|
+
const result = await request<any>('POST', '/parse-structure', formData);
|
|
216
|
+
if (result.error || result.isRise === false) return null;
|
|
217
|
+
return result;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
|
|
177
223
|
// Upload
|
|
178
|
-
uploadCourse: (file: File, folderId: number, options?: { title?: string }) => {
|
|
224
|
+
uploadCourse: (file: File, folderId: number, options?: { title?: string; skin?: string; classMappings?: CourseClassMappings }) => {
|
|
179
225
|
const formData = new FormData();
|
|
180
226
|
formData.append('file', file);
|
|
181
227
|
formData.append('folderId', String(folderId));
|
|
182
228
|
if (options?.title) formData.append('title', options.title);
|
|
229
|
+
if (options?.skin) formData.append('skin', options.skin);
|
|
230
|
+
if (options?.classMappings) formData.append('classMappings', JSON.stringify(options.classMappings));
|
|
183
231
|
return request<UploadResult>('POST', '/upload', formData);
|
|
184
232
|
},
|
|
185
233
|
|
|
234
|
+
// Upload with progress tracking (uses XMLHttpRequest for onprogress events)
|
|
235
|
+
uploadCourseWithProgress: (
|
|
236
|
+
file: File,
|
|
237
|
+
folderId: number,
|
|
238
|
+
options: { title?: string } | undefined,
|
|
239
|
+
onProgress: (percent: number) => void
|
|
240
|
+
): Promise<UploadResult> => {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const xhr = new XMLHttpRequest();
|
|
243
|
+
const formData = new FormData();
|
|
244
|
+
formData.append('file', file);
|
|
245
|
+
formData.append('folderId', String(folderId));
|
|
246
|
+
if (options?.title) formData.append('title', options.title);
|
|
247
|
+
|
|
248
|
+
xhr.upload.onprogress = (event) => {
|
|
249
|
+
if (event.lengthComputable) {
|
|
250
|
+
const percent = Math.round((event.loaded / event.total) * 100);
|
|
251
|
+
onProgress(percent);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
xhr.onload = () => {
|
|
256
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
257
|
+
try {
|
|
258
|
+
resolve(JSON.parse(xhr.responseText));
|
|
259
|
+
} catch {
|
|
260
|
+
reject(new Error('Invalid JSON response'));
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
reject(new Error(xhr.statusText || `Upload failed: ${xhr.status}`));
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
xhr.onerror = () => reject(new Error('Network error during upload'));
|
|
268
|
+
xhr.onabort = () => reject(new Error('Upload aborted'));
|
|
269
|
+
|
|
270
|
+
xhr.open('POST', `${API_BASE}/upload`);
|
|
271
|
+
xhr.withCredentials = true; // Send cookies for cross-origin
|
|
272
|
+
xhr.send(formData);
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
|
|
186
276
|
uploadVersion: (file: File, documentId: number, note?: string) => {
|
|
187
277
|
const formData = new FormData();
|
|
188
278
|
formData.append('file', file);
|
|
@@ -194,4 +284,40 @@ export const api = {
|
|
|
194
284
|
// Launch
|
|
195
285
|
getLaunchUrl: (documentId: number) =>
|
|
196
286
|
request<{ url: string }>('GET', `/documents/${documentId}/launch`),
|
|
287
|
+
|
|
288
|
+
// Wrap & Download (PA-Patcher wrapping without CDS upload)
|
|
289
|
+
wrapAndDownload: async (file: File, options?: { skin?: string; classMappings?: CourseClassMappings }): Promise<void> => {
|
|
290
|
+
const formData = new FormData();
|
|
291
|
+
formData.append('file', file);
|
|
292
|
+
if (options?.skin) formData.append('skin', options.skin);
|
|
293
|
+
if (options?.classMappings) formData.append('classMappings', JSON.stringify(options.classMappings));
|
|
294
|
+
|
|
295
|
+
const response = await fetch(`${API_BASE}/wrap`, {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
body: formData,
|
|
298
|
+
credentials: 'include',
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
const error = await response.text();
|
|
303
|
+
throw new Error(error || `Wrap failed: ${response.status}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const blob = await response.blob();
|
|
307
|
+
|
|
308
|
+
// Extract filename from Content-Disposition header or generate one
|
|
309
|
+
const disposition = response.headers.get('Content-Disposition');
|
|
310
|
+
const filenameMatch = disposition?.match(/filename="(.+)"/);
|
|
311
|
+
const filename = filenameMatch?.[1] || file.name.replace(/\.zip$/i, '-wrapped.zip');
|
|
312
|
+
|
|
313
|
+
// Trigger browser download
|
|
314
|
+
const url = window.URL.createObjectURL(blob);
|
|
315
|
+
const a = document.createElement('a');
|
|
316
|
+
a.href = url;
|
|
317
|
+
a.download = filename;
|
|
318
|
+
document.body.appendChild(a);
|
|
319
|
+
a.click();
|
|
320
|
+
document.body.removeChild(a);
|
|
321
|
+
window.URL.revokeObjectURL(url);
|
|
322
|
+
},
|
|
197
323
|
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Block within a lesson */
|
|
4
|
+
interface CourseStructureBlock {
|
|
5
|
+
id: string;
|
|
6
|
+
type: string;
|
|
7
|
+
family?: string;
|
|
8
|
+
variant?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Lesson in the course */
|
|
13
|
+
interface CourseStructureLesson {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
type: string;
|
|
17
|
+
blocks: CourseStructureBlock[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Full parsed course structure */
|
|
21
|
+
export interface CourseStructure {
|
|
22
|
+
courseTitle: string;
|
|
23
|
+
courseId?: string;
|
|
24
|
+
lessons: CourseStructureLesson[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Class assignments at course/lesson/block levels */
|
|
28
|
+
export interface CourseClassMappings {
|
|
29
|
+
course?: string;
|
|
30
|
+
lessons?: Record<string, string>;
|
|
31
|
+
blocks?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CourseStructureStepProps {
|
|
35
|
+
structure: CourseStructure;
|
|
36
|
+
classMappings: CourseClassMappings;
|
|
37
|
+
onChange: (mappings: CourseClassMappings) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function CourseStructureStep({
|
|
41
|
+
structure,
|
|
42
|
+
classMappings,
|
|
43
|
+
onChange,
|
|
44
|
+
}: CourseStructureStepProps) {
|
|
45
|
+
const [expandedLessons, setExpandedLessons] = useState<Set<string>>(new Set());
|
|
46
|
+
|
|
47
|
+
const toggleLesson = (lessonId: string) => {
|
|
48
|
+
setExpandedLessons((prev) => {
|
|
49
|
+
const next = new Set(prev);
|
|
50
|
+
if (next.has(lessonId)) {
|
|
51
|
+
next.delete(lessonId);
|
|
52
|
+
} else {
|
|
53
|
+
next.add(lessonId);
|
|
54
|
+
}
|
|
55
|
+
return next;
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const setCourseClass = (value: string) => {
|
|
60
|
+
onChange({ ...classMappings, course: value || undefined });
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const setLessonClass = (lessonId: string, value: string) => {
|
|
64
|
+
const lessons = { ...classMappings.lessons };
|
|
65
|
+
if (value) {
|
|
66
|
+
lessons[lessonId] = value;
|
|
67
|
+
} else {
|
|
68
|
+
delete lessons[lessonId];
|
|
69
|
+
}
|
|
70
|
+
onChange({ ...classMappings, lessons: Object.keys(lessons).length > 0 ? lessons : undefined });
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const setBlockClass = (blockId: string, value: string) => {
|
|
74
|
+
const blocks = { ...classMappings.blocks };
|
|
75
|
+
if (value) {
|
|
76
|
+
blocks[blockId] = value;
|
|
77
|
+
} else {
|
|
78
|
+
delete blocks[blockId];
|
|
79
|
+
}
|
|
80
|
+
onChange({ ...classMappings, blocks: Object.keys(blocks).length > 0 ? blocks : undefined });
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const clearAll = () => {
|
|
84
|
+
onChange({});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Count total assignments
|
|
88
|
+
const assignmentCount =
|
|
89
|
+
(classMappings.course ? 1 : 0) +
|
|
90
|
+
Object.keys(classMappings.lessons || {}).length +
|
|
91
|
+
Object.keys(classMappings.blocks || {}).length;
|
|
92
|
+
|
|
93
|
+
const blockCount = structure.lessons.reduce((sum, l) => sum + l.blocks.length, 0);
|
|
94
|
+
|
|
95
|
+
const formatBlockLabel = (block: CourseStructureBlock): string => {
|
|
96
|
+
const parts = [block.type];
|
|
97
|
+
if (block.variant) parts.push(block.variant);
|
|
98
|
+
return parts.join(' / ');
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="course-structure-step">
|
|
103
|
+
<div className="course-structure-header">
|
|
104
|
+
<div>
|
|
105
|
+
<h4>Course Structure</h4>
|
|
106
|
+
<span className="course-structure-meta">
|
|
107
|
+
{structure.lessons.length} lessons, {blockCount} blocks
|
|
108
|
+
{assignmentCount > 0 && (
|
|
109
|
+
<span className="course-structure-badge">{assignmentCount} class{assignmentCount !== 1 ? 'es' : ''} assigned</span>
|
|
110
|
+
)}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
{assignmentCount > 0 && (
|
|
114
|
+
<button className="btn btn-sm btn-secondary" onClick={clearAll}>
|
|
115
|
+
Clear All
|
|
116
|
+
</button>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Course-level class */}
|
|
121
|
+
<div className="tree-node tree-course">
|
|
122
|
+
<div className="tree-node-row">
|
|
123
|
+
<span className="tree-node-icon">📚</span>
|
|
124
|
+
<span className="tree-node-label">{structure.courseTitle}</span>
|
|
125
|
+
</div>
|
|
126
|
+
<input
|
|
127
|
+
type="text"
|
|
128
|
+
className="tree-class-input"
|
|
129
|
+
placeholder="Course-level class..."
|
|
130
|
+
value={classMappings.course || ''}
|
|
131
|
+
onChange={(e) => setCourseClass(e.target.value)}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Lessons */}
|
|
136
|
+
<div className="tree-lessons">
|
|
137
|
+
{structure.lessons.map((lesson, lessonIdx) => {
|
|
138
|
+
const isExpanded = expandedLessons.has(lesson.id);
|
|
139
|
+
const lessonHasClass = !!classMappings.lessons?.[lesson.id];
|
|
140
|
+
const blocksWithClass = lesson.blocks.filter(
|
|
141
|
+
(b) => !!classMappings.blocks?.[b.id]
|
|
142
|
+
).length;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div key={lesson.id} className="tree-lesson">
|
|
146
|
+
<div
|
|
147
|
+
className="tree-node tree-node-lesson"
|
|
148
|
+
onClick={() => toggleLesson(lesson.id)}
|
|
149
|
+
>
|
|
150
|
+
<div className="tree-node-row">
|
|
151
|
+
<span className="tree-expand">{isExpanded ? '\u25BC' : '\u25B6'}</span>
|
|
152
|
+
<span className="tree-node-label">
|
|
153
|
+
<span className="tree-lesson-num">{lessonIdx + 1}.</span>
|
|
154
|
+
{' '}{lesson.title}
|
|
155
|
+
</span>
|
|
156
|
+
{(lessonHasClass || blocksWithClass > 0) && !isExpanded && (
|
|
157
|
+
<span className="tree-node-indicator">
|
|
158
|
+
{lessonHasClass && blocksWithClass > 0
|
|
159
|
+
? `lesson + ${blocksWithClass} block${blocksWithClass > 1 ? 's' : ''}`
|
|
160
|
+
: lessonHasClass
|
|
161
|
+
? 'lesson'
|
|
162
|
+
: `${blocksWithClass} block${blocksWithClass > 1 ? 's' : ''}`}
|
|
163
|
+
</span>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{isExpanded && (
|
|
169
|
+
<div className="tree-lesson-content">
|
|
170
|
+
<div className="tree-lesson-class">
|
|
171
|
+
<input
|
|
172
|
+
type="text"
|
|
173
|
+
className="tree-class-input"
|
|
174
|
+
placeholder="Lesson-level class..."
|
|
175
|
+
value={classMappings.lessons?.[lesson.id] || ''}
|
|
176
|
+
onChange={(e) => setLessonClass(lesson.id, e.target.value)}
|
|
177
|
+
onClick={(e) => e.stopPropagation()}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{lesson.blocks.map((block) => (
|
|
182
|
+
<div key={block.id} className="tree-node tree-node-block">
|
|
183
|
+
<div className="tree-node-row">
|
|
184
|
+
<span className="tree-block-pipe">|--</span>
|
|
185
|
+
<span className="tree-block-type">{formatBlockLabel(block)}</span>
|
|
186
|
+
{block.title && (
|
|
187
|
+
<span className="tree-block-title" title={block.title}>
|
|
188
|
+
{block.title.length > 40
|
|
189
|
+
? block.title.substring(0, 40) + '...'
|
|
190
|
+
: block.title}
|
|
191
|
+
</span>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
<input
|
|
195
|
+
type="text"
|
|
196
|
+
className="tree-class-input tree-class-input-sm"
|
|
197
|
+
placeholder="class..."
|
|
198
|
+
value={classMappings.blocks?.[block.id] || ''}
|
|
199
|
+
onChange={(e) => setBlockClass(block.id, e.target.value)}
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
})}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|