@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.
@@ -84,7 +84,10 @@ export default function ImageBuilderDialog({
84
84
  setIconOpacity(getOptimalIconOpacity(c));
85
85
  };
86
86
  const handleConfirm = () => {
87
- onConfirm(bannerDataUrl, {
87
+ onConfirm({
88
+ bannerDataUrl,
89
+ cardDataUrl
90
+ }, {
88
91
  color,
89
92
  iconKey
90
93
  });
@@ -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: (dataUrl, {
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, "newest"));
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
- // Newest-first is the baseline order; for recentlyViewed it also decides
200
- // where never-opened courses land, since the visit sort is stable.
201
- const newestFirst = [...courses].reverse();
202
- if (sortOrder === "newest") return newestFirst;
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
- return newestFirst.sort((a, b) => compareCoursesByLastVisit(a, b, visits));
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 newest-first order.
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, {
@@ -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
- // "newest" (most recently added to the classroom) has no comparator here —
48
- // there is no timestamp for classroom-course membership, only array position,
49
- // so callers handle it by reversing the already-ordered courses array instead.
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") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nualang/nualang-ui-components",
3
- "version": "0.1.1417",
3
+ "version": "0.1.1419",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "files": [