@zomako/elearning-components 2.0.3 → 2.0.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/InteractiveTimeline/README.md +117 -0
- package/InteractiveTimeline/index.tsx +191 -0
- package/InteractiveTimeline/style.module.css +211 -0
- package/MatchingActivity/README.md +104 -0
- package/MatchingActivity/index.tsx +208 -0
- package/MatchingActivity/style.module.css +195 -0
- package/README.md +24 -0
- package/SortingActivity/README.md +96 -0
- package/SortingActivity/index.tsx +228 -0
- package/SortingActivity/style.module.css +200 -0
- package/dist/elearning-components.css +1 -1
- package/dist/elearning-components.es.js +463 -5573
- package/dist/elearning-components.umd.js +8 -16
- package/package.json +4 -1
- package/rollup.config.js +20 -20
- package/src/App.jsx +3 -3
- package/src/components/Accordion/README.md +27 -44
- package/src/components/Accordion/index.tsx +26 -72
- package/src/components/Accordion/style.module.css +13 -2
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
DndContext,
|
|
4
|
+
closestCenter,
|
|
5
|
+
PointerSensor,
|
|
6
|
+
useSensor,
|
|
7
|
+
useSensors,
|
|
8
|
+
DragEndEvent,
|
|
9
|
+
useDraggable,
|
|
10
|
+
useDroppable,
|
|
11
|
+
} from '@dnd-kit/core';
|
|
12
|
+
import styles from './style.module.css';
|
|
13
|
+
|
|
14
|
+
export interface MatchItem {
|
|
15
|
+
id: string;
|
|
16
|
+
content: string;
|
|
17
|
+
matchId: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MatchTarget {
|
|
21
|
+
id: string;
|
|
22
|
+
content: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MatchingActivityProps {
|
|
26
|
+
items: MatchItem[];
|
|
27
|
+
targets: MatchTarget[];
|
|
28
|
+
onComplete: (result: { score: number; correct: number; total: number }) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shuffleArray<T>(array: T[]): T[] {
|
|
32
|
+
const shuffled = [...array];
|
|
33
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
34
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
35
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
36
|
+
}
|
|
37
|
+
return shuffled;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface DraggableItemProps {
|
|
41
|
+
item: MatchItem;
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function DraggableItem({ item, disabled }: DraggableItemProps) {
|
|
46
|
+
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
|
47
|
+
id: item.id,
|
|
48
|
+
disabled,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const style: React.CSSProperties = transform
|
|
52
|
+
? {
|
|
53
|
+
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
|
54
|
+
}
|
|
55
|
+
: undefined;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<li
|
|
59
|
+
ref={setNodeRef}
|
|
60
|
+
style={style}
|
|
61
|
+
className={`${styles.item} ${isDragging ? styles.itemDragging : ''} ${disabled ? styles.itemDisabled : ''}`}
|
|
62
|
+
{...(disabled ? {} : { ...attributes, ...listeners })}
|
|
63
|
+
>
|
|
64
|
+
<span className={styles.itemContent}>{item.content}</span>
|
|
65
|
+
</li>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface DroppableTargetProps {
|
|
70
|
+
target: MatchTarget;
|
|
71
|
+
matchedItem: MatchItem | null;
|
|
72
|
+
disabled?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function DroppableTarget({ target, matchedItem, disabled }: DroppableTargetProps) {
|
|
76
|
+
const { setNodeRef, isOver } = useDroppable({
|
|
77
|
+
id: target.id,
|
|
78
|
+
disabled: disabled ?? !!matchedItem,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<li
|
|
83
|
+
ref={setNodeRef}
|
|
84
|
+
className={`${styles.target} ${matchedItem ? styles.targetMatched : ''} ${isOver && !matchedItem ? styles.targetOver : ''}`}
|
|
85
|
+
>
|
|
86
|
+
<span className={styles.targetContent}>{target.content}</span>
|
|
87
|
+
{matchedItem ? (
|
|
88
|
+
<span className={styles.matchedContent}>{matchedItem.content}</span>
|
|
89
|
+
) : (
|
|
90
|
+
<span className={styles.targetPlaceholder} aria-hidden="true">
|
|
91
|
+
Drop here
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
</li>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const MatchingActivity: React.FC<MatchingActivityProps> = ({
|
|
99
|
+
items,
|
|
100
|
+
targets,
|
|
101
|
+
onComplete,
|
|
102
|
+
}) => {
|
|
103
|
+
const [shuffledItems] = useState(() => shuffleArray(items));
|
|
104
|
+
const [shuffledTargets] = useState(() => shuffleArray(targets));
|
|
105
|
+
const [matchedTargetIds, setMatchedTargetIds] = useState<Set<string>>(new Set());
|
|
106
|
+
const [hasCompleted, setHasCompleted] = useState(false);
|
|
107
|
+
|
|
108
|
+
const sensors = useSensors(
|
|
109
|
+
useSensor(PointerSensor, {
|
|
110
|
+
activationConstraint: { distance: 8 },
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const itemsByMatchId = useMemo(() => {
|
|
115
|
+
const map = new Map<string, MatchItem>();
|
|
116
|
+
shuffledItems.forEach((i) => map.set(i.matchId, i));
|
|
117
|
+
return map;
|
|
118
|
+
}, [shuffledItems]);
|
|
119
|
+
|
|
120
|
+
const targetById = useMemo(() => {
|
|
121
|
+
const map = new Map<string, MatchTarget>();
|
|
122
|
+
shuffledTargets.forEach((t) => map.set(t.id, t));
|
|
123
|
+
return map;
|
|
124
|
+
}, [shuffledTargets]);
|
|
125
|
+
|
|
126
|
+
const unmatchedItems = useMemo(
|
|
127
|
+
() => shuffledItems.filter((item) => !matchedTargetIds.has(item.matchId)),
|
|
128
|
+
[shuffledItems, matchedTargetIds]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const handleDragEnd = useCallback(
|
|
132
|
+
(event: DragEndEvent) => {
|
|
133
|
+
const { active, over } = event;
|
|
134
|
+
if (!over || hasCompleted) return;
|
|
135
|
+
|
|
136
|
+
const targetId = String(over.id);
|
|
137
|
+
const item = shuffledItems.find((i) => i.id === active.id);
|
|
138
|
+
if (!item || !targetById.has(targetId)) return;
|
|
139
|
+
if (item.matchId !== targetId) return;
|
|
140
|
+
if (matchedTargetIds.has(targetId)) return;
|
|
141
|
+
|
|
142
|
+
const nextMatched = new Set(matchedTargetIds).add(targetId);
|
|
143
|
+
setMatchedTargetIds(nextMatched);
|
|
144
|
+
|
|
145
|
+
if (nextMatched.size === targets.length && !hasCompleted) {
|
|
146
|
+
setHasCompleted(true);
|
|
147
|
+
const correct = nextMatched.size;
|
|
148
|
+
const total = targets.length;
|
|
149
|
+
const score = total > 0 ? Math.round((correct / total) * 100) : 0;
|
|
150
|
+
onComplete({ score, correct, total });
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
[shuffledItems, targetById, matchedTargetIds, targets.length, hasCompleted, onComplete]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (!items.length || !targets.length) {
|
|
157
|
+
return (
|
|
158
|
+
<div className={styles.wrapper}>
|
|
159
|
+
<p className={styles.empty}>No items or targets to match.</p>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div className={styles.wrapper}>
|
|
166
|
+
<p className={styles.instruction}>
|
|
167
|
+
Drag each item from the left and drop it on the correct match on the right.
|
|
168
|
+
</p>
|
|
169
|
+
|
|
170
|
+
<DndContext
|
|
171
|
+
sensors={sensors}
|
|
172
|
+
collisionDetection={closestCenter}
|
|
173
|
+
onDragEnd={handleDragEnd}
|
|
174
|
+
>
|
|
175
|
+
<div className={styles.columns}>
|
|
176
|
+
<div className={styles.column}>
|
|
177
|
+
<h3 className={styles.columnTitle}>Items</h3>
|
|
178
|
+
<ul className={styles.list} role="list">
|
|
179
|
+
{unmatchedItems.map((item) => (
|
|
180
|
+
<DraggableItem
|
|
181
|
+
key={item.id}
|
|
182
|
+
item={item}
|
|
183
|
+
disabled={hasCompleted}
|
|
184
|
+
/>
|
|
185
|
+
))}
|
|
186
|
+
</ul>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div className={styles.column}>
|
|
190
|
+
<h3 className={styles.columnTitle}>Matches</h3>
|
|
191
|
+
<ul className={styles.list} role="list">
|
|
192
|
+
{shuffledTargets.map((target) => (
|
|
193
|
+
<DroppableTarget
|
|
194
|
+
key={target.id}
|
|
195
|
+
target={target}
|
|
196
|
+
matchedItem={matchedTargetIds.has(target.id) ? itemsByMatchId.get(target.id) ?? null : null}
|
|
197
|
+
disabled={hasCompleted}
|
|
198
|
+
/>
|
|
199
|
+
))}
|
|
200
|
+
</ul>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</DndContext>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export default MatchingActivity;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* ---- Wrapper ---- */
|
|
2
|
+
.wrapper {
|
|
3
|
+
--match-accent: #2563eb;
|
|
4
|
+
--match-accent-hover: #1d4ed8;
|
|
5
|
+
--match-bg: #f8fafc;
|
|
6
|
+
--match-border: #e2e8f0;
|
|
7
|
+
--match-text: #1e293b;
|
|
8
|
+
--match-muted: #64748b;
|
|
9
|
+
--match-success: #16a34a;
|
|
10
|
+
--match-success-bg: #dcfce7;
|
|
11
|
+
--match-over: #93c5fd;
|
|
12
|
+
--match-over-bg: #eff6ff;
|
|
13
|
+
--match-radius: 12px;
|
|
14
|
+
--match-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
|
15
|
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
16
|
+
color: var(--match-text);
|
|
17
|
+
max-width: 100%;
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: 1.25rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ---- Instruction ---- */
|
|
24
|
+
.instruction {
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-size: 0.9375rem;
|
|
27
|
+
color: var(--match-muted);
|
|
28
|
+
line-height: 1.5;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ---- Two columns ---- */
|
|
32
|
+
.columns {
|
|
33
|
+
display: grid;
|
|
34
|
+
grid-template-columns: 1fr 1fr;
|
|
35
|
+
gap: 1.5rem;
|
|
36
|
+
align-items: start;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@media (max-width: 640px) {
|
|
40
|
+
.columns {
|
|
41
|
+
grid-template-columns: 1fr;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.column {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
gap: 0.75rem;
|
|
49
|
+
padding: 1.25rem;
|
|
50
|
+
background: var(--match-bg);
|
|
51
|
+
border: 2px solid var(--match-border);
|
|
52
|
+
border-radius: var(--match-radius);
|
|
53
|
+
min-height: 200px;
|
|
54
|
+
transition: border-color 0.25s ease, box-shadow 0.25s ease;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.columnTitle {
|
|
58
|
+
margin: 0;
|
|
59
|
+
font-size: 0.875rem;
|
|
60
|
+
font-weight: 700;
|
|
61
|
+
text-transform: uppercase;
|
|
62
|
+
letter-spacing: 0.05em;
|
|
63
|
+
color: var(--match-muted);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* ---- Lists ---- */
|
|
67
|
+
.list {
|
|
68
|
+
margin: 0;
|
|
69
|
+
padding: 0;
|
|
70
|
+
list-style: none;
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
gap: 0.5rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ---- Draggable items (left column) ---- */
|
|
77
|
+
.item {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
padding: 1rem 1.25rem;
|
|
81
|
+
background: #fff;
|
|
82
|
+
border: 2px solid var(--match-border);
|
|
83
|
+
border-radius: var(--match-radius);
|
|
84
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
85
|
+
cursor: grab;
|
|
86
|
+
transition:
|
|
87
|
+
transform 0.25s ease,
|
|
88
|
+
border-color 0.25s ease,
|
|
89
|
+
background-color 0.25s ease,
|
|
90
|
+
box-shadow 0.25s ease;
|
|
91
|
+
touch-action: none;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.item:hover {
|
|
95
|
+
border-color: var(--match-accent);
|
|
96
|
+
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.12);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.item:active {
|
|
100
|
+
cursor: grabbing;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.itemDragging {
|
|
104
|
+
opacity: 0.95;
|
|
105
|
+
transform: scale(1.02);
|
|
106
|
+
box-shadow: var(--match-shadow);
|
|
107
|
+
border-color: var(--match-accent);
|
|
108
|
+
z-index: 10;
|
|
109
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.itemDisabled {
|
|
113
|
+
cursor: default;
|
|
114
|
+
opacity: 0.7;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.itemContent {
|
|
118
|
+
font-size: 1rem;
|
|
119
|
+
font-weight: 500;
|
|
120
|
+
line-height: 1.4;
|
|
121
|
+
flex: 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ---- Droppable targets (right column) ---- */
|
|
125
|
+
.target {
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-direction: column;
|
|
128
|
+
gap: 0.35rem;
|
|
129
|
+
padding: 1rem 1.25rem;
|
|
130
|
+
background: #fff;
|
|
131
|
+
border: 2px dashed var(--match-border);
|
|
132
|
+
border-radius: var(--match-radius);
|
|
133
|
+
min-height: 3.5rem;
|
|
134
|
+
transition:
|
|
135
|
+
border-color 0.25s ease,
|
|
136
|
+
background-color 0.25s ease,
|
|
137
|
+
transform 0.25s ease,
|
|
138
|
+
box-shadow 0.25s ease;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.targetOver {
|
|
142
|
+
border-style: solid;
|
|
143
|
+
border-color: var(--match-accent);
|
|
144
|
+
background: var(--match-over-bg);
|
|
145
|
+
transform: scale(1.01);
|
|
146
|
+
box-shadow: 0 2px 12px rgba(37, 99, 235, 0.15);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.targetMatched {
|
|
150
|
+
border-style: solid;
|
|
151
|
+
border-color: var(--match-success);
|
|
152
|
+
background: var(--match-success-bg);
|
|
153
|
+
animation: matchSuccess 0.4s ease-out forwards;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@keyframes matchSuccess {
|
|
157
|
+
from {
|
|
158
|
+
transform: scale(1.02);
|
|
159
|
+
box-shadow: 0 0 0 0 rgba(22, 163, 74, 0.4);
|
|
160
|
+
}
|
|
161
|
+
to {
|
|
162
|
+
transform: scale(1);
|
|
163
|
+
box-shadow: 0 2px 8px rgba(22, 163, 74, 0.2);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.targetContent {
|
|
168
|
+
font-size: 0.875rem;
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
color: var(--match-muted);
|
|
171
|
+
line-height: 1.3;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.targetPlaceholder {
|
|
175
|
+
font-size: 0.8125rem;
|
|
176
|
+
color: var(--match-border);
|
|
177
|
+
font-style: italic;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.matchedContent {
|
|
181
|
+
font-size: 1rem;
|
|
182
|
+
font-weight: 500;
|
|
183
|
+
color: var(--match-success);
|
|
184
|
+
line-height: 1.4;
|
|
185
|
+
transition: opacity 0.3s ease;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ---- Empty state ---- */
|
|
189
|
+
.empty {
|
|
190
|
+
margin: 0;
|
|
191
|
+
padding: 2rem;
|
|
192
|
+
text-align: center;
|
|
193
|
+
color: var(--match-muted);
|
|
194
|
+
font-size: 1rem;
|
|
195
|
+
}
|
package/README.md
CHANGED
|
@@ -8,6 +8,18 @@ A collection of reusable React components for e-learning applications.
|
|
|
8
8
|
|
|
9
9
|
A fully-featured flashcard component with 3D flip animations. See `FlashcardDeck/README.md` for detailed documentation.
|
|
10
10
|
|
|
11
|
+
### InteractiveTimeline
|
|
12
|
+
|
|
13
|
+
A horizontal, draggable timeline that displays a series of events with clickable markers and prev/next navigation. See `InteractiveTimeline/README.md` for detailed documentation.
|
|
14
|
+
|
|
15
|
+
### SortingActivity
|
|
16
|
+
|
|
17
|
+
A drag-and-drop sorting activity component where users reorder items into the correct sequence. Uses @dnd-kit/sortable and pure CSS animations. See `SortingActivity/README.md` for detailed documentation.
|
|
18
|
+
|
|
19
|
+
### MatchingActivity
|
|
20
|
+
|
|
21
|
+
A drag-and-drop matching activity where users match items from one column to the correct targets in another. Uses @dnd-kit/core and pure CSS animations. See `MatchingActivity/README.md` for detailed documentation.
|
|
22
|
+
|
|
11
23
|
## Getting Started
|
|
12
24
|
|
|
13
25
|
### Installation
|
|
@@ -38,6 +50,18 @@ elearning-components/
|
|
|
38
50
|
│ ├── FlashcardDeck.css # Styles
|
|
39
51
|
│ ├── index.js # Export
|
|
40
52
|
│ └── README.md # Documentation
|
|
53
|
+
├── InteractiveTimeline/ # InteractiveTimeline component
|
|
54
|
+
│ ├── index.tsx # Component and exports
|
|
55
|
+
│ ├── style.module.css # CSS Modules styles
|
|
56
|
+
│ └── README.md # Documentation
|
|
57
|
+
├── SortingActivity/ # SortingActivity component
|
|
58
|
+
│ ├── index.tsx # Component and exports
|
|
59
|
+
│ ├── style.module.css # CSS Modules styles
|
|
60
|
+
│ └── README.md # Documentation
|
|
61
|
+
├── MatchingActivity/ # MatchingActivity component
|
|
62
|
+
│ ├── index.tsx # Component and exports
|
|
63
|
+
│ ├── style.module.css # CSS Modules styles
|
|
64
|
+
│ └── README.md # Documentation
|
|
41
65
|
├── src/ # Application source
|
|
42
66
|
│ ├── App.jsx # Demo app
|
|
43
67
|
│ └── main.jsx # Entry point
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# SortingActivity
|
|
2
|
+
|
|
3
|
+
A React component for e-learning that lets users drag and drop items to arrange them in the correct order. Built with **@dnd-kit/sortable** for reordering and **pure CSS** for transitions and transforms.
|
|
4
|
+
|
|
5
|
+
## File structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
SortingActivity/
|
|
9
|
+
├── index.tsx # Component, types, and exports
|
|
10
|
+
├── style.module.css # CSS Modules styles
|
|
11
|
+
└── README.md # This file
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Props
|
|
15
|
+
|
|
16
|
+
### `SortingActivityProps`
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Required | Description |
|
|
19
|
+
|------|------|----------|-------------|
|
|
20
|
+
| `items` | `SortableItem[]` | Yes | List of items to sort. Each item has an `id` and `content`. |
|
|
21
|
+
| `correctOrder` | `string[]` | Yes | Array of item IDs in the correct sequence (in order). |
|
|
22
|
+
| `onComplete` | `(result: { isCorrect: boolean }) => void` | Yes | Callback invoked when "Check Answer" is clicked. Receives whether the user's order matches `correctOrder`. |
|
|
23
|
+
|
|
24
|
+
### `SortableItem`
|
|
25
|
+
|
|
26
|
+
| Field | Type | Description |
|
|
27
|
+
|-------|------|-------------|
|
|
28
|
+
| `id` | `string` | Unique identifier for the item (used for ordering and feedback). |
|
|
29
|
+
| `content` | `string` | Text displayed on the item. |
|
|
30
|
+
|
|
31
|
+
## Functionality
|
|
32
|
+
|
|
33
|
+
- **Random initial order**: Items are shuffled when the component mounts.
|
|
34
|
+
- **Drag and drop**: Users can reorder items using mouse or touch; reordering uses @dnd-kit/sortable.
|
|
35
|
+
- **Check Answer**: Compares the current order to `correctOrder` and shows visual feedback:
|
|
36
|
+
- Correct: all items highlighted green; success message shown.
|
|
37
|
+
- Incorrect: each item marked correct (green) or incorrect (red); error message shown.
|
|
38
|
+
- **Reset**: Restores the list to its initial shuffled order and clears feedback.
|
|
39
|
+
- **onComplete**: Called when "Check Answer" is clicked with `{ isCorrect: boolean }`.
|
|
40
|
+
|
|
41
|
+
## Styling and animations
|
|
42
|
+
|
|
43
|
+
- **CSS Modules**: All styles live in `style.module.css`; class names are scoped.
|
|
44
|
+
- **Transitions**: Hover and drag states use CSS transitions (transform, border-color, background, box-shadow).
|
|
45
|
+
- **Feedback animation**: Correct/incorrect feedback uses a short scale + fade-in animation.
|
|
46
|
+
- **Message animation**: Feedback message uses a slide-in animation.
|
|
47
|
+
|
|
48
|
+
## Usage example
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import React from 'react';
|
|
52
|
+
import SortingActivity, {
|
|
53
|
+
type SortableItem,
|
|
54
|
+
type SortingActivityProps,
|
|
55
|
+
} from './SortingActivity';
|
|
56
|
+
|
|
57
|
+
const items: SortableItem[] = [
|
|
58
|
+
{ id: '1', content: 'First step: Gather materials' },
|
|
59
|
+
{ id: '2', content: 'Second step: Prepare the workspace' },
|
|
60
|
+
{ id: '3', content: 'Third step: Assemble the parts' },
|
|
61
|
+
{ id: '4', content: 'Fourth step: Test and verify' },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const correctOrder = ['1', '2', '3', '4'];
|
|
65
|
+
|
|
66
|
+
function App() {
|
|
67
|
+
const handleComplete: SortingActivityProps['onComplete'] = (result) => {
|
|
68
|
+
console.log('Answer correct:', result.isCorrect);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div style={{ padding: '2rem', maxWidth: 500 }}>
|
|
73
|
+
<SortingActivity
|
|
74
|
+
items={items}
|
|
75
|
+
correctOrder={correctOrder}
|
|
76
|
+
onComplete={handleComplete}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
In this example, the component shows four steps in a random order. Users drag items to arrange them, click "Check Answer" to see feedback, and "Reset" to restore the initial order.
|
|
84
|
+
|
|
85
|
+
## Dependencies
|
|
86
|
+
|
|
87
|
+
- **React** 18.x
|
|
88
|
+
- **@dnd-kit/core**
|
|
89
|
+
- **@dnd-kit/sortable**
|
|
90
|
+
- **@dnd-kit/utilities**
|
|
91
|
+
|
|
92
|
+
## Accessibility
|
|
93
|
+
|
|
94
|
+
- List has `role="list"`.
|
|
95
|
+
- Buttons use `aria-label` where appropriate.
|
|
96
|
+
- Feedback message has `role="status"` and `aria-live="polite"` for screen readers.
|