@nualang/nualang-ui-components 0.1.1418 → 0.1.1420

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.
@@ -28,6 +28,10 @@ const Transition = /*#__PURE__*/forwardRef(function Transition(props, ref) {
28
28
  export default function ResponsiveDialog({
29
29
  open,
30
30
  handleClose,
31
+ // Backdrop-click / Escape handler. Defaults to `handleClose`, but can be
32
+ // supplied on its own so a dialog can be dismissable without rendering the
33
+ // built-in footer Close button (which `handleClose` drives).
34
+ onClose,
31
35
  handleSubmit,
32
36
  t = text => text,
33
37
  submitText,
@@ -62,7 +66,7 @@ export default function ResponsiveDialog({
62
66
  fullWidth: fullWidth,
63
67
  maxWidth: maxWidth,
64
68
  open: open,
65
- onClose: handleClose,
69
+ onClose: onClose || handleClose,
66
70
  "aria-label": dialogTitle,
67
71
  transitionDuration: transitionDuration,
68
72
  keepMounted: keepDialogMounted,
@@ -1,46 +1,421 @@
1
- import { useState } from "react";
1
+ import { useState, useMemo, useEffect } from "react";
2
+ import { Box, Typography, Button } from "@mui/material";
3
+ import { makeStyles } from "tss-react/mui";
4
+ import MenuBookIcon from "@mui/icons-material/MenuBook";
5
+ import ForumIcon from "@mui/icons-material/Forum";
6
+ import PlaylistAddCheckIcon from "@mui/icons-material/PlaylistAddCheck";
7
+ import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
8
+ import HearingIcon from "@mui/icons-material/Hearing";
9
+ import ExtensionIcon from "@mui/icons-material/Extension";
10
+ import EditNoteIcon from "@mui/icons-material/EditNote";
11
+ import LiveTvIcon from "@mui/icons-material/LiveTv";
12
+ import AutoStoriesIcon from "@mui/icons-material/AutoStories";
2
13
  import CourseSelection from "../../Lists/CourseSelection/CourseSelection";
3
14
  import ResponsiveDialog from "../ResponsiveDialog/ResponsiveDialog";
4
- import { Box } from "@mui/system";
5
- import { DialogContentText } from "@mui/material";
6
15
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
+ const useStyles = makeStyles()(() => ({
17
+ content: {
18
+ padding: 0
19
+ }
20
+ }));
21
+ const GAME_TYPES = [{
22
+ id: "translation",
23
+ labelKey: "translation",
24
+ Icon: SwapHorizIcon,
25
+ needsDefinitions: false
26
+ }, {
27
+ id: "listening",
28
+ labelKey: "listening",
29
+ Icon: HearingIcon,
30
+ needsDefinitions: false
31
+ }, {
32
+ id: "meaning",
33
+ labelKey: "meaning",
34
+ Icon: ExtensionIcon,
35
+ needsDefinitions: true
36
+ }, {
37
+ id: "meaning-read-and-write",
38
+ labelKey: "meaning_read_write",
39
+ Icon: EditNoteIcon,
40
+ needsDefinitions: true
41
+ }];
42
+
43
+ // The single game mode for scenarios, shown in the footer like the phrase-list
44
+ // exercise toggle.
45
+ const STORY_GAME_TYPE = {
46
+ id: "roleplay-story",
47
+ labelKey: "roleplay-story",
48
+ Icon: AutoStoriesIcon
49
+ };
50
+
51
+ // Fisher-Yates in-place shuffle (inlined to avoid pulling the utils barrel,
52
+ // which imports RiveScript, into the dialog bundle).
53
+ function shuffleInPlace(array) {
54
+ for (let i = array.length - 1; i > 0; i -= 1) {
55
+ const j = Math.floor(Math.random() * (i + 1));
56
+ [array[i], array[j]] = [array[j], array[i]];
57
+ }
58
+ return array;
59
+ }
60
+
61
+ // Merge every selected list's phrases into one pool, tagging each phrase with
62
+ // its source list so the game can later draw evenly across lists, and dropping
63
+ // exact-duplicate phrase strings.
64
+ function dedupeMergedPhrases(lists) {
65
+ const seen = new Set();
66
+ const out = [];
67
+ lists.forEach(list => {
68
+ (list.phrases || []).forEach(p => {
69
+ const key = (p?.phrase || "").trim().toLowerCase();
70
+ if (key && seen.has(key)) return;
71
+ if (key) seen.add(key);
72
+ out.push({
73
+ ...p,
74
+ sourceListId: list.phraseListId
75
+ });
76
+ });
77
+ });
78
+ return out;
79
+ }
80
+ function mergePhrases(lists) {
81
+ // Shuffle the merged pool by default so lists interleave rather than play in
82
+ // per-list blocks (the waiting-room game start reshuffles too).
83
+ return shuffleInPlace(dedupeMergedPhrases(lists));
84
+ }
7
85
  function SelectExercise({
8
86
  t = text => text,
9
87
  open,
10
88
  handleClose,
11
- courses,
89
+ courses = [],
12
90
  getCourseSections,
13
- getRoleplays,
14
91
  getPhraseLists,
15
92
  handleCreateGame,
16
93
  username,
17
- courseSectionTopicId = ""
94
+ courseSectionTopicId = "",
95
+ initialSourceType = "phrases"
18
96
  }) {
19
- const [isExerciseSelected, setIsExerciseSelected] = useState(false);
97
+ const {
98
+ classes
99
+ } = useStyles();
100
+ const [sourceType, setSourceType] = useState(initialSourceType);
101
+ // Map<phraseListId, enrichedList> — keeps the phrase counts and definition
102
+ // flags without re-fetching, order-independent (the pool is shuffled anyway).
103
+ const [selectedLists, setSelectedLists] = useState(() => new Map());
104
+ const [selectedScenario, setSelectedScenario] = useState(null);
105
+ const [gameType, setGameType] = useState("translation");
106
+
107
+ // Selection is one-off per game; reset every time the dialog opens.
108
+ useEffect(() => {
109
+ if (open) {
110
+ setSourceType(initialSourceType);
111
+ setSelectedLists(new Map());
112
+ setSelectedScenario(null);
113
+ setGameType("translation");
114
+ }
115
+ }, [open]);
116
+ const isPhrases = sourceType === "phrases";
117
+ const togglePhraseList = list => {
118
+ setSelectedLists(prev => {
119
+ const next = new Map(prev);
120
+ if (next.has(list.phraseListId)) next.delete(list.phraseListId);else next.set(list.phraseListId, list);
121
+ return next;
122
+ });
123
+ };
124
+ const toggleTopicAll = lists => {
125
+ setSelectedLists(prev => {
126
+ const next = new Map(prev);
127
+ const allSelected = lists.every(l => next.has(l.phraseListId));
128
+ lists.forEach(l => {
129
+ if (allSelected) next.delete(l.phraseListId);else next.set(l.phraseListId, l);
130
+ });
131
+ return next;
132
+ });
133
+ };
134
+ const selectedListsArr = useMemo(() => [...selectedLists.values()], [selectedLists]);
135
+ // Count the deduped merged pool (matching what actually gets played) when the
136
+ // phrases are loaded; otherwise fall back to the sum of the lists' counts.
137
+ const totalPhrases = useMemo(() => {
138
+ const allHavePhrases = selectedListsArr.every(l => Array.isArray(l.phrases));
139
+ return allHavePhrases ? dedupeMergedPhrases(selectedListsArr).length : selectedListsArr.reduce((n, l) => n + (l.phraseCount || 0), 0);
140
+ }, [selectedListsArr]);
141
+ const allHaveDefinitions = selectedListsArr.length > 0 && selectedListsArr.every(l => l.hasDefinitions);
142
+ const availableGameTypes = GAME_TYPES.filter(g => !g.needsDefinitions || allHaveDefinitions);
143
+ const effectiveGameType = availableGameTypes.some(g => g.id === gameType) ? gameType : availableGameTypes[0]?.id;
144
+
145
+ // Footer exercise segments + count are mode-aware: phrase lists show the game
146
+ // types and "n lists · n phrases"; scenarios show a single Story mode and the
147
+ // selected scenario's message count.
148
+ const footerGameTypes = isPhrases ? availableGameTypes : [STORY_GAME_TYPE];
149
+ const activeFooterGameType = isPhrases ? effectiveGameType : STORY_GAME_TYPE.id;
150
+ const canPlay = isPhrases ? selectedListsArr.length > 0 : !!selectedScenario;
151
+ const handlePlay = () => {
152
+ if (!canPlay) return;
153
+ if (isPhrases) {
154
+ const first = selectedListsArr[0];
155
+ handleCreateGame({
156
+ courseId: first.courseId,
157
+ courseSectionTopicId: first.courseSectionTopicId,
158
+ exercise: effectiveGameType,
159
+ phraseListIds: selectedListsArr.map(l => l.phraseListId),
160
+ // Hydrated list metadata for the waiting room's source strip.
161
+ phraseLists: selectedListsArr.map(l => ({
162
+ phraseListId: l.phraseListId,
163
+ phraseListName: l.phraseListName,
164
+ picture: l.picture || null,
165
+ phraseCount: l.phraseCount,
166
+ topicName: l.topicName ?? "",
167
+ courseSectionTopicId: l.courseSectionTopicId ?? null,
168
+ topicVideoUrl: l.topicVideoUrl ?? null
169
+ })),
170
+ phrases: mergePhrases(selectedListsArr),
171
+ shuffle: true,
172
+ timer: 30,
173
+ answerOption: 0,
174
+ topicName: first.topicName ?? ""
175
+ });
176
+ } else {
177
+ handleCreateGame({
178
+ courseSectionTopicId: selectedScenario.courseSectionTopicId,
179
+ exercise: "roleplay-story",
180
+ roleplayId: selectedScenario.roleplayId,
181
+ timer: 30,
182
+ answerOption: 0,
183
+ topicName: selectedScenario.topicName ?? ""
184
+ });
185
+ }
186
+ };
187
+ const sourceTab = active => ({
188
+ display: "flex",
189
+ alignItems: "center",
190
+ gap: 0.75,
191
+ px: 2,
192
+ py: 1,
193
+ fontSize: 14,
194
+ fontWeight: 600,
195
+ cursor: "pointer",
196
+ ...(active ? {
197
+ color: "primary.contrastText",
198
+ backgroundColor: "primary.main"
199
+ } : {
200
+ color: "text.secondary",
201
+ backgroundColor: "background.paper"
202
+ })
203
+ });
204
+ const countLabel = isPhrases ? selectedListsArr.length ? t("lists_and_phrases_selected", {
205
+ lists: selectedListsArr.length,
206
+ phrases: totalPhrases,
207
+ count: selectedListsArr.length
208
+ }) : t("no_lists_selected") : selectedScenario ? t("scenario_selected") : t("no_scenario_selected");
20
209
  return /*#__PURE__*/_jsxs(ResponsiveDialog, {
21
210
  open: open,
22
- handleClose: handleClose,
211
+ onClose: handleClose,
23
212
  dialogTitle: t("play_live"),
24
213
  maxWidth: "md",
25
- children: [/*#__PURE__*/_jsx(DialogContentText, {
26
- children: t("live_dialog_description")
214
+ dialogContentClassName: classes.content,
215
+ children: [/*#__PURE__*/_jsx(Box, {
216
+ sx: {
217
+ display: "flex",
218
+ alignItems: "center",
219
+ justifyContent: "space-between",
220
+ gap: 3,
221
+ flexWrap: "wrap",
222
+ px: 3,
223
+ pt: 1
224
+ },
225
+ children: /*#__PURE__*/_jsxs(Box, {
226
+ sx: {
227
+ display: "flex",
228
+ border: "1px solid",
229
+ borderColor: "divider",
230
+ borderRadius: 1,
231
+ overflow: "hidden",
232
+ flex: "none"
233
+ },
234
+ children: [/*#__PURE__*/_jsxs(Box, {
235
+ role: "button",
236
+ tabIndex: 0,
237
+ "aria-pressed": isPhrases,
238
+ onClick: () => setSourceType("phrases"),
239
+ onKeyDown: e => {
240
+ if (e.key === "Enter" || e.key === " ") {
241
+ e.preventDefault();
242
+ setSourceType("phrases");
243
+ }
244
+ },
245
+ sx: sourceTab(isPhrases),
246
+ children: [/*#__PURE__*/_jsx(MenuBookIcon, {
247
+ sx: {
248
+ fontSize: 18
249
+ }
250
+ }), t("source_type_phrase_lists")]
251
+ }), /*#__PURE__*/_jsxs(Box, {
252
+ role: "button",
253
+ tabIndex: 0,
254
+ "aria-pressed": !isPhrases,
255
+ onClick: () => setSourceType("scenarios"),
256
+ onKeyDown: e => {
257
+ if (e.key === "Enter" || e.key === " ") {
258
+ e.preventDefault();
259
+ setSourceType("scenarios");
260
+ }
261
+ },
262
+ sx: sourceTab(!isPhrases),
263
+ children: [/*#__PURE__*/_jsx(ForumIcon, {
264
+ sx: {
265
+ fontSize: 18
266
+ }
267
+ }), t("source_type_scenarios")]
268
+ })]
269
+ })
27
270
  }), /*#__PURE__*/_jsx(Box, {
28
271
  sx: {
29
- mt: 1
272
+ mt: 2,
273
+ px: 3,
274
+ pb: 2.5,
275
+ minHeight: 280
30
276
  },
31
277
  children: /*#__PURE__*/_jsx(CourseSelection, {
32
278
  courses: courses,
33
279
  getCourseSections: getCourseSections,
34
- getRoleplays: getRoleplays,
35
280
  getPhraseLists: getPhraseLists,
36
281
  username: username,
37
- handleCreateGame: handleCreateGame,
38
282
  t: t,
39
- isExerciseSelected: isExerciseSelected,
40
- setIsExerciseSelected: setIsExerciseSelected,
41
283
  isDialogOpen: open,
284
+ sourceType: sourceType,
285
+ selectedListIds: selectedLists,
286
+ onTogglePhraseList: togglePhraseList,
287
+ onToggleTopicAll: toggleTopicAll,
288
+ selectedScenarioId: selectedScenario?.roleplayId || null,
289
+ onSelectScenario: setSelectedScenario,
42
290
  courseSectionTopicId: courseSectionTopicId
43
291
  })
292
+ }), /*#__PURE__*/_jsxs(Box, {
293
+ sx: {
294
+ position: "sticky",
295
+ bottom: 0,
296
+ borderTop: "1px solid",
297
+ borderColor: "divider",
298
+ backgroundColor: "background.default",
299
+ px: 3,
300
+ py: 1.5,
301
+ display: "flex",
302
+ flexDirection: "column",
303
+ gap: 1.5,
304
+ zIndex: 1
305
+ },
306
+ children: [/*#__PURE__*/_jsxs(Box, {
307
+ sx: {
308
+ display: "flex",
309
+ alignItems: "center",
310
+ gap: 2,
311
+ flexWrap: "wrap"
312
+ },
313
+ children: [/*#__PURE__*/_jsxs(Box, {
314
+ sx: {
315
+ display: "flex",
316
+ alignItems: "center",
317
+ gap: 1
318
+ },
319
+ children: [isPhrases ? /*#__PURE__*/_jsx(PlaylistAddCheckIcon, {
320
+ sx: {
321
+ fontSize: 20,
322
+ color: "primary.main"
323
+ }
324
+ }) : /*#__PURE__*/_jsx(ForumIcon, {
325
+ sx: {
326
+ fontSize: 20,
327
+ color: "primary.main"
328
+ }
329
+ }), /*#__PURE__*/_jsx(Typography, {
330
+ sx: {
331
+ fontSize: 14,
332
+ fontWeight: 600,
333
+ color: "text.primary"
334
+ },
335
+ children: countLabel
336
+ })]
337
+ }), /*#__PURE__*/_jsx(Box, {
338
+ sx: {
339
+ display: "flex",
340
+ alignItems: "center",
341
+ border: "1px solid",
342
+ borderColor: "divider",
343
+ borderRadius: 1,
344
+ overflow: "hidden",
345
+ backgroundColor: "background.paper"
346
+ },
347
+ children: footerGameTypes.map(g => {
348
+ const active = activeFooterGameType === g.id;
349
+ const {
350
+ Icon
351
+ } = g;
352
+ return /*#__PURE__*/_jsxs(Box, {
353
+ onClick: () => isPhrases && setGameType(g.id),
354
+ sx: {
355
+ display: "flex",
356
+ alignItems: "center",
357
+ gap: 0.75,
358
+ px: 1.5,
359
+ py: 1,
360
+ fontSize: 14,
361
+ cursor: isPhrases ? "pointer" : "default",
362
+ transition: "background-color 200ms cubic-bezier(0.4,0,0.2,1)",
363
+ ...(active ? {
364
+ color: "primary.contrastText",
365
+ backgroundColor: "primary.main",
366
+ fontWeight: 600,
367
+ ...(isPhrases ? {
368
+ "&:hover": {
369
+ backgroundColor: "primary.dark"
370
+ }
371
+ } : {})
372
+ } : {
373
+ color: "text.secondary",
374
+ "&:hover": {
375
+ backgroundColor: "action.hover"
376
+ }
377
+ })
378
+ },
379
+ children: [/*#__PURE__*/_jsx(Icon, {
380
+ sx: {
381
+ fontSize: 18
382
+ }
383
+ }), t(g.labelKey)]
384
+ }, g.id);
385
+ })
386
+ })]
387
+ }), /*#__PURE__*/_jsxs(Box, {
388
+ sx: {
389
+ display: "flex",
390
+ alignItems: "center",
391
+ justifyContent: "flex-end",
392
+ gap: 1
393
+ },
394
+ children: [/*#__PURE__*/_jsx(Button, {
395
+ onClick: handleClose,
396
+ color: "primary",
397
+ sx: {
398
+ fontWeight: 600
399
+ },
400
+ children: t("close")
401
+ }), /*#__PURE__*/_jsx(Button, {
402
+ variant: "contained",
403
+ onClick: handlePlay,
404
+ disabled: !canPlay,
405
+ startIcon: /*#__PURE__*/_jsx(LiveTvIcon, {}),
406
+ "data-cy": "play-live",
407
+ sx: {
408
+ fontWeight: 600,
409
+ borderRadius: "25px",
410
+ px: 2.25,
411
+ backgroundColor: "secondary.main",
412
+ "&:hover": {
413
+ backgroundColor: "secondary.dark"
414
+ }
415
+ },
416
+ children: t("play_live")
417
+ })]
418
+ })]
44
419
  })]
45
420
  });
46
421
  }
@@ -3,27 +3,23 @@ import { List, ListItem, ListItemButton, ListItemText, Collapse, Avatar, Box, To
3
3
  import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
4
4
  import ExpandLessIcon from "@mui/icons-material/ExpandLess";
5
5
  import ExerciseSelection from "../ExerciseSelection/ExerciseSelection";
6
+ import { topicHasScenario } from "../RoleplaySelection/RoleplaySelection";
6
7
  import { courses as courseQuerys } from "@nualang/nualang-api-and-queries/Queries";
7
8
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
9
  function Course({
9
10
  course,
10
11
  index,
11
12
  getCourseSections,
12
- getRoleplays,
13
13
  getPhraseLists,
14
14
  username,
15
- handleCreateGame,
16
15
  t,
17
- isExerciseSelected,
18
- setIsExerciseSelected,
19
16
  isDialogOpen,
20
- handleSelectExercise,
21
- selectedExercises,
22
- useCase,
23
- assignment,
24
- handleStartExercise,
25
- isAssignment,
26
- isCreator,
17
+ sourceType,
18
+ selectedListIds,
19
+ onTogglePhraseList,
20
+ onToggleTopicAll,
21
+ selectedScenarioId,
22
+ onSelectScenario,
27
23
  courseSectionTopicId
28
24
  }) {
29
25
  const [open, setOpen] = useState(false);
@@ -32,7 +28,7 @@ function Course({
32
28
  return response;
33
29
  }, {
34
30
  courseId: course.courseId,
35
- username: username,
31
+ username,
36
32
  filters: {
37
33
  limit: 1000
38
34
  }
@@ -40,12 +36,19 @@ function Course({
40
36
  enabled: !!course.courseId && isDialogOpen
41
37
  });
42
38
  useEffect(() => {
43
- if (courseSectionTopicId.split("|")[0] === course.courseId) {
39
+ if (courseSectionTopicId?.split("|")[0] === course.courseId) {
44
40
  setOpen(true);
45
41
  }
46
42
  }, [courseSectionTopicId]);
47
43
  const courseSections = useMemo(() => sectionsQuery.isSuccess && Array.isArray(sectionsQuery.data.Items) && sectionsQuery.data.Items.length ? sectionsQuery.data.Items : [], [sectionsQuery.data, sectionsQuery.isSuccess]);
48
- const isCourseEmpty = courseSections.length === 0 && !sectionsQuery.isLoading ? true : false;
44
+ const isCourseEmpty = courseSections.length === 0 && !sectionsQuery.isLoading;
45
+
46
+ // In scenarios mode, once this course's sections have loaded, hide the whole
47
+ // course if none of its topics have scenarios.
48
+ const hasScenarios = courseSections.some(sec => (sec.topics || []).some(tp => topicHasScenario(tp.roleplays)));
49
+ if (sourceType === "scenarios" && sectionsQuery.isSuccess && !hasScenarios) {
50
+ return null;
51
+ }
49
52
  return /*#__PURE__*/_jsxs(List, {
50
53
  disablePadding: true,
51
54
  children: [/*#__PURE__*/_jsx(Tooltip, {
@@ -54,7 +57,7 @@ function Course({
54
57
  disablePadding: true,
55
58
  children: /*#__PURE__*/_jsx(ListItemButton, {
56
59
  onClick: () => setOpen(!open),
57
- disabled: isCourseEmpty || isExerciseSelected,
60
+ disabled: isCourseEmpty,
58
61
  children: /*#__PURE__*/_jsxs(Box, {
59
62
  sx: {
60
63
  display: "flex",
@@ -79,11 +82,11 @@ function Course({
79
82
  })]
80
83
  }), open ? /*#__PURE__*/_jsx(ExpandLessIcon, {
81
84
  sx: {
82
- color: "#757575"
85
+ color: "text.secondary"
83
86
  }
84
87
  }) : /*#__PURE__*/_jsx(ExpandMoreIcon, {
85
88
  sx: {
86
- color: "#757575"
89
+ color: "text.secondary"
87
90
  }
88
91
  })]
89
92
  })
@@ -95,25 +98,20 @@ function Course({
95
98
  unmountOnExit: true,
96
99
  children: /*#__PURE__*/_jsx(Box, {
97
100
  sx: {
98
- ml: 3.5
101
+ ml: 7
99
102
  },
100
103
  children: /*#__PURE__*/_jsx(ExerciseSelection, {
101
104
  t: t,
102
105
  sections: courseSections,
103
106
  isLoading: sectionsQuery.isLoading,
104
- handleCreateGame: handleCreateGame,
105
107
  courseId: course.courseId,
106
- isExerciseSelected: isExerciseSelected,
107
- setIsExerciseSelected: setIsExerciseSelected,
108
- getRoleplays: getRoleplays,
109
108
  getPhraseLists: getPhraseLists,
110
- handleSelectExercise: handleSelectExercise,
111
- selectedExercises: selectedExercises,
112
- useCase: useCase,
113
- assignment: assignment,
114
- handleStartExercise: handleStartExercise,
115
- isAssignment: isAssignment,
116
- isCreator: isCreator,
109
+ sourceType: sourceType,
110
+ selectedListIds: selectedListIds,
111
+ onTogglePhraseList: onTogglePhraseList,
112
+ onToggleTopicAll: onToggleTopicAll,
113
+ selectedScenarioId: selectedScenarioId,
114
+ onSelectScenario: onSelectScenario,
117
115
  courseSectionTopicId: courseSectionTopicId
118
116
  })
119
117
  })
@@ -121,44 +119,35 @@ function Course({
121
119
  }, index);
122
120
  }
123
121
  function CourseSelection({
124
- courses,
122
+ courses = [],
125
123
  getCourseSections,
126
- getRoleplays,
127
124
  getPhraseLists,
128
125
  username,
129
- handleCreateGame,
130
126
  t,
131
- isExerciseSelected,
132
- setIsExerciseSelected,
133
- handleSelectExercise,
134
- selectedExercises,
135
- useCase,
136
- assignment,
137
- handleStartExercise,
138
- isAssignment,
139
- isCreator,
140
- courseSectionTopicId,
141
- ...otherProps
127
+ isDialogOpen,
128
+ sourceType,
129
+ selectedListIds,
130
+ onTogglePhraseList,
131
+ onToggleTopicAll,
132
+ selectedScenarioId,
133
+ onSelectScenario,
134
+ courseSectionTopicId
142
135
  }) {
143
136
  return /*#__PURE__*/_jsx("div", {
144
137
  children: courses.map((course, index) => /*#__PURE__*/_jsx(Course, {
145
138
  course: course,
146
139
  index: index,
147
140
  getCourseSections: getCourseSections,
148
- getRoleplays: getRoleplays,
149
141
  getPhraseLists: getPhraseLists,
150
142
  username: username,
151
- handleCreateGame: handleCreateGame,
152
143
  t: t,
153
- isExerciseSelected: isExerciseSelected,
154
- setIsExerciseSelected: setIsExerciseSelected,
155
- handleSelectExercise: handleSelectExercise,
156
- selectedExercises: selectedExercises,
157
- useCase: useCase,
158
- assignment: assignment,
159
- handleStartExercise: handleStartExercise,
160
- isAssignment: isAssignment,
161
- isCreator: isCreator,
144
+ isDialogOpen: isDialogOpen,
145
+ sourceType: sourceType,
146
+ selectedListIds: selectedListIds,
147
+ onTogglePhraseList: onTogglePhraseList,
148
+ onToggleTopicAll: onToggleTopicAll,
149
+ selectedScenarioId: selectedScenarioId,
150
+ onSelectScenario: onSelectScenario,
162
151
  courseSectionTopicId: courseSectionTopicId
163
152
  }, course.courseId))
164
153
  });
@@ -5,7 +5,7 @@ import CourseCard from "../../Cards/Course/Course";
5
5
  import useListScroll from "../../hooks/useListScroll";
6
6
  import { SingleLineGridList, SingleLineGridListItem, GridListItem } from "../ListElements";
7
7
  import { getStudentSortOrder, setStudentSortOrder } from "../../utils/studentSort";
8
- import { CLASSROOM_COURSES_SORT_ORDER_KEY, compareCoursesBySortOrder, compareCoursesByLastVisit, getCourseVisits, recordCourseVisit } from "../../utils/courseSort";
8
+ import { CLASSROOM_COURSES_SORT_ORDER_KEY, compareCoursesBySortOrder, compareCoursesByLastVisit, getCourseVisits, normalizeCourseSortOrder, recordCourseVisit } from "../../utils/courseSort";
9
9
 
10
10
  // import { propsAreEqual } from '../../utils';
11
11
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
@@ -165,7 +165,7 @@ function Courses({
165
165
  handleViewCourseTopic,
166
166
  ...otherProps
167
167
  }) {
168
- const [sortOrder, setSortOrder] = useState(() => getStudentSortOrder(CLASSROOM_COURSES_SORT_ORDER_KEY, "oldest"));
168
+ const [sortOrder, setSortOrder] = useState(() => normalizeCourseSortOrder(getStudentSortOrder(CLASSROOM_COURSES_SORT_ORDER_KEY, "default")));
169
169
  // Snapshot taken on mount so opening a course doesn't reshuffle the list
170
170
  // under the user mid-click; the fresh order shows on the way back in.
171
171
  const [visits, setVisits] = useState(getCourseVisits);
@@ -199,7 +199,7 @@ function Courses({
199
199
  // The courses arrive from Dynamo oldest-first, so the default order is
200
200
  // simply the array as given — the courses added to the classroom first
201
201
  // stay at the top.
202
- if (sortOrder === "oldest") return courses;
202
+ if (sortOrder === "default") return courses;
203
203
  if (sortOrder === "newest") return [...courses].reverse();
204
204
  if (sortOrder === "recentlyViewed") {
205
205
  // Sorting the default order means never-opened courses stay in it,
@@ -235,8 +235,8 @@ function Courses({
235
235
  label: t("sort_by"),
236
236
  autoWidth: true,
237
237
  children: [/*#__PURE__*/_jsx(MenuItem, {
238
- value: "oldest",
239
- children: t("oldest")
238
+ value: "default",
239
+ children: t("default")
240
240
  }), /*#__PURE__*/_jsx(MenuItem, {
241
241
  value: "newest",
242
242
  children: t("newest")