@nualang/nualang-ui-components 0.1.1419 → 0.1.1421

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.
@@ -1,9 +1,20 @@
1
1
  import { useState, useEffect } from "react";
2
- import { Box, Slider, Button, Typography } from "@mui/material";
2
+ import { Box, Slider, Button, Typography, Radio, Divider, IconButton } from "@mui/material";
3
3
  import SwipeableDrawer from "@mui/material/SwipeableDrawer";
4
+ import ScheduleIcon from "@mui/icons-material/Schedule";
4
5
  import useDebounce from "../../hooks/useDebounce";
5
- import { Grid, FormControl, FormLabel, FormControlLabel, RadioGroup, Radio } from "@mui/material";
6
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
6
+ import { selectedRowBg } from "../../Lists/RoleplaySelection/RoleplaySelection";
7
+
8
+ // Which answer methods each game type actually supports.
9
+ // 0 = keyboard + word bank, 1 = keyboard only (text), 2 = word bank only.
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ const ANSWER_MODES_BY_EXERCISE = {
12
+ translation: [0, 1, 2],
13
+ listening: [0, 1, 2],
14
+ meaning: [2],
15
+ // choose the phrase from tiles
16
+ "meaning-read-and-write": [1] // type the answer
17
+ };
7
18
  export default function GameSettings({
8
19
  t,
9
20
  isOpen,
@@ -18,30 +29,35 @@ export default function GameSettings({
18
29
  handleConfirm,
19
30
  numberOfQuestions,
20
31
  exercise,
21
- roleplay
32
+ roleplay,
33
+ phraseListCount
22
34
  }) {
23
35
  const [timer, setTimer] = useState(time);
24
36
  const debouncedTimer = useDebounce(timer, 500);
25
- const calculateEstimatedTime = (time, numberOfQuestions) => {
26
- const totalTimeInSeconds = time * numberOfQuestions;
27
- let totalTimeWithExtra = totalTimeInSeconds + numberOfQuestions * 10;
28
- if (exercise.includes("roleplay")) {
29
- totalTimeWithExtra = totalTimeWithExtra + roleplay?.script?.length * 10;
37
+ const isRoleplay = exercise.includes("roleplay");
38
+
39
+ // Live-scenario terminology: messages = the script's dialogue lines,
40
+ // interactions = the questions the student answers.
41
+ const roleplayScriptLength = roleplay?.script?.length ?? 0;
42
+ const numberOfRoleplayInteractions = isRoleplay ? roleplay?.script?.filter(item => Object.hasOwn(item, "question")).length ?? 0 : 0;
43
+ const numberOfRoleplayMessages = isRoleplay ? roleplayScriptLength - numberOfRoleplayInteractions : 0;
44
+ const roleplayHasMessages = numberOfRoleplayMessages > 0;
45
+ const roleplayHasInteractions = numberOfRoleplayInteractions > 0;
46
+
47
+ // For scenarios the game length is driven by the roleplay script: time
48
+ // answering each interaction, plus reading time for every message, plus feedback.
49
+ const calculateEstimatedTime = (timeValue, questions) => {
50
+ if (isRoleplay) {
51
+ const answering = numberOfRoleplayInteractions * timeValue;
52
+ const readingMessages = numberOfRoleplayMessages * 10;
53
+ const feedback = numberOfRoleplayInteractions * 10;
54
+ return Math.max(1, Math.ceil((answering + readingMessages + feedback) / 60));
30
55
  }
31
- const estimatedMinutes = Math.ceil(totalTimeWithExtra / 60);
32
- return estimatedMinutes;
56
+ const totalTimeInSeconds = timeValue * questions;
57
+ const totalTimeWithExtra = totalTimeInSeconds + questions * 10;
58
+ return Math.max(1, Math.ceil(totalTimeWithExtra / 60));
33
59
  };
34
60
  const [estimatedTime, setEstimatedTime] = useState(calculateEstimatedTime(time, numberOfQuestions));
35
- let numberOfRoleplayInteractions = 0;
36
- let numberOfRoleplayQuestions = 0;
37
- let roleplayHasInteractions = false;
38
- let roleplayHasQuestions = false;
39
- if (exercise.includes("roleplay")) {
40
- numberOfRoleplayQuestions = roleplay?.script?.filter(item => Object.hasOwn(item, "question")).length;
41
- numberOfRoleplayInteractions = roleplay?.script?.length - numberOfRoleplayQuestions;
42
- roleplayHasInteractions = numberOfRoleplayInteractions > 0;
43
- roleplayHasQuestions = numberOfRoleplayQuestions > 0;
44
- }
45
61
  useEffect(() => {
46
62
  if (isCreator) {
47
63
  handleSetTimer(debouncedTimer);
@@ -57,192 +73,333 @@ export default function GameSettings({
57
73
  setEstimatedTime(calculateEstimatedTime(timer, numberOfQuestions));
58
74
  }
59
75
  }, [timer, debouncedTimer, numberOfQuestions, isCreator]);
60
- const handleChange = e => {
61
- handleChangeAnswerOptions(e.target.value);
62
- };
76
+ const allowedAnswerValues = ANSWER_MODES_BY_EXERCISE[exercise] || [0, 1, 2];
77
+ const answerModes = [{
78
+ value: 0,
79
+ label: t("answer_keyboard_and_word_bank")
80
+ }, {
81
+ value: 1,
82
+ label: t("answer_keyboard_only")
83
+ }, {
84
+ value: 2,
85
+ label: t("answer_word_bank_only")
86
+ }].filter(mode => allowedAnswerValues.includes(mode.value));
87
+
88
+ // If the current answer method isn't valid for this game type, fall back to
89
+ // the first supported method (e.g. read & write coerces to keyboard only).
90
+ useEffect(() => {
91
+ if (isCreator && !allowedAnswerValues.includes(Number(answerOptions))) {
92
+ handleChangeAnswerOptions(allowedAnswerValues[0]);
93
+ }
94
+ // Only re-run when the game type changes; allowedAnswerValues is derived
95
+ // from `exercise` and answerOptions is intentionally read, not depended on.
96
+ // eslint-disable-next-line react-hooks/exhaustive-deps
97
+ }, [exercise]);
63
98
  const handleConfirmAndClose = () => {
64
99
  handleConfirm();
65
100
  handleClose();
66
101
  };
67
- return /*#__PURE__*/_jsxs(SwipeableDrawer, {
102
+ const questionsAtMax = numberOfQuestions >= numberOfPhrases;
103
+ const questionsReadout = questionsAtMax ? t("all_n_phrases", {
104
+ count: numberOfPhrases
105
+ }) : String(numberOfQuestions);
106
+
107
+ // Keep the question count at or above the number of phrase lists so the
108
+ // even-across-lists draw can always include at least one phrase from each
109
+ // selected list. Never exceed the total pool size.
110
+ const questionsMin = Math.min(numberOfPhrases || 1, Math.max(5, phraseListCount || 1));
111
+ const sectionLabelSx = {
112
+ fontSize: 14,
113
+ fontWeight: 700,
114
+ color: "text.secondary"
115
+ };
116
+ const readoutSx = {
117
+ fontSize: 14,
118
+ fontWeight: 700,
119
+ color: "primary.main"
120
+ };
121
+ return /*#__PURE__*/_jsx(SwipeableDrawer, {
68
122
  open: isOpen,
69
123
  anchor: "right",
70
124
  onClose: handleClose,
125
+ onOpen: () => {},
71
126
  sx: {
72
- zIndex: "3000"
127
+ zIndex: 3000
73
128
  },
74
- children: [/*#__PURE__*/_jsx(Box, {
129
+ children: /*#__PURE__*/_jsxs(Box, {
75
130
  sx: {
76
- p: 5,
77
- maxWidth: "700px"
131
+ width: 520,
132
+ maxWidth: "100vw",
133
+ height: "100%",
134
+ display: "flex",
135
+ flexDirection: "column",
136
+ backgroundColor: "background.paper"
78
137
  },
79
- children: /*#__PURE__*/_jsxs(Grid, {
80
- container: true,
138
+ children: [/*#__PURE__*/_jsx(Box, {
81
139
  sx: {
82
- width: "100%"
140
+ p: "24px 24px 16px",
141
+ display: "flex",
142
+ alignItems: "flex-start",
143
+ justifyContent: "space-between",
144
+ gap: 2
83
145
  },
84
- spacing: 1,
85
- children: [/*#__PURE__*/_jsx(Typography, {
86
- variant: "h5",
87
- component: "h1",
88
- gutterBottom: true,
89
- children: t("game_settings")
90
- }), /*#__PURE__*/_jsx(Grid, {
146
+ children: /*#__PURE__*/_jsx(Box, {
91
147
  sx: {
92
- mt: 2
93
- },
94
- size: {
95
- xs: 12,
96
- md: 12
148
+ display: "flex",
149
+ flexDirection: "column",
150
+ gap: 0.5
97
151
  },
98
- children: !exercise.includes("roleplay") && /*#__PURE__*/_jsxs(FormControl, {
99
- component: "fieldset",
100
- children: [/*#__PURE__*/_jsx(FormLabel, {
101
- component: "legend",
102
- children: t("answer_options")
103
- }), /*#__PURE__*/_jsxs(RadioGroup, {
104
- "aria-label": "Answer Options",
105
- name: "Answer Options",
106
- onChange: handleChange,
107
- value: answerOptions,
108
- children: [/*#__PURE__*/_jsx(FormControlLabel, {
109
- value: 0,
110
- control: /*#__PURE__*/_jsx(Radio, {}),
111
- label: `${t("keyboard_and_word_bank")}`
112
- }), /*#__PURE__*/_jsx(FormControlLabel, {
113
- value: 1,
114
- control: /*#__PURE__*/_jsx(Radio, {}),
115
- label: `${t("keyboard_only")}`
116
- }), /*#__PURE__*/_jsx(FormControlLabel, {
117
- value: 2,
118
- control: /*#__PURE__*/_jsx(Radio, {}),
119
- label: `${t("word_bank_only")}`
120
- })]
121
- })]
152
+ children: /*#__PURE__*/_jsx(Typography, {
153
+ sx: {
154
+ fontSize: 24,
155
+ fontWeight: 700,
156
+ lineHeight: 1.3,
157
+ color: "text.primary"
158
+ },
159
+ children: t("game_settings")
122
160
  })
123
- }), exercise.includes("roleplay") && /*#__PURE__*/_jsxs(_Fragment, {
124
- children: [roleplayHasInteractions && /*#__PURE__*/_jsx(_Fragment, {
125
- children: /*#__PURE__*/_jsx(Grid, {
126
- sx: {
127
- mt: 2
128
- },
129
- size: {
130
- xs: 12,
131
- md: 12
132
- },
133
- children: /*#__PURE__*/_jsxs(Typography, {
134
- variant: "body1",
135
- component: "p",
136
- children: [t("number_of_interactions"), ":", " ", /*#__PURE__*/_jsx("strong", {
137
- children: numberOfRoleplayInteractions
138
- })]
139
- })
140
- })
141
- }), roleplayHasQuestions && /*#__PURE__*/_jsx(_Fragment, {
142
- children: /*#__PURE__*/_jsx(Grid, {
143
- sx: {
144
- mt: 2
145
- },
146
- size: {
147
- xs: 12,
148
- md: 12
149
- },
150
- children: /*#__PURE__*/_jsxs(Typography, {
151
- variant: "body1",
152
- component: "p",
153
- children: [t("number_of_questions"), ":", " ", /*#__PURE__*/_jsx("strong", {
154
- children: numberOfRoleplayQuestions
161
+ })
162
+ }), /*#__PURE__*/_jsxs(Box, {
163
+ sx: {
164
+ flex: 1,
165
+ p: "0 24px 24px",
166
+ display: "flex",
167
+ flexDirection: "column",
168
+ gap: 3,
169
+ overflow: "auto"
170
+ },
171
+ children: [!isRoleplay && /*#__PURE__*/_jsxs(Box, {
172
+ sx: {
173
+ display: "flex",
174
+ flexDirection: "column",
175
+ gap: 1
176
+ },
177
+ children: [/*#__PURE__*/_jsx(Typography, {
178
+ sx: sectionLabelSx,
179
+ children: t("how_students_answer")
180
+ }), /*#__PURE__*/_jsx(Box, {
181
+ role: "radiogroup",
182
+ sx: {
183
+ border: "1px solid",
184
+ borderColor: "divider",
185
+ borderRadius: "16px",
186
+ overflow: "hidden"
187
+ },
188
+ children: answerModes.map((mode, i) => {
189
+ const selected = Number(answerOptions) === mode.value;
190
+ return /*#__PURE__*/_jsxs(Box, {
191
+ onClick: () => handleChangeAnswerOptions(mode.value),
192
+ sx: {
193
+ display: "flex",
194
+ alignItems: "center",
195
+ gap: 2,
196
+ p: 2,
197
+ cursor: "pointer",
198
+ transition: "background-color 200ms cubic-bezier(0.4,0,0.2,1)",
199
+ ...(i > 0 ? {
200
+ borderTop: "1px solid",
201
+ borderColor: "divider"
202
+ } : {}),
203
+ ...(selected ? {
204
+ backgroundColor: selectedRowBg
205
+ } : {})
206
+ },
207
+ children: [/*#__PURE__*/_jsx(Radio, {
208
+ checked: selected,
209
+ tabIndex: -1,
210
+ disableRipple: true,
211
+ color: "primary",
212
+ sx: {
213
+ p: 0
214
+ },
215
+ inputProps: {
216
+ "aria-label": mode.label
217
+ }
218
+ }), /*#__PURE__*/_jsx(Box, {
219
+ sx: {
220
+ display: "flex",
221
+ flexDirection: "column",
222
+ gap: 0.25
223
+ },
224
+ children: /*#__PURE__*/_jsx(Typography, {
225
+ sx: {
226
+ fontSize: 14,
227
+ fontWeight: 600,
228
+ color: "text.primary"
229
+ },
230
+ children: mode.label
231
+ })
155
232
  })]
156
- })
233
+ }, mode.value);
157
234
  })
158
235
  })]
159
- }), /*#__PURE__*/_jsx(Grid, {
236
+ }), isRoleplay && /*#__PURE__*/_jsxs(Box, {
160
237
  sx: {
161
- mt: 2
238
+ display: "flex",
239
+ flexDirection: "column",
240
+ gap: 1
162
241
  },
163
- size: {
164
- xs: 12,
165
- md: 12
166
- },
167
- children: /*#__PURE__*/_jsxs(FormControl, {
168
- component: "fieldset",
169
- children: [/*#__PURE__*/_jsx(FormLabel, {
170
- component: "legend",
171
- children: t("timer_length")
172
- }), /*#__PURE__*/_jsx(Slider, {
173
- min: 10,
174
- max: 60,
175
- step: 1,
176
- onChange: e => setTimer(e.target.value),
177
- value: timer,
178
- valueLabelDisplay: "auto",
179
- "aria-valuetext": `${timer} seconds`,
180
- "aria-label": "Timer length"
242
+ children: [roleplayHasMessages && /*#__PURE__*/_jsxs(Typography, {
243
+ sx: {
244
+ fontSize: 16,
245
+ color: "text.primary"
246
+ },
247
+ children: [t("number_of_messages"), ":", " ", /*#__PURE__*/_jsx("strong", {
248
+ children: numberOfRoleplayMessages
181
249
  })]
182
- })
183
- }), !exercise.includes("roleplay") && /*#__PURE__*/_jsx(Grid, {
250
+ }), roleplayHasInteractions && /*#__PURE__*/_jsxs(Typography, {
251
+ sx: {
252
+ fontSize: 16,
253
+ color: "text.primary"
254
+ },
255
+ children: [t("number_of_interactions"), ":", " ", /*#__PURE__*/_jsx("strong", {
256
+ children: numberOfRoleplayInteractions
257
+ })]
258
+ })]
259
+ }), /*#__PURE__*/_jsxs(Box, {
184
260
  sx: {
185
- mt: 2
261
+ display: "flex",
262
+ flexDirection: "column",
263
+ gap: 1
186
264
  },
187
- size: {
188
- xs: 12,
189
- md: 12
265
+ children: [/*#__PURE__*/_jsxs(Box, {
266
+ sx: {
267
+ display: "flex",
268
+ alignItems: "baseline",
269
+ justifyContent: "space-between"
270
+ },
271
+ children: [/*#__PURE__*/_jsx(Typography, {
272
+ sx: sectionLabelSx,
273
+ children: t("time_per_question")
274
+ }), /*#__PURE__*/_jsx(Typography, {
275
+ sx: readoutSx,
276
+ children: `${timer}s`
277
+ })]
278
+ }), /*#__PURE__*/_jsx(Slider, {
279
+ min: 10,
280
+ max: 60,
281
+ step: 5,
282
+ onChange: (_event, value) => setTimer(Array.isArray(value) ? value[0] : value),
283
+ value: timer,
284
+ valueLabelDisplay: "auto",
285
+ "aria-valuetext": `${timer} seconds`,
286
+ "aria-label": t("time_per_question")
287
+ })]
288
+ }), !isRoleplay && /*#__PURE__*/_jsxs(Box, {
289
+ sx: {
290
+ display: "flex",
291
+ flexDirection: "column",
292
+ gap: 1
190
293
  },
191
- children: /*#__PURE__*/_jsxs(FormControl, {
192
- component: "fieldset",
193
- children: [/*#__PURE__*/_jsx(FormLabel, {
194
- component: "legend",
294
+ children: [/*#__PURE__*/_jsxs(Box, {
295
+ sx: {
296
+ display: "flex",
297
+ alignItems: "baseline",
298
+ justifyContent: "space-between"
299
+ },
300
+ children: [/*#__PURE__*/_jsx(Typography, {
301
+ sx: sectionLabelSx,
195
302
  children: t("number_of_questions")
196
- }), /*#__PURE__*/_jsx(Slider, {
197
- min: 1,
198
- max: numberOfPhrases,
199
- step: 1,
200
- onChange: handleSetNumberOfQuestions,
201
- value: numberOfQuestions,
202
- valueLabelDisplay: "auto",
203
- "aria-valuetext": `${numberOfQuestions} questions`,
204
- "aria-label": "Number of questions"
303
+ }), /*#__PURE__*/_jsx(Typography, {
304
+ sx: readoutSx,
305
+ children: questionsReadout
205
306
  })]
206
- })
207
- }), /*#__PURE__*/_jsx(Grid, {
307
+ }), /*#__PURE__*/_jsx(Slider, {
308
+ min: questionsMin,
309
+ max: numberOfPhrases,
310
+ step: 1,
311
+ onChange: handleSetNumberOfQuestions,
312
+ value: numberOfQuestions,
313
+ valueLabelDisplay: "auto",
314
+ "aria-valuetext": questionsAtMax ? t("all_n_phrases", {
315
+ count: numberOfPhrases
316
+ }) : `${numberOfQuestions} questions`,
317
+ "aria-label": t("number_of_questions")
318
+ }), phraseListCount > 1 && /*#__PURE__*/_jsx(Typography, {
319
+ sx: {
320
+ fontSize: 14,
321
+ color: "text.secondary"
322
+ },
323
+ children: t("includes_one_from_each_list")
324
+ })]
325
+ }), /*#__PURE__*/_jsxs(Box, {
208
326
  sx: {
209
- mt: 2
327
+ backgroundColor: "action.hover",
328
+ borderRadius: "16px",
329
+ p: 2,
330
+ minHeight: 96,
331
+ flexShrink: 0,
332
+ display: "flex",
333
+ alignItems: "center",
334
+ gap: 2
210
335
  },
211
- size: {
212
- xs: 12,
213
- md: 12
214
- },
215
- children: /*#__PURE__*/_jsxs(Typography, {
216
- variant: "body1",
217
- component: "p",
218
- children: [t("estimated_game_time"), ":", " ", /*#__PURE__*/_jsx("strong", {
219
- children: estimatedTime <= 1 ? `${estimatedTime} ${t("minute")}` : `${estimatedTime} ${t("minutes")}`
336
+ children: [/*#__PURE__*/_jsx(ScheduleIcon, {
337
+ sx: {
338
+ fontSize: 28,
339
+ color: "primary.main"
340
+ }
341
+ }), /*#__PURE__*/_jsxs(Box, {
342
+ sx: {
343
+ display: "flex",
344
+ flexDirection: "column",
345
+ gap: 0.25
346
+ },
347
+ children: [/*#__PURE__*/_jsx(Typography, {
348
+ sx: {
349
+ fontSize: 16,
350
+ fontWeight: 700,
351
+ color: "text.primary"
352
+ },
353
+ children: t("estimated_game_time", {
354
+ time: t("n_minutes", {
355
+ count: estimatedTime
356
+ })
357
+ })
358
+ }), /*#__PURE__*/_jsx(Typography, {
359
+ sx: {
360
+ fontSize: 14,
361
+ lineHeight: 1.43,
362
+ color: "text.secondary",
363
+ minHeight: 40
364
+ },
365
+ children: t("estimated_game_time_hint", {
366
+ questions: isRoleplay ? numberOfRoleplayInteractions : numberOfQuestions,
367
+ seconds: timer
368
+ })
220
369
  })]
221
- })
370
+ })]
371
+ })]
372
+ }), /*#__PURE__*/_jsx(Divider, {}), /*#__PURE__*/_jsxs(Box, {
373
+ sx: {
374
+ p: "16px 24px",
375
+ display: "flex",
376
+ alignItems: "center",
377
+ justifyContent: "flex-end",
378
+ gap: 1.5
379
+ },
380
+ children: [/*#__PURE__*/_jsx(Button, {
381
+ onClick: handleClose,
382
+ color: "primary",
383
+ sx: {
384
+ fontSize: 15,
385
+ fontWeight: 600,
386
+ borderRadius: "25px",
387
+ px: 2
388
+ },
389
+ children: t("close")
390
+ }), /*#__PURE__*/_jsx(Button, {
391
+ variant: "contained",
392
+ color: "primary",
393
+ onClick: handleConfirmAndClose,
394
+ sx: {
395
+ fontSize: 15,
396
+ fontWeight: 600,
397
+ borderRadius: "25px",
398
+ px: 3.5
399
+ },
400
+ children: t("confirm")
222
401
  })]
223
- })
224
- }), /*#__PURE__*/_jsx(Box, {
225
- sx: {
226
- flexGrow: 1
227
- }
228
- }), /*#__PURE__*/_jsxs(Box, {
229
- sx: theme => ({
230
- display: "flex",
231
- alignItems: "center",
232
- justifyContent: "flex-start",
233
- padding: "0 8px",
234
- ...theme.mixins.toolbar
235
- }),
236
- children: [/*#__PURE__*/_jsx(Button, {
237
- color: "primary",
238
- onClick: handleClose,
239
- children: t("close")
240
- }), /*#__PURE__*/_jsx(Button, {
241
- color: "primary",
242
- variant: "contained",
243
- onClick: handleConfirmAndClose,
244
- children: t("confirm")
245
402
  })]
246
- })]
403
+ })
247
404
  });
248
405
  }