@package-uploader/ui 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-CAZIUAbb.js +71 -0
- package/dist/assets/{index-CcXisJMx.css → index-DHoRoGws.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/api/client.ts +10 -0
- package/src/components/CourseStructureStep.tsx +315 -140
- package/src/components/UploadModal.tsx +253 -34
- package/src/index.css +207 -95
- package/dist/assets/index-95fEc7BA.js +0 -71
- package/src/components/BlockGroupingPanel.tsx +0 -264
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
import { useBlockGrouping, type BlockGroup } from '../hooks/useBlockGrouping';
|
|
3
|
-
|
|
4
|
-
interface Block {
|
|
5
|
-
id: string;
|
|
6
|
-
type: string;
|
|
7
|
-
family?: string;
|
|
8
|
-
variant?: string;
|
|
9
|
-
title?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface BlockGroupingPanelProps {
|
|
13
|
-
lessonId: string;
|
|
14
|
-
blocks: Block[];
|
|
15
|
-
groups: BlockGroup[];
|
|
16
|
-
onGroupsChange: (groups: BlockGroup[]) => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Color palette for block families */
|
|
20
|
-
function familyColor(family?: string): string {
|
|
21
|
-
switch (family) {
|
|
22
|
-
case 'text': return '#3b82f6';
|
|
23
|
-
case 'media': return '#22c55e';
|
|
24
|
-
case 'interactive': return '#a855f7';
|
|
25
|
-
case 'knowledge-check': return '#f59e0b';
|
|
26
|
-
case 'quote': return '#ec4899';
|
|
27
|
-
default: return '#6b7280';
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export default function BlockGroupingPanel({
|
|
32
|
-
lessonId,
|
|
33
|
-
blocks,
|
|
34
|
-
groups,
|
|
35
|
-
onGroupsChange,
|
|
36
|
-
}: BlockGroupingPanelProps) {
|
|
37
|
-
const [newGroupClass, setNewGroupClass] = useState('');
|
|
38
|
-
const [newGroupLabel, setNewGroupLabel] = useState('');
|
|
39
|
-
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
|
|
40
|
-
const [editClass, setEditClass] = useState('');
|
|
41
|
-
const [editLabel, setEditLabel] = useState('');
|
|
42
|
-
|
|
43
|
-
const lessonBlockIds = blocks.map((b) => b.id);
|
|
44
|
-
|
|
45
|
-
const {
|
|
46
|
-
selectedBlockIds,
|
|
47
|
-
toggleBlock,
|
|
48
|
-
clearSelection,
|
|
49
|
-
canCreateGroup,
|
|
50
|
-
validationError,
|
|
51
|
-
createGroup,
|
|
52
|
-
removeGroup,
|
|
53
|
-
updateGroup,
|
|
54
|
-
isBlockGrouped,
|
|
55
|
-
} = useBlockGrouping({
|
|
56
|
-
lessonBlockIds,
|
|
57
|
-
lessonId,
|
|
58
|
-
groups,
|
|
59
|
-
onGroupsChange,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const handleCreateGroup = () => {
|
|
63
|
-
if (!canCreateGroup || !newGroupClass.trim()) return;
|
|
64
|
-
createGroup(newGroupClass, newGroupLabel || undefined);
|
|
65
|
-
setNewGroupClass('');
|
|
66
|
-
setNewGroupLabel('');
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const startEditGroup = (group: BlockGroup) => {
|
|
70
|
-
setEditingGroupId(group.id);
|
|
71
|
-
setEditClass(group.className);
|
|
72
|
-
setEditLabel(group.label || '');
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const saveEditGroup = () => {
|
|
76
|
-
if (editingGroupId) {
|
|
77
|
-
updateGroup(editingGroupId, { className: editClass, label: editLabel });
|
|
78
|
-
setEditingGroupId(null);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const cancelEdit = () => {
|
|
83
|
-
setEditingGroupId(null);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// Build blockId -> group lookup
|
|
87
|
-
const blockGroupMap = new Map<string, BlockGroup>();
|
|
88
|
-
for (const group of groups) {
|
|
89
|
-
for (const bid of group.blockIds) {
|
|
90
|
-
blockGroupMap.set(bid, group);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Render a single block rectangle
|
|
95
|
-
const renderBlock = (block: Block, isGrouped: boolean) => {
|
|
96
|
-
const isSelected = selectedBlockIds.has(block.id);
|
|
97
|
-
const canSelect = !isGrouped;
|
|
98
|
-
const color = familyColor(block.family);
|
|
99
|
-
const label = [block.type, block.variant].filter(Boolean).join(' / ');
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<div
|
|
103
|
-
key={block.id}
|
|
104
|
-
className={`bgp-block${isSelected ? ' selected' : ''}${isGrouped ? ' grouped' : ''}`}
|
|
105
|
-
onClick={() => canSelect && toggleBlock(block.id)}
|
|
106
|
-
style={{ '--block-color': color } as React.CSSProperties}
|
|
107
|
-
title={canSelect ? 'Click to select' : 'Already in a group'}
|
|
108
|
-
>
|
|
109
|
-
<span className="bgp-block-color" />
|
|
110
|
-
<span className="bgp-block-label">{label}</span>
|
|
111
|
-
{block.title && (
|
|
112
|
-
<span className="bgp-block-title">
|
|
113
|
-
{block.title.length > 50 ? block.title.substring(0, 50) + '...' : block.title}
|
|
114
|
-
</span>
|
|
115
|
-
)}
|
|
116
|
-
{isSelected && <span className="bgp-block-check">✓</span>}
|
|
117
|
-
</div>
|
|
118
|
-
);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
// Render group header (with edit/ungroup controls)
|
|
122
|
-
const renderGroupHeader = (group: BlockGroup) => {
|
|
123
|
-
const isEditing = editingGroupId === group.id;
|
|
124
|
-
|
|
125
|
-
if (isEditing) {
|
|
126
|
-
return (
|
|
127
|
-
<div className="bgp-group-header">
|
|
128
|
-
<div className="bgp-group-edit">
|
|
129
|
-
<input
|
|
130
|
-
type="text"
|
|
131
|
-
value={editClass}
|
|
132
|
-
onChange={(e) => setEditClass(e.target.value)}
|
|
133
|
-
placeholder="CSS class(es)"
|
|
134
|
-
className="bgp-edit-input"
|
|
135
|
-
/>
|
|
136
|
-
<input
|
|
137
|
-
type="text"
|
|
138
|
-
value={editLabel}
|
|
139
|
-
onChange={(e) => setEditLabel(e.target.value)}
|
|
140
|
-
placeholder="Label (optional)"
|
|
141
|
-
className="bgp-edit-input bgp-edit-input-sm"
|
|
142
|
-
/>
|
|
143
|
-
<button className="bgp-btn bgp-btn-save" onClick={saveEditGroup}>Save</button>
|
|
144
|
-
<button className="bgp-btn bgp-btn-cancel" onClick={cancelEdit}>Cancel</button>
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<div className="bgp-group-header">
|
|
152
|
-
<span className="bgp-group-label">
|
|
153
|
-
{group.label || group.className}
|
|
154
|
-
</span>
|
|
155
|
-
<span className="bgp-group-class">.{group.className}</span>
|
|
156
|
-
<button
|
|
157
|
-
className="bgp-btn bgp-btn-edit"
|
|
158
|
-
onClick={() => startEditGroup(group)}
|
|
159
|
-
title="Edit group"
|
|
160
|
-
>
|
|
161
|
-
Edit
|
|
162
|
-
</button>
|
|
163
|
-
<button
|
|
164
|
-
className="bgp-btn bgp-btn-ungroup"
|
|
165
|
-
onClick={() => removeGroup(group.id)}
|
|
166
|
-
title="Remove group"
|
|
167
|
-
>
|
|
168
|
-
Ungroup
|
|
169
|
-
</button>
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
// Build flat display list, rendering groups as nested containers
|
|
175
|
-
const renderedGroups = new Set<string>();
|
|
176
|
-
const previewItems: React.ReactNode[] = [];
|
|
177
|
-
|
|
178
|
-
for (const block of blocks) {
|
|
179
|
-
const group = blockGroupMap.get(block.id);
|
|
180
|
-
|
|
181
|
-
if (group) {
|
|
182
|
-
// Skip blocks already rendered as part of their group
|
|
183
|
-
if (renderedGroups.has(group.id)) continue;
|
|
184
|
-
renderedGroups.add(group.id);
|
|
185
|
-
|
|
186
|
-
// Render the entire group container
|
|
187
|
-
const groupBlocks = group.blockIds
|
|
188
|
-
.map((bid) => blocks.find((b) => b.id === bid))
|
|
189
|
-
.filter(Boolean) as Block[];
|
|
190
|
-
|
|
191
|
-
previewItems.push(
|
|
192
|
-
<div key={`group-${group.id}`} className="bgp-group-wrapper">
|
|
193
|
-
{renderGroupHeader(group)}
|
|
194
|
-
{groupBlocks.map((b) => renderBlock(b, true))}
|
|
195
|
-
</div>
|
|
196
|
-
);
|
|
197
|
-
} else {
|
|
198
|
-
previewItems.push(renderBlock(block, false));
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const hasSelection = selectedBlockIds.size > 0;
|
|
203
|
-
|
|
204
|
-
return (
|
|
205
|
-
<div className="block-grouping-panel">
|
|
206
|
-
<div className="bgp-header">
|
|
207
|
-
<span className="bgp-title">Block Groups</span>
|
|
208
|
-
{groups.length > 0 && (
|
|
209
|
-
<span className="bgp-count">{groups.length} group{groups.length !== 1 ? 's' : ''}</span>
|
|
210
|
-
)}
|
|
211
|
-
</div>
|
|
212
|
-
|
|
213
|
-
{/* Schematic preview */}
|
|
214
|
-
<div className="bgp-preview">
|
|
215
|
-
{previewItems}
|
|
216
|
-
</div>
|
|
217
|
-
|
|
218
|
-
{/* Create group controls */}
|
|
219
|
-
{hasSelection && (
|
|
220
|
-
<div className="bgp-create">
|
|
221
|
-
<div className="bgp-create-info">
|
|
222
|
-
{selectedBlockIds.size} block{selectedBlockIds.size !== 1 ? 's' : ''} selected
|
|
223
|
-
{validationError && <span className="bgp-validation-error"> — {validationError}</span>}
|
|
224
|
-
</div>
|
|
225
|
-
<div className="bgp-create-inputs">
|
|
226
|
-
<input
|
|
227
|
-
type="text"
|
|
228
|
-
value={newGroupClass}
|
|
229
|
-
onChange={(e) => setNewGroupClass(e.target.value)}
|
|
230
|
-
placeholder="CSS class(es) for wrapper..."
|
|
231
|
-
className="bgp-create-class"
|
|
232
|
-
onKeyDown={(e) => e.key === 'Enter' && handleCreateGroup()}
|
|
233
|
-
/>
|
|
234
|
-
<input
|
|
235
|
-
type="text"
|
|
236
|
-
value={newGroupLabel}
|
|
237
|
-
onChange={(e) => setNewGroupLabel(e.target.value)}
|
|
238
|
-
placeholder="Label (optional)"
|
|
239
|
-
className="bgp-create-label"
|
|
240
|
-
/>
|
|
241
|
-
</div>
|
|
242
|
-
<div className="bgp-create-actions">
|
|
243
|
-
<button
|
|
244
|
-
className="btn btn-sm btn-primary"
|
|
245
|
-
onClick={handleCreateGroup}
|
|
246
|
-
disabled={!canCreateGroup || !newGroupClass.trim()}
|
|
247
|
-
>
|
|
248
|
-
Create Group
|
|
249
|
-
</button>
|
|
250
|
-
<button className="btn btn-sm btn-secondary" onClick={clearSelection}>
|
|
251
|
-
Cancel
|
|
252
|
-
</button>
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
)}
|
|
256
|
-
|
|
257
|
-
{!hasSelection && groups.length === 0 && (
|
|
258
|
-
<div className="bgp-hint">
|
|
259
|
-
Click adjacent blocks above to select them, then create a group to wrap them in a container div.
|
|
260
|
-
</div>
|
|
261
|
-
)}
|
|
262
|
-
</div>
|
|
263
|
-
);
|
|
264
|
-
}
|