@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--color-primary: #2563eb;--color-primary-hover: #1d4ed8;--color-success: #10b981;--color-error: #ef4444;--color-warning: #f59e0b;--color-bg: #ffffff;--color-bg-secondary: #f9fafb;--color-border: #e5e7eb;--color-text: #111827;--color-text-muted: #6b7280;--radius: 8px;--radius-sm: 4px;--shadow: 0 1px 3px rgba(0, 0, 0, .1);--shadow-lg: 0 4px 6px rgba(0, 0, 0, .1)}*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;background:var(--color-bg-secondary);color:var(--color-text);line-height:1.5}.app{min-height:100vh;display:flex;flex-direction:column}.header{background:var(--color-bg);border-bottom:1px solid var(--color-border);padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between}.header h1{font-size:1.25rem;font-weight:600}.nav{display:flex;gap:1rem}.nav a{color:var(--color-text-muted);text-decoration:none;padding:.5rem 1rem;border-radius:var(--radius);transition:all .2s}.nav a:hover,.nav a.active{color:var(--color-primary);background:#2563eb1a}.main{flex:1;padding:2rem;max-width:1200px;margin:0 auto;width:100%}.card{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1.5rem;box-shadow:var(--shadow)}.card+.card{margin-top:1rem}.card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem}.card-title{font-size:1.125rem;font-weight:600}.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:var(--radius);border:none;font-size:.875rem;font-weight:500;cursor:pointer;transition:all .2s}.btn-primary{background:var(--color-primary);color:#fff}.btn-primary:hover{background:var(--color-primary-hover)}.btn-secondary{background:var(--color-bg);border:1px solid var(--color-border);color:var(--color-text)}.btn-secondary:hover{background:var(--color-bg-secondary)}.btn:disabled{opacity:.5;cursor:not-allowed}.dropzone{border:2px dashed var(--color-border);border-radius:var(--radius);padding:3rem;text-align:center;cursor:pointer;transition:all .2s}.dropzone:hover,.dropzone.active{border-color:var(--color-primary);background:#2563eb0d}.dropzone p{color:var(--color-text-muted);margin-top:.5rem}.file-list{margin-top:1rem}.file-item{display:flex;align-items:center;justify-content:space-between;padding:.75rem;border:1px solid var(--color-border);border-radius:var(--radius);margin-bottom:.5rem}.file-item .name{font-weight:500}.file-item .size{color:var(--color-text-muted);font-size:.875rem}.file-item .status{display:flex;align-items:center;gap:.5rem}.status-pending{color:var(--color-text-muted)}.status-uploading{color:var(--color-primary)}.status-success{color:var(--color-success)}.status-error{color:var(--color-error)}.document-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}.document-card{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem;transition:all .2s}.document-card:hover{box-shadow:var(--shadow-lg)}.document-card .title{font-weight:600;margin-bottom:.25rem}.document-card .meta{color:var(--color-text-muted);font-size:.875rem;margin-bottom:.75rem}.document-card .actions{display:flex;gap:.5rem}.folder-tree{border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem;max-height:400px;overflow-y:auto}.folder-item{padding:.5rem;cursor:pointer;border-radius:var(--radius);display:flex;align-items:center;gap:.5rem}.folder-item:hover{background:var(--color-bg-secondary)}.folder-item.selected{background:#2563eb1a;color:var(--color-primary)}.folder-item .icon{font-size:1.25rem}.modal-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.modal{background:var(--color-bg);border-radius:var(--radius);padding:1.5rem;max-width:500px;width:90%;max-height:80vh;overflow-y:auto}.modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem}.modal-title{font-size:1.125rem;font-weight:600}.modal-close{background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--color-text-muted)}.form-group{margin-bottom:1rem}.form-label{display:block;font-weight:500;margin-bottom:.25rem}.form-input,.form-select{width:100%;padding:.5rem;border:1px solid var(--color-border);border-radius:var(--radius);font-size:1rem}.form-input:focus,.form-select:focus{outline:none;border-color:var(--color-primary)}.spinner{width:20px;height:20px;border:2px solid var(--color-border);border-top-color:var(--color-primary);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.empty-state{text-align:center;padding:3rem;color:var(--color-text-muted)}.empty-state h3{color:var(--color-text);margin-bottom:.5rem}.alert{padding:1rem;border-radius:var(--radius);margin-bottom:1rem}.alert-success{background:#10b9811a;border:1px solid var(--color-success);color:var(--color-success)}.alert-error{background:#ef44441a;border:1px solid var(--color-error);color:var(--color-error)}.alert-warning{background:#f59e0b1a;border:1px solid var(--color-warning);color:var(--color-warning)}.breadcrumb{display:flex;align-items:center;flex-wrap:wrap;gap:0;font-size:.875rem}.breadcrumb-item{background:none;border:none;color:var(--color-primary);cursor:pointer;padding:.25rem .5rem;border-radius:var(--radius);font-size:inherit}.breadcrumb-item:hover{background:#2563eb1a}.breadcrumb-current{color:var(--color-text);cursor:default}.breadcrumb-current:hover{background:transparent}.breadcrumb-separator{color:var(--color-text-muted);margin:0 .25rem}.browse-page{display:flex;flex-direction:column;gap:1rem}.toolbar{display:flex;justify-content:space-between;align-items:center;background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:.75rem 1rem;gap:1rem;flex-wrap:wrap}.toolbar-left{display:flex;align-items:center;gap:.5rem;flex:1;min-width:200px}.toolbar-right{display:flex;align-items:center;gap:.5rem}.search-input{padding:.5rem .75rem;border:1px solid var(--color-border);border-radius:var(--radius);font-size:.875rem;width:200px}.search-input:focus{outline:none;border-color:var(--color-primary)}.content-area{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);overflow:hidden}.data-grid-container{display:flex;flex-direction:column}.data-grid{width:100%;border-collapse:collapse;font-size:.875rem}.data-grid thead{background:var(--color-bg-secondary);border-bottom:1px solid var(--color-border)}.data-grid th{text-align:left;padding:.75rem 1rem;font-weight:600;color:var(--color-text-muted);white-space:nowrap}.data-grid th.sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.data-grid th.sortable:hover{color:var(--color-text)}.data-grid td{padding:.75rem 1rem;border-bottom:1px solid var(--color-border);vertical-align:middle}.grid-row{transition:background .15s}.grid-row:hover{background:var(--color-bg-secondary)}.folder-row{cursor:pointer}.folder-row:hover{background:#2563eb0d}.col-checkbox{width:40px;text-align:center}.col-name{min-width:250px}.col-format,.col-type{width:120px}.col-updated,.col-created{width:140px}.col-actions{width:100px;text-align:right}.item-icon{margin-right:.5rem;font-size:1rem}.item-name{font-weight:500}.subfolder-count{color:var(--color-text-muted);font-size:.75rem;margin-left:.5rem}.btn-icon{background:none;border:none;padding:.25rem .5rem;cursor:pointer;font-size:1rem;opacity:.6;transition:opacity .15s}.btn-icon:hover{opacity:1}.btn-icon:disabled{opacity:.3;cursor:not-allowed}.btn-icon.pinned,.btn-delete:hover{opacity:1}.data-grid-footer{padding:.75rem 1rem;background:var(--color-bg-secondary);border-top:1px solid var(--color-border);font-size:.75rem;color:var(--color-text-muted)}.data-grid-loading,.data-grid-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:3rem;text-align:center;color:var(--color-text-muted);gap:.5rem}.data-grid-empty .empty-icon{font-size:3rem;margin-bottom:.5rem}.data-grid-empty h3{color:var(--color-text);margin:0}.data-grid-empty p{margin:.5rem 0 1rem}.folder-browser{border:1px solid var(--color-border);border-radius:var(--radius);overflow:hidden}.folder-browser-toolbar{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:var(--color-bg-secondary);border-bottom:1px solid var(--color-border)}.folder-browser-actions{display:flex;gap:.5rem}.folder-browser-list{max-height:300px;overflow-y:auto}.folder-browser-table{width:100%;border-collapse:collapse}.folder-browser-row{transition:background .15s}.folder-browser-row:hover{background:var(--color-bg-secondary)}.folder-browser-row.selected{background:#2563eb1a}.folder-browser-row td{padding:.5rem;border-bottom:1px solid var(--color-border);vertical-align:middle}.folder-browser-icon{width:40px;text-align:center;font-size:1.25rem}.folder-browser-name{cursor:pointer;font-weight:500}.folder-browser-name:hover{color:var(--color-primary)}.subfolder-indicator{color:var(--color-text-muted);font-size:.75rem;font-weight:400;margin-left:.5rem}.folder-browser-select{width:100px;text-align:center}.folder-browser-delete{width:50px;text-align:center}.folder-browser-loading,.folder-browser-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;color:var(--color-text-muted);gap:.75rem}.upload-page{display:flex;flex-direction:column;gap:1rem}.selected-folder-badge{background:#2563eb1a;color:var(--color-primary);padding:.25rem .75rem;border-radius:999px;font-size:.75rem;font-weight:500}.upload-actions{margin-top:1rem;display:flex;gap:.5rem;justify-content:flex-end}.btn-sm{padding:.25rem .5rem;font-size:.75rem}.document-row{cursor:pointer;transition:background .15s}.document-row:hover{background:var(--color-bg-secondary)}.document-row.expanded{background:#2563eb0d}.expand-icon{display:inline-block;width:1rem;margin-right:.25rem;transition:transform .2s;color:var(--color-text-muted)}.expand-icon.expanded{transform:rotate(90deg)}.expanded-row td{padding:0!important;background:var(--color-bg-secondary);border-bottom:2px solid var(--color-border)}.course-detail-panel{padding:1rem;display:flex;flex-direction:column;gap:1rem}.course-detail-loading,.course-detail-error{display:flex;align-items:center;justify-content:center;gap:.75rem;padding:2rem;color:var(--color-text-muted)}.course-detail-error{color:var(--color-error);flex-direction:column}.course-detail-tile{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem}.tile-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:1px solid var(--color-border)}.tile-header h3{font-size:1rem;font-weight:600;margin:0}.btn-close{background:none;border:none;font-size:1.25rem;color:var(--color-text-muted);cursor:pointer;padding:.25rem;line-height:1;border-radius:var(--radius);transition:all .15s}.btn-close:hover{background:var(--color-bg-secondary);color:var(--color-text)}.detail-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.75rem 1.5rem}.detail-item{display:flex;flex-direction:column;gap:.125rem}.detail-item.span-2{grid-column:span 2}.detail-label{font-size:.7rem;font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.025em}.detail-value{font-size:.875rem;color:var(--color-text)}.detail-value.mono{font-family:SF Mono,Monaco,Consolas,monospace;font-size:.75rem;background:var(--color-bg-secondary);padding:.125rem .375rem;border-radius:4px}.detail-value.description{line-height:1.4;max-height:4.2em;overflow:hidden;text-overflow:ellipsis}.thin-pack-row{display:grid;grid-template-columns:repeat(2,1fr);gap:1rem}.thin-pack-tile{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius);padding:1rem;border-left:4px solid}.thin-pack-tile.public{border-left-color:var(--color-success)}.thin-pack-tile.private{border-left-color:var(--color-primary)}.thin-pack-header{display:flex;align-items:flex-start;gap:.75rem;margin-bottom:.75rem}.thin-pack-icon{font-size:1.5rem}.thin-pack-titles{flex:1}.thin-pack-title{font-size:.9rem;font-weight:600;margin:0}.thin-pack-subtitle{font-size:.75rem;color:var(--color-text-muted);margin:.125rem 0 0}.thin-pack-content{display:flex;flex-direction:column;gap:.5rem}.thin-pack-error{font-size:.75rem;color:var(--color-error);padding:.5rem;background:#ef44441a;border-radius:4px}.thin-pack-status{display:flex;align-items:center;gap:.5rem;font-size:.8rem}.thin-pack-status.exists{color:var(--color-success)}.thin-pack-status.exists .status-icon{display:inline-flex;align-items:center;justify-content:center;width:1.25rem;height:1.25rem;background:var(--color-success);color:#fff;border-radius:50%;font-size:.7rem}.thin-pack-status.missing{color:var(--color-text-muted);font-style:italic}.thin-pack-meta{display:flex;gap:1rem;font-size:.7rem;color:var(--color-text-muted)}.thin-pack-actions{display:flex;gap:.5rem;margin-top:.25rem}.thin-pack-slug-input{display:flex;flex-direction:column;gap:.25rem}.thin-pack-slug-input label{font-size:.7rem;color:var(--color-text-muted)}.thin-pack-slug-input input{padding:.375rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text)}.thin-pack-slug-input input:focus{outline:none;border-color:var(--color-primary)}.thin-pack-slug-input input::placeholder{color:var(--color-text-muted);opacity:.6}.thin-pack-name-input{display:flex;flex-direction:column;gap:.25rem}.thin-pack-name-input label{font-size:.7rem;color:var(--color-text-muted)}.thin-pack-name-input input{padding:.375rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text)}.thin-pack-name-input input:focus{outline:none;border-color:var(--color-primary)}.thin-pack-name-input input::placeholder{color:var(--color-text-muted);opacity:.6}.token-preview{font-size:.65rem;color:var(--color-text-muted);font-family:monospace;word-break:break-all}.thin-pack-visibility{display:flex;flex-direction:column;gap:.25rem;padding:.5rem;background:var(--color-bg-tertiary);border-radius:4px}.visibility-toggle{display:flex;align-items:center;gap:.5rem;cursor:pointer}.visibility-toggle input[type=checkbox]{width:16px;height:16px;cursor:pointer}.toggle-label{font-size:.85rem;font-weight:500}.visibility-hint{font-size:.7rem;color:var(--color-text-muted);margin-left:24px}.thin-pack-tile.lms{border-left-color:var(--color-success)}.thin-pack-tile.shared{border-left-color:var(--color-primary)}.upload-modal{width:600px;max-width:90vw;max-height:90vh;display:flex;flex-direction:column}.upload-modal-content{flex:1;overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:1rem}.upload-modal .dropzone{min-height:120px}.upload-file-list{display:flex;flex-direction:column;gap:.5rem}.upload-file-list h4{margin:0;font-size:.85rem;color:var(--color-text-muted)}.upload-file-item{display:flex;justify-content:space-between;align-items:center;padding:.5rem;background:var(--color-bg-secondary);border-radius:8px}.upload-file-info{display:flex;flex-direction:column;gap:.125rem}.upload-file-name{font-weight:500;font-size:.875rem}.upload-file-size{font-size:.75rem;color:var(--color-text-muted)}.upload-file-status{display:flex;align-items:center;gap:.5rem}.upload-progress-container{display:flex;align-items:center;gap:.75rem;min-width:150px}.upload-progress-bar{flex:1;height:8px;background:var(--color-border);border-radius:4px;overflow:hidden}.upload-progress-fill{height:100%;background:var(--color-primary);transition:width .15s ease-out}.upload-progress-text{font-size:.75rem;font-weight:500;color:var(--color-text-muted);min-width:36px;text-align:right}.status-processing{color:var(--color-primary);font-size:.875rem}.upload-link-options{background:var(--color-bg-secondary);border-radius:var(--radius);padding:1rem;display:flex;flex-direction:column;gap:.75rem}.upload-link-options h4{margin:0;font-size:.85rem;color:var(--color-text-muted)}.upload-link-option{display:flex;flex-direction:column;gap:.5rem}.upload-link-toggle{display:flex;align-items:center;gap:.5rem;cursor:pointer}.upload-link-toggle input[type=checkbox]{width:1rem;height:1rem;cursor:pointer}.upload-link-toggle span{font-size:.875rem}.upload-link-name-input{display:flex;flex-direction:column;gap:.25rem;padding-left:1.5rem}.upload-link-name-input input{padding:.375rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text)}.upload-link-name-input input:focus{outline:none;border-color:var(--color-primary)}.upload-link-name-input input::placeholder{color:var(--color-text-muted);opacity:.6}.slug-preview{font-size:.7rem;color:var(--color-text-muted);font-family:monospace}.modal-footer{display:flex;justify-content:flex-end;gap:.5rem;padding:1rem;border-top:1px solid var(--color-border)}@media (max-width: 768px){.detail-grid{grid-template-columns:1fr}.detail-item.span-2{grid-column:span 1}.thin-pack-row{grid-template-columns:1fr}}.user-menu{display:flex;align-items:center;gap:.75rem;margin-left:auto}.user-info{display:flex;align-items:center;gap:.5rem}.user-avatar{width:2rem;height:2rem;border-radius:50%;background:var(--color-primary);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:.875rem}.user-name{font-size:.875rem;color:var(--color-text);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.header{display:flex;align-items:center;gap:1.5rem}.header .nav{margin-right:auto}.course-structure-step{display:flex;flex-direction:column;gap:.5rem}.course-structure-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.25rem}.course-structure-header h4{margin:0;font-size:1rem;font-weight:600}.course-structure-meta{display:flex;align-items:center;gap:.5rem;font-size:.75rem;color:var(--color-text-muted)}.course-structure-badge{background:#2563eb1a;color:var(--color-primary);padding:.125rem .5rem;border-radius:999px;font-size:.7rem;font-weight:500}.tree-node{padding:.25rem 0}.tree-node-row{display:flex;align-items:center;gap:.375rem;min-height:1.5rem}.tree-node-icon{font-size:1rem;flex-shrink:0}.tree-node-label{font-weight:500;font-size:.875rem;flex:1}.tree-class-input{width:100%;padding:.25rem .5rem;font-size:.8rem;border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-bg);color:var(--color-text);margin-top:.25rem}.tree-class-input:focus{outline:none;border-color:var(--color-primary)}.tree-class-input::placeholder{color:var(--color-text-muted);opacity:.6}.tree-class-input-sm{max-width:200px;margin-top:0;margin-left:auto;flex-shrink:0}.tree-course{background:var(--color-bg-secondary);border:1px solid var(--color-border);border-radius:var(--radius);padding:.5rem .75rem}.tree-lessons{display:flex;flex-direction:column;gap:.125rem;max-height:400px;overflow-y:auto;border:1px solid var(--color-border);border-radius:var(--radius);padding:.25rem}.tree-lesson{border-bottom:1px solid var(--color-border)}.tree-lesson:last-child{border-bottom:none}.tree-node-lesson{cursor:pointer;padding:.375rem .5rem;border-radius:var(--radius-sm);transition:background .15s}.tree-node-lesson:hover{background:var(--color-bg-secondary)}.tree-expand{font-size:.625rem;width:1rem;text-align:center;color:var(--color-text-muted);flex-shrink:0}.tree-lesson-num{color:var(--color-text-muted);font-weight:600;font-size:.75rem}.tree-node-indicator{font-size:.65rem;color:var(--color-primary);background:#2563eb1a;padding:.0625rem .375rem;border-radius:999px;white-space:nowrap;flex-shrink:0}.tree-lesson-content{padding:.25rem .5rem .5rem 1.5rem}.tree-lesson-class{margin-bottom:.375rem}.tree-node-block{display:flex;align-items:center;gap:.375rem;padding:.1875rem 0}.tree-node-block .tree-node-row{flex:1;min-width:0}.tree-block-pipe{font-family:monospace;color:var(--color-border);font-size:.75rem;flex-shrink:0}.tree-block-type{font-size:.75rem;font-weight:500;color:var(--color-text);white-space:nowrap}.tree-block-title{font-size:.7rem;color:var(--color-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}.structure-parsing-indicator{display:flex;align-items:center;gap:.75rem;padding:.75rem;color:var(--color-primary);font-size:.875rem}.structure-detected-badge{background:#10b9811a;color:var(--color-success);border:1px solid var(--color-success);border-radius:var(--radius);padding:.5rem .75rem;font-size:.8rem}.structure-multi-note{font-size:.75rem;color:var(--color-text-muted);font-style:italic;padding:.25rem 0}.structure-summary-badge{background:#2563eb1a;color:var(--color-primary);border-radius:var(--radius);padding:.5rem .75rem;font-size:.8rem;margin-top:.25rem}
|
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-C2Iuqreu.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CooIw87J.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
|
+
}
|