@nualang/nualang-ui-components 0.1.1417 → 0.1.1419
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/Dialogs/ImageBuilder/ImageBuilderDialog.js +4 -1
- package/dist/Exercises/PhraseGroup/PhraseGroupInfo/PhraseGroupInfo.js +12 -8
- package/dist/Forms/ImageSelector/ImageSelector.js +9 -4
- package/dist/Lists/Courses/Courses.js +13 -7
- package/dist/Lists/Courses/Courses.test.js +52 -1
- package/dist/Live/WaitingRoom/WaitingRoom.js +16 -16
- package/dist/utils/courseSort.js +10 -3
- package/package.json +1 -1
|
@@ -65,6 +65,16 @@ function PhraseGroupInfo({
|
|
|
65
65
|
description = "",
|
|
66
66
|
phrases = []
|
|
67
67
|
}) {
|
|
68
|
+
// Fixed-footprint style so every source — a landscape library photo or a
|
|
69
|
+
// square Image Builder card — renders at the same size. Without a height +
|
|
70
|
+
// object-fit, a square card fills the full 250px width as a 250×250 block and
|
|
71
|
+
// dominates the header.
|
|
72
|
+
const imageStyle = {
|
|
73
|
+
width: 250,
|
|
74
|
+
height: 160,
|
|
75
|
+
objectFit: "cover",
|
|
76
|
+
borderRadius: "10%"
|
|
77
|
+
};
|
|
68
78
|
return /*#__PURE__*/_jsxs(Box, {
|
|
69
79
|
sx: {
|
|
70
80
|
width: "100%",
|
|
@@ -83,18 +93,12 @@ function PhraseGroupInfo({
|
|
|
83
93
|
},
|
|
84
94
|
children: picture ? /*#__PURE__*/_jsx("img", {
|
|
85
95
|
src: picture,
|
|
86
|
-
width: 250,
|
|
87
96
|
alt: "",
|
|
88
|
-
style:
|
|
89
|
-
borderRadius: "10%"
|
|
90
|
-
}
|
|
97
|
+
style: imageStyle
|
|
91
98
|
}) : /*#__PURE__*/_jsx("img", {
|
|
92
99
|
src: PlaceholderImages.roleplay,
|
|
93
|
-
width: 250,
|
|
94
100
|
alt: "",
|
|
95
|
-
style:
|
|
96
|
-
borderRadius: "10%"
|
|
97
|
-
}
|
|
101
|
+
style: imageStyle
|
|
98
102
|
})
|
|
99
103
|
})
|
|
100
104
|
}), /*#__PURE__*/_jsxs(Box, {
|
|
@@ -283,7 +283,8 @@ function ImageSelector({
|
|
|
283
283
|
});
|
|
284
284
|
}
|
|
285
285
|
const {
|
|
286
|
-
bannerDataUrl
|
|
286
|
+
bannerDataUrl,
|
|
287
|
+
cardDataUrl
|
|
287
288
|
} = generateBuilderImages({
|
|
288
289
|
color: randomColor,
|
|
289
290
|
iconKey: randomIcon,
|
|
@@ -291,9 +292,9 @@ function ImageSelector({
|
|
|
291
292
|
iconOpacity: getOptimalIconOpacity(randomColor),
|
|
292
293
|
darkIcons: shouldUseDarkIcons(randomColor)
|
|
293
294
|
});
|
|
294
|
-
onChange(bannerDataUrl);
|
|
295
|
+
onChange(isBanner ? bannerDataUrl : cardDataUrl);
|
|
295
296
|
}
|
|
296
|
-
}, [value, autoGenerateDefault, onChange]);
|
|
297
|
+
}, [value, autoGenerateDefault, isBanner, onChange, onBuilderSettingsChange]);
|
|
297
298
|
const handleTogglePreview = () => {
|
|
298
299
|
setOpenPreview(!openPreview);
|
|
299
300
|
};
|
|
@@ -617,10 +618,14 @@ function ImageSelector({
|
|
|
617
618
|
onClose: () => setIsImageBuilderOpen(false),
|
|
618
619
|
initialColor: builderColor,
|
|
619
620
|
initialIconKey: builderIcon,
|
|
620
|
-
onConfirm: (
|
|
621
|
+
onConfirm: ({
|
|
622
|
+
bannerDataUrl,
|
|
623
|
+
cardDataUrl
|
|
624
|
+
}, {
|
|
621
625
|
color,
|
|
622
626
|
iconKey
|
|
623
627
|
} = {}) => {
|
|
628
|
+
const dataUrl = isBanner ? bannerDataUrl : cardDataUrl;
|
|
624
629
|
setFileSize(0);
|
|
625
630
|
setImageSrc(dataUrl);
|
|
626
631
|
setOriginalImage(dataUrl);
|
|
@@ -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, "
|
|
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);
|
|
@@ -196,12 +196,15 @@ function Courses({
|
|
|
196
196
|
// otherwise the caller has already sorted/filtered courses and this
|
|
197
197
|
// must not reorder them.
|
|
198
198
|
if (!showSortControl) return courses;
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
if (sortOrder === "
|
|
199
|
+
// The courses arrive from Dynamo oldest-first, so the default order is
|
|
200
|
+
// simply the array as given — the courses added to the classroom first
|
|
201
|
+
// stay at the top.
|
|
202
|
+
if (sortOrder === "default") return courses;
|
|
203
|
+
if (sortOrder === "newest") return [...courses].reverse();
|
|
203
204
|
if (sortOrder === "recentlyViewed") {
|
|
204
|
-
|
|
205
|
+
// Sorting the default order means never-opened courses stay in it,
|
|
206
|
+
// below the opened ones, since the visit sort is stable.
|
|
207
|
+
return [...courses].sort((a, b) => compareCoursesByLastVisit(a, b, visits));
|
|
205
208
|
}
|
|
206
209
|
return [...courses].sort((a, b) => compareCoursesBySortOrder(a, b, sortOrder));
|
|
207
210
|
}, [courses, sortOrder, visits, showSortControl]);
|
|
@@ -232,6 +235,9 @@ function Courses({
|
|
|
232
235
|
label: t("sort_by"),
|
|
233
236
|
autoWidth: true,
|
|
234
237
|
children: [/*#__PURE__*/_jsx(MenuItem, {
|
|
238
|
+
value: "default",
|
|
239
|
+
children: t("default")
|
|
240
|
+
}), /*#__PURE__*/_jsx(MenuItem, {
|
|
235
241
|
value: "newest",
|
|
236
242
|
children: t("newest")
|
|
237
243
|
}), /*#__PURE__*/_jsx(MenuItem, {
|
|
@@ -44,10 +44,46 @@ describe("Courses", () => {
|
|
|
44
44
|
} = renderCourses();
|
|
45
45
|
unmount();
|
|
46
46
|
});
|
|
47
|
+
it("defaults to the order the courses arrive in, oldest added first", async () => {
|
|
48
|
+
// No stored preference — courses added to the classroom first stay at the
|
|
49
|
+
// top, in the order Dynamo sent them down.
|
|
50
|
+
const {
|
|
51
|
+
unmount
|
|
52
|
+
} = renderCourses({
|
|
53
|
+
showSortControl: true
|
|
54
|
+
});
|
|
55
|
+
const courseNames = await renderedCourseNames();
|
|
56
|
+
expect(courseNames).toEqual(props.courses.map(c => c.courseName));
|
|
57
|
+
unmount();
|
|
58
|
+
});
|
|
59
|
+
it("falls back to the default order when a stale preference is stored", async () => {
|
|
60
|
+
// The order was named "oldest" in 0.1.1418; a teacher who picked it then
|
|
61
|
+
// still has that in localStorage.
|
|
62
|
+
localStorage.setItem(CLASSROOM_COURSES_SORT_ORDER_KEY, "oldest");
|
|
63
|
+
const {
|
|
64
|
+
unmount
|
|
65
|
+
} = renderCourses({
|
|
66
|
+
showSortControl: true
|
|
67
|
+
});
|
|
68
|
+
const courseNames = await renderedCourseNames();
|
|
69
|
+
expect(courseNames).toEqual(props.courses.map(c => c.courseName));
|
|
70
|
+
unmount();
|
|
71
|
+
});
|
|
72
|
+
it("reverses the courses when the newest preference is stored", async () => {
|
|
73
|
+
localStorage.setItem(CLASSROOM_COURSES_SORT_ORDER_KEY, "newest");
|
|
74
|
+
const {
|
|
75
|
+
unmount
|
|
76
|
+
} = renderCourses({
|
|
77
|
+
showSortControl: true
|
|
78
|
+
});
|
|
79
|
+
const courseNames = await renderedCourseNames();
|
|
80
|
+
expect(courseNames).toEqual(props.courses.map(c => c.courseName).reverse());
|
|
81
|
+
unmount();
|
|
82
|
+
});
|
|
47
83
|
it("puts recently viewed courses first when that preference is stored", async () => {
|
|
48
84
|
localStorage.setItem(CLASSROOM_COURSES_SORT_ORDER_KEY, "recentlyViewed");
|
|
49
85
|
// Courses 2, 3 and 5 are "English Basics", "Learn Greetings 2" and
|
|
50
|
-
// "English Basics 2" — none of which lead the default
|
|
86
|
+
// "English Basics 2" — none of which lead the default order.
|
|
51
87
|
localStorage.setItem(COURSE_VISITS_KEY, JSON.stringify({
|
|
52
88
|
2: 100,
|
|
53
89
|
5: 300,
|
|
@@ -62,6 +98,21 @@ describe("Courses", () => {
|
|
|
62
98
|
expect(courseNames.slice(0, 3)).toEqual(["English Basics 2", "Learn Greetings 2", "English Basics"]);
|
|
63
99
|
unmount();
|
|
64
100
|
});
|
|
101
|
+
it("leaves never-viewed courses in the default order, below the viewed ones", async () => {
|
|
102
|
+
localStorage.setItem(CLASSROOM_COURSES_SORT_ORDER_KEY, "recentlyViewed");
|
|
103
|
+
localStorage.setItem(COURSE_VISITS_KEY, JSON.stringify({
|
|
104
|
+
5: 300
|
|
105
|
+
}));
|
|
106
|
+
const {
|
|
107
|
+
unmount
|
|
108
|
+
} = renderCourses({
|
|
109
|
+
showSortControl: true
|
|
110
|
+
});
|
|
111
|
+
const courseNames = await renderedCourseNames();
|
|
112
|
+
const unviewed = props.courses.filter(c => c.courseId !== "5").map(c => c.courseName);
|
|
113
|
+
expect(courseNames).toEqual(["English Basics 2", ...unviewed]);
|
|
114
|
+
unmount();
|
|
115
|
+
});
|
|
65
116
|
it("renders courses in the order the caller passed them when showSortControl is not set", async () => {
|
|
66
117
|
// Caller (e.g. CoursesScreen) already filters/sorts before handing courses
|
|
67
118
|
// to this component, so it must not silently reorder them.
|
|
@@ -41,6 +41,18 @@ export default function WaitingRoom({
|
|
|
41
41
|
const handleOpenViewVideo = () => {
|
|
42
42
|
setIsViewVideoOpen(true);
|
|
43
43
|
};
|
|
44
|
+
|
|
45
|
+
// Fixed-footprint style for the lobby media image. Constraining both
|
|
46
|
+
// dimensions with object-fit keeps every source (roleplay/phrase list/topic
|
|
47
|
+
// picture — whether a landscape library photo or a square Image Builder card)
|
|
48
|
+
// at the same size, instead of a square card rendering much taller than a
|
|
49
|
+
// landscape image when only width is set.
|
|
50
|
+
const mediaImageStyle = {
|
|
51
|
+
width: 250,
|
|
52
|
+
height: 160,
|
|
53
|
+
objectFit: "cover",
|
|
54
|
+
borderRadius: "5%"
|
|
55
|
+
};
|
|
44
56
|
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
45
57
|
children: [/*#__PURE__*/_jsxs(Box, {
|
|
46
58
|
sx: {
|
|
@@ -68,32 +80,20 @@ export default function WaitingRoom({
|
|
|
68
80
|
},
|
|
69
81
|
children: selectedRoleplay ? /*#__PURE__*/_jsx("img", {
|
|
70
82
|
src: selectedRoleplay.picture,
|
|
71
|
-
width: 250,
|
|
72
83
|
alt: "",
|
|
73
|
-
style:
|
|
74
|
-
borderRadius: "5%"
|
|
75
|
-
}
|
|
84
|
+
style: mediaImageStyle
|
|
76
85
|
}) : phraseListImage ? /*#__PURE__*/_jsx("img", {
|
|
77
86
|
src: phraseListImage,
|
|
78
|
-
width: 250,
|
|
79
87
|
alt: topicName || t("topic_image"),
|
|
80
|
-
style:
|
|
81
|
-
borderRadius: "5%"
|
|
82
|
-
}
|
|
88
|
+
style: mediaImageStyle
|
|
83
89
|
}) : topicImage ? /*#__PURE__*/_jsx("img", {
|
|
84
90
|
src: topicImage,
|
|
85
|
-
width: 250,
|
|
86
91
|
alt: "",
|
|
87
|
-
style:
|
|
88
|
-
borderRadius: "5%"
|
|
89
|
-
}
|
|
92
|
+
style: mediaImageStyle
|
|
90
93
|
}) : /*#__PURE__*/_jsx("img", {
|
|
91
94
|
src: PlaceholderImages.topic,
|
|
92
|
-
width: 250,
|
|
93
95
|
alt: "",
|
|
94
|
-
style:
|
|
95
|
-
borderRadius: "5%"
|
|
96
|
-
}
|
|
96
|
+
style: mediaImageStyle
|
|
97
97
|
})
|
|
98
98
|
})
|
|
99
99
|
}), /*#__PURE__*/_jsxs(Grid, {
|
package/dist/utils/courseSort.js
CHANGED
|
@@ -44,9 +44,16 @@ export const recordCourseVisit = (courseId, timestamp = Date.now()) => {
|
|
|
44
44
|
// stable sort leaves them in whatever order the caller passed them in.
|
|
45
45
|
export const compareCoursesByLastVisit = (a = {}, b = {}, visits = {}) => (visits[b.courseId] || 0) - (visits[a.courseId] || 0);
|
|
46
46
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
47
|
+
// Every order the classroom courses dropdown offers. Anything else — a stale
|
|
48
|
+
// preference from an older release, or a hand-edited value — is treated as
|
|
49
|
+
// "default" rather than leaving the dropdown showing a blank label.
|
|
50
|
+
export const COURSE_SORT_ORDERS = ["default", "newest", "alphabetical", "recentlyChanged", "recentlyViewed"];
|
|
51
|
+
export const normalizeCourseSortOrder = order => COURSE_SORT_ORDERS.includes(order) ? order : "default";
|
|
52
|
+
|
|
53
|
+
// "default" and "newest" have no comparator here — there is no timestamp for
|
|
54
|
+
// classroom-course membership, only array position. "default" is the order the
|
|
55
|
+
// courses arrive in from Dynamo, so callers leave the array untouched; "newest"
|
|
56
|
+
// is that same array reversed.
|
|
50
57
|
// "recentlyViewed" needs the visits map, so it has its own comparator above.
|
|
51
58
|
export const compareCoursesBySortOrder = (a = {}, b = {}, order) => {
|
|
52
59
|
if (order === "alphabetical") {
|