catchup-library-web 1.20.35 → 1.20.36
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/index.js +626 -598
- package/dist/index.mjs +540 -512
- package/package.json +1 -1
- package/src/components/activities/material-contents/GroupingActivityMaterialContent.tsx +291 -203
- package/src/components/activities/material-contents/GroupingActivityMaterialContent2.tsx +362 -0
- package/src/components/activities/material-contents/MatchingActivityMaterialContent.tsx +246 -172
- package/src/components/activities/material-contents/MatchingActivityMaterialContent2.tsx +350 -0
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { useDrop } from "react-dnd";
|
|
3
2
|
import ShowMaterialMediaByContentType from "./ShowMaterialMediaByContentType";
|
|
4
3
|
import { InlineMath } from "react-katex";
|
|
5
4
|
import { constructInputWithSpecialExpressionList } from "../../../utilization/CatchtivityUtilization";
|
|
6
5
|
import { IMatchingActivityMaterialProps } from "../../../properties/ActivityProperties";
|
|
7
|
-
import DraggableItem from "../../dnds/DraggableItem";
|
|
8
|
-
import DroppableItem from "../../dnds/DroppableItem";
|
|
9
6
|
import DividerLine from "../../dividers/DividerLine";
|
|
10
7
|
|
|
11
8
|
const MatchingActivityMaterialContent = ({
|
|
@@ -18,17 +15,18 @@ const MatchingActivityMaterialContent = ({
|
|
|
18
15
|
isPreview,
|
|
19
16
|
showCorrectAnswer,
|
|
20
17
|
}: IMatchingActivityMaterialProps) => {
|
|
21
|
-
const [selectedValue, setSelectedValue] = useState(null);
|
|
22
|
-
const [
|
|
18
|
+
const [selectedValue, setSelectedValue] = useState<string | null>(null);
|
|
19
|
+
const [draggedValue, setDraggedValue] = useState<string | null>(null);
|
|
20
|
+
const [dropTargetKey, setDropTargetKey] = useState<string | null>(null);
|
|
21
|
+
const [draggedElement, setDraggedElement] = useState<HTMLElement | null>(
|
|
22
|
+
null
|
|
23
|
+
);
|
|
23
24
|
const [isShuffled, setIsShuffled] = useState(false);
|
|
24
|
-
const [shuffledMaterialList, setShuffledMaterialList] = useState([]);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
isOver: monitor.isOver(),
|
|
30
|
-
canDrop: monitor.canDrop(),
|
|
31
|
-
}),
|
|
25
|
+
const [shuffledMaterialList, setShuffledMaterialList] = useState<any[]>([]);
|
|
26
|
+
const dragElementRef = useRef<HTMLDivElement>(null);
|
|
27
|
+
const [touchPosition, setTouchPosition] = useState<{ x: number; y: number }>({
|
|
28
|
+
x: 0,
|
|
29
|
+
y: 0,
|
|
32
30
|
});
|
|
33
31
|
const itemsRef = useRef<HTMLDivElement>(null);
|
|
34
32
|
|
|
@@ -50,32 +48,20 @@ const MatchingActivityMaterialContent = ({
|
|
|
50
48
|
materialList.push(materialMap[materialKey]);
|
|
51
49
|
});
|
|
52
50
|
setShuffledMaterialList(shuffleArray(materialList));
|
|
53
|
-
}, []);
|
|
51
|
+
}, [materialMap, isShuffled]);
|
|
54
52
|
|
|
55
53
|
useEffect(() => {
|
|
56
54
|
if (!showCorrectAnswer) return;
|
|
57
55
|
answer.data.find(
|
|
58
56
|
(answerData: any) => answerData.type === "MATCHING"
|
|
59
57
|
).answerMap = materialMap;
|
|
60
|
-
}, [showCorrectAnswer]);
|
|
58
|
+
}, [showCorrectAnswer, answer, materialMap]);
|
|
61
59
|
|
|
62
60
|
const retrieveAnswerMap = () => {
|
|
63
61
|
const foundIndex = answer.data.findIndex(
|
|
64
62
|
(answerData: any) => answerData.type === "MATCHING"
|
|
65
63
|
);
|
|
66
64
|
return answer.data[foundIndex].answerMap;
|
|
67
|
-
// const sortedAnswerMapKeys = Object.keys(answerMap).sort((a, b) =>
|
|
68
|
-
// answerMap[a]
|
|
69
|
-
// ? answerMap[b]
|
|
70
|
-
// ? answerMap[a].localeCompare(answerMap[b])
|
|
71
|
-
// : 1
|
|
72
|
-
// : -1
|
|
73
|
-
// );
|
|
74
|
-
// const sortedAnswerMap: any = {};
|
|
75
|
-
// for (const answerMapKey of sortedAnswerMapKeys) {
|
|
76
|
-
// sortedAnswerMap[answerMapKey] = answerMap[answerMapKey];
|
|
77
|
-
// }
|
|
78
|
-
// return sortedAnswerMap;
|
|
79
65
|
};
|
|
80
66
|
|
|
81
67
|
const retrieveFilteredMaterialList = (answerMap: any) => {
|
|
@@ -100,12 +86,79 @@ const MatchingActivityMaterialContent = ({
|
|
|
100
86
|
return "INCORRECT";
|
|
101
87
|
};
|
|
102
88
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
89
|
+
// Mouse drag handlers
|
|
90
|
+
const handleMouseDown = (
|
|
91
|
+
e: React.MouseEvent,
|
|
92
|
+
materialValue: string
|
|
93
|
+
): void => {
|
|
94
|
+
if (!checkCanAnswerQuestion()) return;
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
setDraggedValue(materialValue);
|
|
97
|
+
setSelectedValue(null);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleMouseUp = (): void => {
|
|
101
|
+
if (dropTargetKey !== null && draggedValue !== null) {
|
|
102
|
+
onChange(answer, dropTargetKey, draggedValue);
|
|
103
|
+
}
|
|
104
|
+
setDraggedValue(null);
|
|
105
|
+
setDropTargetKey(null);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Touch drag handlers
|
|
109
|
+
const handleTouchStart = (
|
|
110
|
+
e: React.TouchEvent,
|
|
111
|
+
materialValue: string,
|
|
112
|
+
element: HTMLElement
|
|
113
|
+
): void => {
|
|
114
|
+
if (!checkCanAnswerQuestion()) return;
|
|
115
|
+
const touch = e.touches[0];
|
|
116
|
+
setDraggedValue(materialValue);
|
|
117
|
+
setDraggedElement(element);
|
|
118
|
+
setTouchPosition({ x: touch.clientX, y: touch.clientY });
|
|
119
|
+
setSelectedValue(null);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleTouchMove = (e: React.TouchEvent): void => {
|
|
123
|
+
if (!draggedValue) return;
|
|
124
|
+
|
|
125
|
+
const touch = e.touches[0];
|
|
126
|
+
setTouchPosition({ x: touch.clientX, y: touch.clientY });
|
|
127
|
+
|
|
128
|
+
// Find the element under the touch point
|
|
129
|
+
const elementUnder = document.elementFromPoint(
|
|
130
|
+
touch.clientX,
|
|
131
|
+
touch.clientY
|
|
132
|
+
);
|
|
133
|
+
const dropZone = elementUnder?.closest("[data-matching-drop-zone]");
|
|
134
|
+
|
|
135
|
+
if (dropZone) {
|
|
136
|
+
const dropKey = dropZone.getAttribute("data-matching-drop-zone");
|
|
137
|
+
setDropTargetKey(dropKey);
|
|
138
|
+
} else {
|
|
139
|
+
setDropTargetKey(null);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleTouchEnd = (): void => {
|
|
144
|
+
if (dropTargetKey !== null && draggedValue !== null) {
|
|
145
|
+
onChange(answer, dropTargetKey, draggedValue);
|
|
146
|
+
}
|
|
147
|
+
setDraggedValue(null);
|
|
148
|
+
setDropTargetKey(null);
|
|
149
|
+
setDraggedElement(null);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Click/tap to select (for easier mobile interaction)
|
|
153
|
+
const handleSelectItem = (materialValue: string): void => {
|
|
154
|
+
if (!checkCanAnswerQuestion()) return;
|
|
155
|
+
setSelectedValue(materialValue);
|
|
156
|
+
setDraggedValue(null);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleDropZoneClick = (answerMapKey: string): void => {
|
|
160
|
+
if (selectedValue !== null) {
|
|
161
|
+
onChange(answer, answerMapKey, selectedValue);
|
|
109
162
|
setSelectedValue(null);
|
|
110
163
|
}
|
|
111
164
|
};
|
|
@@ -114,7 +167,55 @@ const MatchingActivityMaterialContent = ({
|
|
|
114
167
|
const filteredMaterialList = retrieveFilteredMaterialList(answerMap);
|
|
115
168
|
|
|
116
169
|
return (
|
|
117
|
-
|
|
170
|
+
<div onMouseUp={handleMouseUp}>
|
|
171
|
+
{/* Floating drag preview for touch */}
|
|
172
|
+
{draggedValue && touchPosition.x > 0 && (
|
|
173
|
+
<div
|
|
174
|
+
className="fixed pointer-events-none z-50 opacity-80"
|
|
175
|
+
style={{
|
|
176
|
+
left: `${touchPosition.x}px`,
|
|
177
|
+
top: `${touchPosition.y}px`,
|
|
178
|
+
transform: "translate(-50%, -50%)",
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
{contentMap.type === "TEXT" ? (
|
|
182
|
+
<div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
|
|
183
|
+
<div className="flex flex-col items-center justify-center m-2 min-w-[200px] px-4">
|
|
184
|
+
<p className="text-lg whitespace-pre-wrap">
|
|
185
|
+
{constructInputWithSpecialExpressionList(draggedValue).map(
|
|
186
|
+
(inputPart, index) => (
|
|
187
|
+
<span
|
|
188
|
+
key={index}
|
|
189
|
+
className={`${inputPart.isBold ? "font-bold" : ""} ${
|
|
190
|
+
inputPart.isUnderline ? "underline" : ""
|
|
191
|
+
}`}
|
|
192
|
+
>
|
|
193
|
+
{inputPart.isEquation ? (
|
|
194
|
+
<span className="text-xl">
|
|
195
|
+
<InlineMath math={inputPart.value} />
|
|
196
|
+
</span>
|
|
197
|
+
) : (
|
|
198
|
+
inputPart.value
|
|
199
|
+
)}
|
|
200
|
+
</span>
|
|
201
|
+
)
|
|
202
|
+
)}
|
|
203
|
+
</p>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
) : (
|
|
207
|
+
<div className="border-catchup-blue border-2 rounded-catchup-xlarge bg-white shadow-lg">
|
|
208
|
+
<ShowMaterialMediaByContentType
|
|
209
|
+
key={`${uniqueValue}-drag`}
|
|
210
|
+
contentType={contentMap.type}
|
|
211
|
+
src={draggedValue}
|
|
212
|
+
canFullScreen={false}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
|
|
118
219
|
{filteredMaterialList.length > 0 ? (
|
|
119
220
|
<>
|
|
120
221
|
<div
|
|
@@ -122,84 +223,68 @@ const MatchingActivityMaterialContent = ({
|
|
|
122
223
|
className="flex-shrink-0 flex flex-row gap-x-4 gap-y-4 overflow-x-auto py-2"
|
|
123
224
|
>
|
|
124
225
|
{filteredMaterialList.map((materialValue, index) => (
|
|
125
|
-
<
|
|
226
|
+
<div
|
|
126
227
|
key={index}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
: "h-catchup-activity-media-box-item"
|
|
139
|
-
} flex flex-col items-center justify-center border-2 rounded-catchup-xlarge cursor-pointer transition-all duration-300`}
|
|
140
|
-
onMouseDown={() => {
|
|
141
|
-
if (checkCanAnswerQuestion()) {
|
|
142
|
-
setSelectedValue(materialValue);
|
|
143
|
-
}
|
|
144
|
-
}}
|
|
145
|
-
onTouchEnd={() => {
|
|
146
|
-
if (checkCanAnswerQuestion()) {
|
|
147
|
-
setSelectedValue(materialValue);
|
|
148
|
-
}
|
|
149
|
-
}}
|
|
150
|
-
onMouseUp={() => {
|
|
151
|
-
if (checkCanAnswerQuestion()) {
|
|
152
|
-
setSelectedValue(null);
|
|
153
|
-
}
|
|
154
|
-
}}
|
|
155
|
-
onTouchStart={() => {
|
|
156
|
-
if (checkCanAnswerQuestion()) {
|
|
157
|
-
setSelectedValue(null);
|
|
158
|
-
}
|
|
159
|
-
}}
|
|
160
|
-
>
|
|
161
|
-
{contentMap.type === "TEXT" ? (
|
|
162
|
-
<div className="flex flex-col items-center justify-center m-2 min-w-[200px] overflow-y-auto px-4">
|
|
163
|
-
<p className="text-lg whitespace-pre-wrap">
|
|
164
|
-
{constructInputWithSpecialExpressionList(
|
|
165
|
-
materialValue
|
|
166
|
-
).map((inputPart, index) => (
|
|
167
|
-
<span
|
|
168
|
-
key={index}
|
|
169
|
-
className={`${
|
|
170
|
-
inputPart.isBold ? "font-bold" : ""
|
|
171
|
-
} ${inputPart.isUnderline ? "underline" : ""}`}
|
|
172
|
-
>
|
|
173
|
-
{inputPart.isEquation ? (
|
|
174
|
-
<span className="text-xl">
|
|
175
|
-
<InlineMath math={inputPart.value} />
|
|
176
|
-
</span>
|
|
177
|
-
) : (
|
|
178
|
-
inputPart.value
|
|
179
|
-
)}
|
|
180
|
-
</span>
|
|
181
|
-
))}
|
|
182
|
-
</p>
|
|
183
|
-
</div>
|
|
184
|
-
) : (
|
|
185
|
-
<ShowMaterialMediaByContentType
|
|
186
|
-
key={`${uniqueValue}-${index}`}
|
|
187
|
-
contentType={contentMap.type}
|
|
188
|
-
src={materialValue}
|
|
189
|
-
canFullScreen={true}
|
|
190
|
-
/>
|
|
191
|
-
)}
|
|
192
|
-
</div>
|
|
228
|
+
ref={draggedValue === materialValue ? dragElementRef : null}
|
|
229
|
+
className={`${
|
|
230
|
+
draggedValue === materialValue
|
|
231
|
+
? "opacity-40"
|
|
232
|
+
: selectedValue === materialValue
|
|
233
|
+
? "ring-2 ring-blue-500"
|
|
234
|
+
: "opacity-100"
|
|
235
|
+
} transition-all duration-200`}
|
|
236
|
+
onMouseDown={(e) => handleMouseDown(e, materialValue)}
|
|
237
|
+
onTouchStart={(e) =>
|
|
238
|
+
handleTouchStart(e, materialValue, e.currentTarget)
|
|
193
239
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
selectedValue
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
240
|
+
onTouchMove={handleTouchMove}
|
|
241
|
+
onTouchEnd={handleTouchEnd}
|
|
242
|
+
>
|
|
243
|
+
<div
|
|
244
|
+
className={`${
|
|
245
|
+
selectedValue === materialValue
|
|
246
|
+
? "border-catchup-blue"
|
|
247
|
+
: "border-catchup-lighter-gray"
|
|
248
|
+
} ${
|
|
249
|
+
contentMap.type === "TEXT"
|
|
250
|
+
? "h-catchup-activity-text-box-item"
|
|
251
|
+
: "h-catchup-activity-media-box-item"
|
|
252
|
+
} flex flex-col items-center justify-center border-2 rounded-catchup-xlarge cursor-pointer transition-all duration-300`}
|
|
253
|
+
onClick={() => handleSelectItem(materialValue)}
|
|
254
|
+
>
|
|
255
|
+
{contentMap.type === "TEXT" ? (
|
|
256
|
+
<div className="flex flex-col items-center justify-center m-2 min-w-[200px] overflow-y-auto px-4">
|
|
257
|
+
<p className="text-lg whitespace-pre-wrap">
|
|
258
|
+
{constructInputWithSpecialExpressionList(
|
|
259
|
+
materialValue
|
|
260
|
+
).map((inputPart, index) => (
|
|
261
|
+
<span
|
|
262
|
+
key={index}
|
|
263
|
+
className={`${
|
|
264
|
+
inputPart.isBold ? "font-bold" : ""
|
|
265
|
+
} ${inputPart.isUnderline ? "underline" : ""}`}
|
|
266
|
+
>
|
|
267
|
+
{inputPart.isEquation ? (
|
|
268
|
+
<span className="text-xl">
|
|
269
|
+
<InlineMath math={inputPart.value} />
|
|
270
|
+
</span>
|
|
271
|
+
) : (
|
|
272
|
+
inputPart.value
|
|
273
|
+
)}
|
|
274
|
+
</span>
|
|
275
|
+
))}
|
|
276
|
+
</p>
|
|
277
|
+
</div>
|
|
278
|
+
) : (
|
|
279
|
+
<ShowMaterialMediaByContentType
|
|
280
|
+
key={`${uniqueValue}-${index}`}
|
|
281
|
+
contentType={contentMap.type}
|
|
282
|
+
src={materialValue}
|
|
283
|
+
canFullScreen={true}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
203
288
|
))}
|
|
204
289
|
</div>
|
|
205
290
|
<div className="flex-shrink-0">
|
|
@@ -260,11 +345,15 @@ const MatchingActivityMaterialContent = ({
|
|
|
260
345
|
<div className="mx-4 w-[2px] bg-catchup-lighter-gray"></div>
|
|
261
346
|
<div className="flex-1">
|
|
262
347
|
<div
|
|
348
|
+
data-matching-drop-zone={answerMapKey}
|
|
349
|
+
onMouseEnter={() =>
|
|
350
|
+
draggedValue && setDropTargetKey(answerMapKey)
|
|
351
|
+
}
|
|
352
|
+
onMouseLeave={() => setDropTargetKey(null)}
|
|
353
|
+
onClick={() => handleDropZoneClick(answerMapKey)}
|
|
263
354
|
className={`${
|
|
264
|
-
|
|
265
|
-
?
|
|
266
|
-
? "bg-catchup-light-blue"
|
|
267
|
-
: "bg-catchup-light-blue opacity-40"
|
|
355
|
+
dropTargetKey === answerMapKey
|
|
356
|
+
? "bg-catchup-light-blue ring-2 ring-blue-400"
|
|
268
357
|
: ""
|
|
269
358
|
} ${
|
|
270
359
|
contentMap.type === "TEXT"
|
|
@@ -279,71 +368,56 @@ const MatchingActivityMaterialContent = ({
|
|
|
279
368
|
? "border-catchup-red"
|
|
280
369
|
: "border-catchup-blue"
|
|
281
370
|
}`}
|
|
282
|
-
onClick={() => {
|
|
283
|
-
if (checkCanAnswerQuestion()) {
|
|
284
|
-
setSelectedValue(null);
|
|
285
|
-
}
|
|
286
|
-
}}
|
|
287
371
|
>
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
<ShowMaterialMediaByContentType
|
|
331
|
-
key={`${uniqueValue}-${index}`}
|
|
332
|
-
contentType={contentMap.type}
|
|
333
|
-
src={answerMap[answerMapKey]}
|
|
334
|
-
canFullScreen={false}
|
|
335
|
-
/>
|
|
336
|
-
)}
|
|
337
|
-
</div>
|
|
338
|
-
}
|
|
339
|
-
/>
|
|
372
|
+
<div
|
|
373
|
+
className="h-full flex-1 flex flex-row items-center justify-center px-4"
|
|
374
|
+
onClick={(e) => {
|
|
375
|
+
e.stopPropagation();
|
|
376
|
+
if (checkCanAnswerQuestion() && answerMap[answerMapKey]) {
|
|
377
|
+
onChange(answer, answerMapKey, null);
|
|
378
|
+
setSelectedValue(null);
|
|
379
|
+
}
|
|
380
|
+
}}
|
|
381
|
+
>
|
|
382
|
+
{answerMap[answerMapKey] ? (
|
|
383
|
+
contentMap.type === "TEXT" ? (
|
|
384
|
+
<p className="text-lg whitespace-pre-wrap">
|
|
385
|
+
{constructInputWithSpecialExpressionList(
|
|
386
|
+
answerMap[answerMapKey]
|
|
387
|
+
).map((inputPart, index) => (
|
|
388
|
+
<span
|
|
389
|
+
key={index}
|
|
390
|
+
className={`${
|
|
391
|
+
inputPart.isBold ? "font-bold" : ""
|
|
392
|
+
} ${inputPart.isUnderline ? "underline" : ""}`}
|
|
393
|
+
>
|
|
394
|
+
{inputPart.isEquation ? (
|
|
395
|
+
<span className="text-xl">
|
|
396
|
+
<InlineMath math={inputPart.value} />
|
|
397
|
+
</span>
|
|
398
|
+
) : (
|
|
399
|
+
inputPart.value
|
|
400
|
+
)}
|
|
401
|
+
</span>
|
|
402
|
+
))}
|
|
403
|
+
</p>
|
|
404
|
+
) : (
|
|
405
|
+
<ShowMaterialMediaByContentType
|
|
406
|
+
key={`${uniqueValue}-${index}`}
|
|
407
|
+
contentType={contentMap.type}
|
|
408
|
+
src={answerMap[answerMapKey]}
|
|
409
|
+
canFullScreen={false}
|
|
410
|
+
/>
|
|
411
|
+
)
|
|
412
|
+
) : null}
|
|
413
|
+
</div>
|
|
340
414
|
</div>
|
|
341
415
|
</div>
|
|
342
416
|
</div>
|
|
343
417
|
);
|
|
344
418
|
})}
|
|
345
419
|
</div>
|
|
346
|
-
|
|
420
|
+
</div>
|
|
347
421
|
);
|
|
348
422
|
};
|
|
349
423
|
|