@edx/frontend-app-subscription-learner-dashboard 1.13.0 → 1.14.0

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.
Files changed (37) hide show
  1. package/dist/app.js +4 -1
  2. package/dist/app.js.map +1 -1
  3. package/dist/constants.d.ts +8 -0
  4. package/dist/constants.js +7 -0
  5. package/dist/constants.js.map +1 -1
  6. package/dist/containers/ProductCard/index.js +4 -1
  7. package/dist/containers/ProductCard/index.js.map +1 -1
  8. package/dist/containers/RecentlyViewedPanel/RecentlyViewedCourseProgram/index.js +1 -1
  9. package/dist/containers/RecentlyViewedPanel/RecentlyViewedCourseProgram/index.js.map +1 -1
  10. package/dist/containers/RecentlyViewedPanel/index.js +3 -1
  11. package/dist/containers/RecentlyViewedPanel/index.js.map +1 -1
  12. package/dist/data/services/subs/api.d.ts +0 -20
  13. package/dist/data/services/subs/api.js +0 -111
  14. package/dist/data/services/subs/api.js.map +1 -1
  15. package/dist/data/services/subs/index.d.ts +1 -1
  16. package/dist/data/services/subs/index.js +1 -1
  17. package/dist/data/services/subs/index.js.map +1 -1
  18. package/dist/hooks/useRecentlyViewedCoursesAndProgramsData.d.ts +11 -15
  19. package/dist/hooks/useRecentlyViewedCoursesAndProgramsData.js +39 -3
  20. package/dist/hooks/useRecentlyViewedCoursesAndProgramsData.js.map +1 -1
  21. package/dist/types/types.d.ts +32 -0
  22. package/dist/types/types.js +7 -0
  23. package/dist/types/types.js.map +1 -0
  24. package/dist/utils/algoliaUtils.d.ts +57 -0
  25. package/dist/utils/algoliaUtils.js +127 -0
  26. package/dist/utils/algoliaUtils.js.map +1 -0
  27. package/package.json +1 -1
  28. package/dist/containers/RecommendedCourse/index.d.ts +0 -3
  29. package/dist/containers/RecommendedCourse/index.js +0 -16
  30. package/dist/containers/RecommendedCourse/index.js.map +0 -1
  31. package/dist/containers/RecommendedCourse/index.scss +0 -21
  32. package/dist/containers/RecommendedCourse/messages.d.ts +0 -8
  33. package/dist/containers/RecommendedCourse/messages.js +0 -10
  34. package/dist/containers/RecommendedCourse/messages.js.map +0 -1
  35. package/dist/hooks/useRecommendedCourseData.d.ts +0 -15
  36. package/dist/hooks/useRecommendedCourseData.js +0 -6
  37. package/dist/hooks/useRecommendedCourseData.js.map +0 -1
package/dist/app.js CHANGED
@@ -14,7 +14,10 @@ const app = {
14
14
  ORDER_HISTORY_URL: '',
15
15
  SUPPORT_URL: '',
16
16
  SHOW_UNENROLL_SURVEY: false,
17
- }
17
+ ALGOLIA_APP_ID: 'IGSYV1Z1XI',
18
+ ALGOLIA_SEARCH_API_KEY: '9b43cfe4f35ae59780d99ea70e3e239d',
19
+ ALGOLIA_PRODUCT_INDEX_NAME: 'product',
20
+ },
18
21
  };
19
22
  export default app;
20
23
  //# sourceMappingURL=app.js.map
package/dist/app.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,MAAM,MAAM,UAAU,CAAC;AAC9B,OAAO,SAAS,MAAM,aAAa,CAAC;AACpC,OAAO,KAAK,MAAM,SAAS,CAAC;AAE5B,MAAM,GAAG,GAAQ;IACf,KAAK;IACL,MAAM;IACN,SAAS;IACT,KAAK;IACL,MAAM,EAAE;QACN,iBAAiB,EAAE,mCAAmC;QACtD,eAAe,EAAE,KAAK;QACtB,kBAAkB,EAAE,EAAE;QACtB,iBAAiB,EAAE,EAAE;QACrB,WAAW,EAAE,EAAE;QACf,oBAAoB,EAAE,KAAK;KAC5B;CACF,CAAC;AAEF,eAAe,GAAG,CAAC","sourcesContent":["import { App } from '@openedx/frontend-base';\nimport { appId } from './constants';\nimport routes from './routes';\nimport providers from './providers';\nimport slots from './slots';\n\nconst app: App = {\n appId,\n routes,\n providers,\n slots,\n config: {\n LEARNING_BASE_URL: 'http://apps.local.openedx.io:2000',\n ENABLE_PROGRAMS: false,\n ECOMMERCE_BASE_URL: '',\n ORDER_HISTORY_URL: '',\n SUPPORT_URL: '',\n SHOW_UNENROLL_SURVEY: false,\n }\n};\n\nexport default app;\n"]}
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,MAAM,MAAM,UAAU,CAAC;AAC9B,OAAO,SAAS,MAAM,aAAa,CAAC;AACpC,OAAO,KAAK,MAAM,SAAS,CAAC;AAE5B,MAAM,GAAG,GAAQ;IACf,KAAK;IACL,MAAM;IACN,SAAS;IACT,KAAK;IACL,MAAM,EAAE;QACN,iBAAiB,EAAE,mCAAmC;QACtD,eAAe,EAAE,KAAK;QACtB,kBAAkB,EAAE,EAAE;QACtB,iBAAiB,EAAE,EAAE;QACrB,WAAW,EAAE,EAAE;QACf,oBAAoB,EAAE,KAAK;QAC3B,cAAc,EAAE,YAAY;QAC5B,sBAAsB,EAAE,kCAAkC;QAC1D,0BAA0B,EAAE,SAAS;KACtC;CACF,CAAC;AAEF,eAAe,GAAG,CAAC","sourcesContent":["import { App } from '@openedx/frontend-base';\nimport { appId } from './constants';\nimport routes from './routes';\nimport providers from './providers';\nimport slots from './slots';\n\nconst app: App = {\n appId,\n routes,\n providers,\n slots,\n config: {\n LEARNING_BASE_URL: 'http://apps.local.openedx.io:2000',\n ENABLE_PROGRAMS: false,\n ECOMMERCE_BASE_URL: '',\n ORDER_HISTORY_URL: '',\n SUPPORT_URL: '',\n SHOW_UNENROLL_SURVEY: false,\n ALGOLIA_APP_ID: 'IGSYV1Z1XI',\n ALGOLIA_SEARCH_API_KEY: '9b43cfe4f35ae59780d99ea70e3e239d',\n ALGOLIA_PRODUCT_INDEX_NAME: 'product',\n },\n};\n\nexport default app;\n"]}
@@ -1 +1,9 @@
1
+ import { ProductType } from './types/types';
1
2
  export declare const appId = "org.openedx.frontend.app.learnerDashboard";
3
+ export declare const HITS_PER_PAGE_PROGRAMS = 4;
4
+ export declare const HITS_PER_PAGE_COURSES = 4;
5
+ /** Facet attribute names in ALGOLIA_PRODUCT_INDEX_NAME — update if your index differs */
6
+ export declare const FACET_PRODUCT_TYPE = "product";
7
+ export declare const FACET_SUBJECTS = "subjects";
8
+ export declare const PRODUCT_TYPE_PROGRAM: ProductType;
9
+ export declare const PRODUCT_TYPE_COURSE: ProductType;
package/dist/constants.js CHANGED
@@ -1,2 +1,9 @@
1
1
  export const appId = 'org.openedx.frontend.app.learnerDashboard';
2
+ export const HITS_PER_PAGE_PROGRAMS = 4;
3
+ export const HITS_PER_PAGE_COURSES = 4;
4
+ /** Facet attribute names in ALGOLIA_PRODUCT_INDEX_NAME — update if your index differs */
5
+ export const FACET_PRODUCT_TYPE = 'product';
6
+ export const FACET_SUBJECTS = 'subjects';
7
+ export const PRODUCT_TYPE_PROGRAM = 'program';
8
+ export const PRODUCT_TYPE_COURSE = 'course';
2
9
  //# sourceMappingURL=constants.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,KAAK,GAAG,2CAA2C,CAAC","sourcesContent":["export const appId = 'org.openedx.frontend.app.learnerDashboard';\n"]}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,KAAK,GAAG,2CAA2C,CAAC;AAEjE,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AACxC,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC,yFAAyF;AACzF,MAAM,CAAC,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAC5C,MAAM,CAAC,MAAM,cAAc,GAAG,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,oBAAoB,GAAgB,SAAS,CAAC;AAC3D,MAAM,CAAC,MAAM,mBAAmB,GAAgB,QAAQ,CAAC","sourcesContent":["import { ProductType } from './types/types';\n\nexport const appId = 'org.openedx.frontend.app.learnerDashboard';\n\nexport const HITS_PER_PAGE_PROGRAMS = 4;\nexport const HITS_PER_PAGE_COURSES = 4;\n\n/** Facet attribute names in ALGOLIA_PRODUCT_INDEX_NAME — update if your index differs */\nexport const FACET_PRODUCT_TYPE = 'product';\nexport const FACET_SUBJECTS = 'subjects';\n\nexport const PRODUCT_TYPE_PROGRAM: ProductType = 'program';\nexport const PRODUCT_TYPE_COURSE: ProductType = 'course';\n"]}
@@ -2,7 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Card, Badge, Icon } from '@openedx/paragon';
3
3
  import { School } from '@openedx/paragon/icons';
4
4
  import './index.scss';
5
+ const DESCRIPTION_MAX_LENGTH = 130;
6
+ const getPlainText = (value) => value.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
7
+ const truncateText = (value, maxLength) => (value.length > maxLength ? `${value.slice(0, maxLength).trimEnd()}...` : value);
5
8
  export const ProductCard = ({ item, isLoading }) => {
6
- return (_jsxs(Card, { className: "shadow-sm d-flex flex-column rounded w-100 overflow-hidden", variant: item.isProgram ? 'dark' : 'light', "data-testid": "product-card", isLoading: isLoading, children: [_jsx("div", { className: "position-relative", children: _jsx(Card.ImageCap, { src: item.url, srcAlt: `${item.title} main-image`, "data-testid": "product-card-image", logoSrc: item.thumbnail || '', logoAlt: `${item.title} thumbnail-image` }) }), _jsx(Card.Header, { title: item.title }), _jsxs(Card.Section, { className: "d-flex flex-column flex-grow-1 overflow-hidden", children: [_jsx("div", { className: "flex-grow-1 min-h-0", children: _jsx("p", { className: "text-truncate-3 mb-2", children: item.body }) }), (item.isProgram && item.tagText) && (_jsxs(Badge, { variant: "light", className: "d-inline-flex align-items-center px-2 py-1 align-self-start", children: [_jsx(Icon, { src: School, className: "mr-2" }), item.tagText] }))] }), _jsx(Card.Footer, { className: "d-flex justify-content-between align-items-center", children: _jsx("span", { className: `small ${item.isProgram ? 'text-white' : 'text-muted'}`, children: item.footerLabel }) })] }));
9
+ return (_jsxs(Card, { className: "shadow-sm d-flex flex-column rounded w-100 overflow-hidden", variant: item.product === 'Course' ? 'light' : 'dark', "data-testid": "product-card", isLoading: isLoading, children: [_jsx("div", { className: "position-relative", children: _jsx(Card.ImageCap, { src: item.url, srcAlt: `${item.title} main-image`, "data-testid": "product-card-image", logoSrc: item.thumbnail || '', logoAlt: `${item.title} thumbnail-image` }) }), _jsx(Card.Header, { title: item.title }), _jsxs(Card.Section, { className: "d-flex flex-column flex-grow-1 overflow-hidden", children: [_jsx("div", { className: "flex-grow-1 min-h-0", children: _jsx("p", { className: "text-truncate-3 mb-2", children: truncateText(getPlainText(item.primary_description), DESCRIPTION_MAX_LENGTH) }) }), (item.product === 'Program' && item.tagText) && (_jsxs(Badge, { variant: "light", className: "d-inline-flex align-items-center px-2 py-1 align-self-start", children: [_jsx(Icon, { src: School, className: "mr-2" }), item.tagText] }))] }), _jsx(Card.Footer, { className: "d-flex justify-content-between align-items-center", children: _jsx("span", { className: `small ${item.product === 'Program' ? 'text-white' : 'text-muted'}`, children: item.footerLabel }) })] }));
7
10
  };
8
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/containers/ProductCard/index.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAIhD,OAAO,cAAc,CAAC;AAEtB,MAAM,CAAC,MAAM,WAAW,GAAyB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE;IACvE,OAAO,CACL,MAAC,IAAI,IACH,SAAS,EAAC,4DAA4D,EACtE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,iBAC9B,cAAc,EAC1B,SAAS,EAAE,SAAS,aAEpB,cAAK,SAAS,EAAC,mBAAmB,YAChC,KAAC,IAAI,CAAC,QAAQ,IACZ,GAAG,EAAE,IAAI,CAAC,GAAG,EACb,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,aAAa,iBACtB,oBAAoB,EAChC,OAAO,EAAE,IAAI,CAAC,SAAS,IAAI,EAAE,EAC7B,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,kBAAkB,GACxC,GACE,EAEN,KAAC,IAAI,CAAC,MAAM,IAAC,KAAK,EAAE,IAAI,CAAC,KAAK,GAAI,EAElC,MAAC,IAAI,CAAC,OAAO,IAAC,SAAS,EAAC,gDAAgD,aACtE,cAAK,SAAS,EAAC,qBAAqB,YAClC,YAAG,SAAS,EAAC,sBAAsB,YAAE,IAAI,CAAC,IAAI,GAAK,GAC/C,EAEL,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CACnC,MAAC,KAAK,IACJ,OAAO,EAAC,OAAO,EACf,SAAS,EAAC,6DAA6D,aAEvE,KAAC,IAAI,IAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAC,MAAM,GAAG,EACrC,IAAI,CAAC,OAAO,IACP,CACT,IACY,EAEf,KAAC,IAAI,CAAC,MAAM,IAAC,SAAS,EAAC,mDAAmD,YACxE,eAAM,SAAS,EAAE,SAAS,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,YAAY,EAAE,YAAG,IAAI,CAAC,WAAW,GAAQ,GACvF,IACT,CACR,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { FC } from 'react';\nimport { Card, Badge, Icon } from '@openedx/paragon';\nimport { School } from '@openedx/paragon/icons';\n\nimport { ProductCardProps } from './data/types';\n\nimport './index.scss';\n\nexport const ProductCard: FC<ProductCardProps> = ({ item, isLoading }) => {\n return (\n <Card\n className=\"shadow-sm d-flex flex-column rounded w-100 overflow-hidden\"\n variant={item.isProgram ? 'dark' : 'light'}\n data-testid=\"product-card\"\n isLoading={isLoading}\n >\n <div className=\"position-relative\">\n <Card.ImageCap\n src={item.url}\n srcAlt={`${item.title} main-image`}\n data-testid=\"product-card-image\"\n logoSrc={item.thumbnail || ''}\n logoAlt={`${item.title} thumbnail-image`}\n />\n </div>\n\n <Card.Header title={item.title} />\n\n <Card.Section className=\"d-flex flex-column flex-grow-1 overflow-hidden\">\n <div className=\"flex-grow-1 min-h-0\">\n <p className=\"text-truncate-3 mb-2\">{item.body}</p>\n </div>\n\n {(item.isProgram && item.tagText) && (\n <Badge\n variant=\"light\"\n className=\"d-inline-flex align-items-center px-2 py-1 align-self-start\"\n >\n <Icon src={School} className=\"mr-2\" />\n {item.tagText}\n </Badge>\n )}\n </Card.Section>\n\n <Card.Footer className=\"d-flex justify-content-between align-items-center\">\n <span className={`small ${item.isProgram ? 'text-white' : 'text-muted'}`}>{item.footerLabel}</span>\n </Card.Footer>\n </Card>\n );\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/containers/ProductCard/index.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAIhD,OAAO,cAAc,CAAC;AAEtB,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAEnC,MAAM,YAAY,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAEnG,MAAM,YAAY,GAAG,CAAC,KAAa,EAAE,SAAiB,EAAE,EAAE,CAAC,CACzD,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAC/E,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAyB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE;IACvE,OAAO,CACL,MAAC,IAAI,IACH,SAAS,EAAC,4DAA4D,EACtE,OAAO,EAAE,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,iBACzC,cAAc,EAC1B,SAAS,EAAE,SAAS,aAEpB,cAAK,SAAS,EAAC,mBAAmB,YAChC,KAAC,IAAI,CAAC,QAAQ,IACZ,GAAG,EAAE,IAAI,CAAC,GAAG,EACb,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,aAAa,iBACtB,oBAAoB,EAChC,OAAO,EAAE,IAAI,CAAC,SAAS,IAAI,EAAE,EAC7B,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,kBAAkB,GACxC,GACE,EAEN,KAAC,IAAI,CAAC,MAAM,IAAC,KAAK,EAAE,IAAI,CAAC,KAAK,GAAI,EAElC,MAAC,IAAI,CAAC,OAAO,IAAC,SAAS,EAAC,gDAAgD,aACtE,cAAK,SAAS,EAAC,qBAAqB,YAClC,YAAG,SAAS,EAAC,sBAAsB,YAChC,YAAY,CAAC,YAAY,CAAC,IAAI,CAAC,mBAAmB,CAAC,EAAE,sBAAsB,CAAC,GAC3E,GACA,EAEL,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAC/C,MAAC,KAAK,IACJ,OAAO,EAAC,OAAO,EACf,SAAS,EAAC,6DAA6D,aAEvE,KAAC,IAAI,IAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAC,MAAM,GAAG,EACrC,IAAI,CAAC,OAAO,IACP,CACT,IACY,EAEf,KAAC,IAAI,CAAC,MAAM,IAAC,SAAS,EAAC,mDAAmD,YACxE,eAAM,SAAS,EAAE,SAAS,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,YAAY,EAAE,YAAG,IAAI,CAAC,WAAW,GAAQ,GACnG,IACT,CACR,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { FC } from 'react';\nimport { Card, Badge, Icon } from '@openedx/paragon';\nimport { School } from '@openedx/paragon/icons';\n\nimport { ProductCardProps } from './data/types';\n\nimport './index.scss';\n\nconst DESCRIPTION_MAX_LENGTH = 130;\n\nconst getPlainText = (value: string) => value.replace(/<[^>]*>/g, ' ').replace(/\\s+/g, ' ').trim();\n\nconst truncateText = (value: string, maxLength: number) => (\n value.length > maxLength ? `${value.slice(0, maxLength).trimEnd()}...` : value\n);\n\nexport const ProductCard: FC<ProductCardProps> = ({ item, isLoading }) => {\n return (\n <Card\n className=\"shadow-sm d-flex flex-column rounded w-100 overflow-hidden\"\n variant={item.product === 'Course' ? 'light' : 'dark'}\n data-testid=\"product-card\"\n isLoading={isLoading}\n >\n <div className=\"position-relative\">\n <Card.ImageCap\n src={item.url}\n srcAlt={`${item.title} main-image`}\n data-testid=\"product-card-image\"\n logoSrc={item.thumbnail || ''}\n logoAlt={`${item.title} thumbnail-image`}\n />\n </div>\n\n <Card.Header title={item.title} />\n\n <Card.Section className=\"d-flex flex-column flex-grow-1 overflow-hidden\">\n <div className=\"flex-grow-1 min-h-0\">\n <p className=\"text-truncate-3 mb-2\">\n {truncateText(getPlainText(item.primary_description), DESCRIPTION_MAX_LENGTH)}\n </p>\n </div>\n\n {(item.product === 'Program' && item.tagText) && (\n <Badge\n variant=\"light\"\n className=\"d-inline-flex align-items-center px-2 py-1 align-self-start\"\n >\n <Icon src={School} className=\"mr-2\" />\n {item.tagText}\n </Badge>\n )}\n </Card.Section>\n\n <Card.Footer className=\"d-flex justify-content-between align-items-center\">\n <span className={`small ${item.product === 'Program' ? 'text-white' : 'text-muted'}`}>{item.footerLabel}</span>\n </Card.Footer>\n </Card>\n );\n};\n"]}
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { ProductCard } from '../../ProductCard';
3
3
  export const RecentlyViewedCourseProgram = ({ items, isLoading }) => {
4
- return (_jsx("div", { "data-testid": "recently-viewed-items", children: _jsx("div", { className: "row g-3 flex-nowrap flex-md-wrap overflow-auto", children: items.map((item) => (_jsx("div", { className: "col-12 col-md-6 col-xl-3 d-flex mb-2", children: _jsx(ProductCard, { item: item, isLoading: isLoading }) }, item.id))) }) }));
4
+ return (_jsx("div", { "data-testid": "recently-viewed-items", children: _jsx("div", { className: "row g-3 flex-nowrap flex-md-wrap overflow-auto", children: items.map((item) => (_jsx("div", { className: "col-12 col-md-6 col-xl-3 d-flex mb-2", children: _jsx(ProductCard, { item: item, isLoading: isLoading }) }, item.objectID))) }) }));
5
5
  };
6
6
  export default RecentlyViewedCourseProgram;
7
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/containers/RecentlyViewedPanel/RecentlyViewedCourseProgram/index.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,MAAM,CAAC,MAAM,2BAA2B,GAAyC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE;IACxG,OAAO,CACL,6BAAiB,uBAAuB,YACtC,cAAK,SAAS,EAAC,gDAAgD,YAC5D,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACnB,cAAmB,SAAS,EAAC,sCAAsC,YACjE,KAAC,WAAW,IAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,GAAI,IADzC,IAAI,CAAC,EAAE,CAEX,CACP,CAAC,GACE,GACF,CACP,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,2BAA2B,CAAC","sourcesContent":["import { FC } from 'react';\n\nimport { ProductCard } from '../../ProductCard';\nimport { RecentlyViewedCourseProgramProps } from './data/types';\n\nexport const RecentlyViewedCourseProgram: FC<RecentlyViewedCourseProgramProps> = ({ items, isLoading }) => {\n return (\n <div data-testid=\"recently-viewed-items\">\n <div className=\"row g-3 flex-nowrap flex-md-wrap overflow-auto\">\n {items.map((item) => (\n <div key={item.id} className=\"col-12 col-md-6 col-xl-3 d-flex mb-2\">\n <ProductCard item={item} isLoading={isLoading} />\n </div>\n ))}\n </div>\n </div>\n );\n};\n\nexport default RecentlyViewedCourseProgram;\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/containers/RecentlyViewedPanel/RecentlyViewedCourseProgram/index.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhD,MAAM,CAAC,MAAM,2BAA2B,GAAyC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE;IACxG,OAAO,CACL,6BAAiB,uBAAuB,YACtC,cAAK,SAAS,EAAC,gDAAgD,YAC5D,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACnB,cAAyB,SAAS,EAAC,sCAAsC,YACvE,KAAC,WAAW,IAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,GAAI,IADzC,IAAI,CAAC,QAAQ,CAEjB,CACP,CAAC,GACE,GACF,CACP,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,2BAA2B,CAAC","sourcesContent":["import { FC } from 'react';\n\nimport { ProductCard } from '../../ProductCard';\nimport { RecentlyViewedCourseProgramProps } from './data/types';\n\nexport const RecentlyViewedCourseProgram: FC<RecentlyViewedCourseProgramProps> = ({ items, isLoading }) => {\n return (\n <div data-testid=\"recently-viewed-items\">\n <div className=\"row g-3 flex-nowrap flex-md-wrap overflow-auto\">\n {items.map((item) => (\n <div key={item.objectID} className=\"col-12 col-md-6 col-xl-3 d-flex mb-2\">\n <ProductCard item={item} isLoading={isLoading} />\n </div>\n ))}\n </div>\n </div>\n );\n};\n\nexport default RecentlyViewedCourseProgram;\n"]}
@@ -3,12 +3,14 @@ import { useMemo } from 'react';
3
3
  import { Alert } from '@openedx/paragon';
4
4
  import { useIntl } from '@openedx/frontend-base';
5
5
  import { useRecentlyViewedCoursesAndProgramsData } from '../../hooks/useRecentlyViewedCoursesAndProgramsData';
6
+ import { useAlgoliaSearch } from '../../utils/algoliaUtils';
6
7
  import messages from './messages';
7
8
  import NoRecentlyViewedItems from './NoRecentlyViewedItems';
8
9
  import RecentlyViewedCourseProgram from './RecentlyViewedCourseProgram';
9
10
  export const RecentlyViewedPanel = () => {
10
11
  const { formatMessage } = useIntl();
11
- const { data: recentlyViewedCoursesAndProgramsData = [], isError, isLoading, } = useRecentlyViewedCoursesAndProgramsData();
12
+ const [searchClient] = useAlgoliaSearch();
13
+ const { data: recentlyViewedCoursesAndProgramsData = [], isError, isLoading, } = useRecentlyViewedCoursesAndProgramsData(searchClient, true);
12
14
  const hasRecentlyViewedData = useMemo(() => recentlyViewedCoursesAndProgramsData.length > 0, [recentlyViewedCoursesAndProgramsData]);
13
15
  if (isError) {
14
16
  return (_jsx("div", { className: "recently-viewed-container mt-5 mb-3", "data-testid": "recently-viewed-panel", children: _jsx(Alert, { variant: "danger", "data-testid": "recently-viewed-error", children: formatMessage(messages.recentlyViewedErrorText) }) }));
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/containers/RecentlyViewedPanel/index.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAM,OAAO,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,uCAAuC,EAAE,MAAM,oDAAoD,CAAC;AAC7G,OAAO,QAAQ,MAAM,YAAY,CAAC;AAClC,OAAO,qBAAqB,MAAM,yBAAyB,CAAC;AAC5D,OAAO,2BAA2B,MAAM,+BAA+B,CAAC;AAExE,MAAM,CAAC,MAAM,mBAAmB,GAAO,GAAG,EAAE;IAC1C,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,EAAE,CAAC;IACpC,MAAM,EACJ,IAAI,EAAE,oCAAoC,GAAG,EAAE,EAC/C,OAAO,EACP,SAAS,GACV,GAAG,uCAAuC,EAAE,CAAC;IAC9C,MAAM,qBAAqB,GAAG,OAAO,CACnC,GAAG,EAAE,CAAC,oCAAoC,CAAC,MAAM,GAAG,CAAC,EACrD,CAAC,oCAAoC,CAAC,CACvC,CAAC;IAEF,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CACL,cAAK,SAAS,EAAC,qCAAqC,iBAAa,uBAAuB,YACtF,KAAC,KAAK,IAAC,OAAO,EAAC,QAAQ,iBAAa,uBAAuB,YACxD,aAAa,CAAC,QAAQ,CAAC,uBAAuB,CAAC,GAC1C,GACJ,CACP,CAAC;IACJ,CAAC;IAED,OAAO,CACL,eAAK,SAAS,EAAC,qCAAqC,iBAAa,uBAAuB,aACtF,cAAK,SAAS,EAAC,wCAAwC,YACrD,uBAAK,aAAa,CAAC,QAAQ,CAAC,oBAAoB,CAAC,GAAM,GACnD,EACL,CAAC,SAAS,IAAI,qBAAqB,CAAC,CAAC,CAAC,CAAC,CACtC,KAAC,2BAA2B,IAC1B,KAAK,EAAE,oCAAoC,EAC3C,SAAS,EAAE,SAAS,GACpB,CACH,CAAC,CAAC,CAAC,CACF,KAAC,qBAAqB,KAAG,CAC1B,IACG,CACP,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,mBAAmB,CAAC","sourcesContent":["import { FC, useMemo } from 'react';\nimport { Alert } from '@openedx/paragon';\nimport { useIntl } from '@openedx/frontend-base';\n\nimport { useRecentlyViewedCoursesAndProgramsData } from '@src/hooks/useRecentlyViewedCoursesAndProgramsData';\nimport messages from './messages';\nimport NoRecentlyViewedItems from './NoRecentlyViewedItems';\nimport RecentlyViewedCourseProgram from './RecentlyViewedCourseProgram';\n\nexport const RecentlyViewedPanel: FC = () => {\n const { formatMessage } = useIntl();\n const {\n data: recentlyViewedCoursesAndProgramsData = [],\n isError,\n isLoading,\n } = useRecentlyViewedCoursesAndProgramsData();\n const hasRecentlyViewedData = useMemo(\n () => recentlyViewedCoursesAndProgramsData.length > 0,\n [recentlyViewedCoursesAndProgramsData]\n );\n\n if (isError) {\n return (\n <div className=\"recently-viewed-container mt-5 mb-3\" data-testid=\"recently-viewed-panel\">\n <Alert variant=\"danger\" data-testid=\"recently-viewed-error\">\n {formatMessage(messages.recentlyViewedErrorText)}\n </Alert>\n </div>\n );\n }\n\n return (\n <div className=\"recently-viewed-container mt-5 mb-3\" data-testid=\"recently-viewed-panel\">\n <div className=\"recently-viewed-heading-container mb-4\">\n <h2>{formatMessage(messages.recentlyViewedHeader)}</h2>\n </div>\n {(isLoading || hasRecentlyViewedData) ? (\n <RecentlyViewedCourseProgram\n items={recentlyViewedCoursesAndProgramsData}\n isLoading={isLoading}\n />\n ) : (\n <NoRecentlyViewedItems />\n )}\n </div>\n );\n};\n\nexport default RecentlyViewedPanel;\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/containers/RecentlyViewedPanel/index.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAM,OAAO,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,uCAAuC,EAAE,MAAM,oDAAoD,CAAC;AAC7G,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,QAAQ,MAAM,YAAY,CAAC;AAClC,OAAO,qBAAqB,MAAM,yBAAyB,CAAC;AAC5D,OAAO,2BAA2B,MAAM,+BAA+B,CAAC;AAExE,MAAM,CAAC,MAAM,mBAAmB,GAAO,GAAG,EAAE;IAC1C,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,EAAE,CAAC;IACpC,MAAM,CAAC,YAAY,CAAC,GAAG,gBAAgB,EAAE,CAAC;IAC1C,MAAM,EACJ,IAAI,EAAE,oCAAoC,GAAG,EAAE,EAC/C,OAAO,EACP,SAAS,GACV,GAAG,uCAAuC,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;IAChE,MAAM,qBAAqB,GAAG,OAAO,CACnC,GAAG,EAAE,CAAC,oCAAoC,CAAC,MAAM,GAAG,CAAC,EACrD,CAAC,oCAAoC,CAAC,CACvC,CAAC;IAEF,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CACL,cAAK,SAAS,EAAC,qCAAqC,iBAAa,uBAAuB,YACtF,KAAC,KAAK,IAAC,OAAO,EAAC,QAAQ,iBAAa,uBAAuB,YACxD,aAAa,CAAC,QAAQ,CAAC,uBAAuB,CAAC,GAC1C,GACJ,CACP,CAAC;IACJ,CAAC;IAED,OAAO,CACL,eAAK,SAAS,EAAC,qCAAqC,iBAAa,uBAAuB,aACtF,cAAK,SAAS,EAAC,wCAAwC,YACrD,uBAAK,aAAa,CAAC,QAAQ,CAAC,oBAAoB,CAAC,GAAM,GACnD,EACL,CAAC,SAAS,IAAI,qBAAqB,CAAC,CAAC,CAAC,CAAC,CACtC,KAAC,2BAA2B,IAC1B,KAAK,EAAE,oCAAoC,EAC3C,SAAS,EAAE,SAAS,GACpB,CACH,CAAC,CAAC,CAAC,CACF,KAAC,qBAAqB,KAAG,CAC1B,IACG,CACP,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,mBAAmB,CAAC","sourcesContent":["import { FC, useMemo } from 'react';\nimport { Alert } from '@openedx/paragon';\nimport { useIntl } from '@openedx/frontend-base';\n\nimport { useRecentlyViewedCoursesAndProgramsData } from '@src/hooks/useRecentlyViewedCoursesAndProgramsData';\nimport { useAlgoliaSearch } from '@src/utils/algoliaUtils';\nimport messages from './messages';\nimport NoRecentlyViewedItems from './NoRecentlyViewedItems';\nimport RecentlyViewedCourseProgram from './RecentlyViewedCourseProgram';\n\nexport const RecentlyViewedPanel: FC = () => {\n const { formatMessage } = useIntl();\n const [searchClient] = useAlgoliaSearch();\n const {\n data: recentlyViewedCoursesAndProgramsData = [],\n isError,\n isLoading,\n } = useRecentlyViewedCoursesAndProgramsData(searchClient, true);\n const hasRecentlyViewedData = useMemo(\n () => recentlyViewedCoursesAndProgramsData.length > 0,\n [recentlyViewedCoursesAndProgramsData]\n );\n\n if (isError) {\n return (\n <div className=\"recently-viewed-container mt-5 mb-3\" data-testid=\"recently-viewed-panel\">\n <Alert variant=\"danger\" data-testid=\"recently-viewed-error\">\n {formatMessage(messages.recentlyViewedErrorText)}\n </Alert>\n </div>\n );\n }\n\n return (\n <div className=\"recently-viewed-container mt-5 mb-3\" data-testid=\"recently-viewed-panel\">\n <div className=\"recently-viewed-heading-container mb-4\">\n <h2>{formatMessage(messages.recentlyViewedHeader)}</h2>\n </div>\n {(isLoading || hasRecentlyViewedData) ? (\n <RecentlyViewedCourseProgram\n items={recentlyViewedCoursesAndProgramsData}\n isLoading={isLoading}\n />\n ) : (\n <NoRecentlyViewedItems />\n )}\n </div>\n );\n};\n\nexport default RecentlyViewedPanel;\n"]}
@@ -1,24 +1,4 @@
1
1
  export declare const initializeSubsList: (user: any) => Promise<any>;
2
2
  export declare const initializeCourseList: (user: any) => Promise<any>;
3
- export declare const fetchRecommendedCourses: () => Promise<{
4
- id: number;
5
- title: string;
6
- body: string;
7
- url: string;
8
- thumbnail: string;
9
- isProgram: boolean;
10
- tagText: string;
11
- footerLabel: string;
12
- }[]>;
13
- export declare const fetchRecentlyViewedCoursesAndPrograms: () => Promise<{
14
- id: number;
15
- title: string;
16
- body: string;
17
- url: string;
18
- thumbnail: string;
19
- isProgram: boolean;
20
- tagText: string;
21
- footerLabel: string;
22
- }[]>;
23
3
  export declare const getProgramProgressData: (uuid: string) => Promise<any>;
24
4
  export declare const getProgramsListData: () => Promise<any>;
@@ -20,117 +20,6 @@ export const initializeCourseList = (user) => __awaiter(void 0, void 0, void 0,
20
20
  const { data } = yield getAuthenticatedHttpClient().get(stringifyUrl(urls.getSubsCoursesApiUrl(), { [apiKeys.user]: user }));
21
21
  return data;
22
22
  });
23
- const optimizedData = (data, size) => (data.length > size ? `${data.slice(0, size)}...` : data);
24
- export const fetchRecommendedCourses = () => __awaiter(void 0, void 0, void 0, function* () {
25
- /* TODO [TEMP]: Replace with actual API call to fetch recommended courses data. For now, returning hardcoded data to simulate the API response. Also, built the test case for the same.
26
- Reason: The API endpoint is not yet available and the data structure is still being finalized.
27
- Action: Revisit after endpoint is made available.
28
- */
29
- try {
30
- const finalData = [
31
- {
32
- id: 1,
33
- title: optimizedData('Essence Mascara Lash Princess', 30),
34
- body: optimizedData('The Essence Mascara Lash Princess is a popular mascara known for its volumizing and lengthening effects. Achieve dramatic lashes with this long-lasting and cruelty-free formula.', 100),
35
- url: 'https://cdn.dummyjson.com/product-images/beauty/essence-mascara-lash-princess/1.webp',
36
- thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/essence-mascara-lash-princess/thumbnail.webp',
37
- isProgram: false,
38
- tagText: '',
39
- footerLabel: 'Course'
40
- },
41
- {
42
- id: 2,
43
- title: optimizedData('Eyeshadow Palette with Mirror', 30),
44
- body: optimizedData("The Eyeshadow Palette with Mirror offers a versatile range of eyeshadow shades for creating stunning eye looks. With a built-in mirror, it's convenient for on-the-go makeup application.", 100),
45
- url: 'https://cdn.dummyjson.com/product-images/beauty/eyeshadow-palette-with-mirror/1.webp',
46
- thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/eyeshadow-palette-with-mirror/thumbnail.webp',
47
- isProgram: false,
48
- tagText: '',
49
- footerLabel: 'Course'
50
- },
51
- {
52
- id: 3,
53
- title: optimizedData('Powder Canister', 30),
54
- body: optimizedData('The Powder Canister is a finely milled setting powder designed to set makeup and control shine. With a lightweight and translucent formula, it provides a smooth and matte finish.', 100),
55
- url: 'https://cdn.dummyjson.com/product-images/beauty/powder-canister/1.webp',
56
- thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/powder-canister/thumbnail.webp',
57
- isProgram: true, // TODO
58
- tagText: 'Professional Certificate',
59
- footerLabel: '2 Courses'
60
- },
61
- {
62
- id: 4,
63
- title: optimizedData('Red Lipstick', 30),
64
- body: optimizedData('The Red Lipstick is a classic and bold choice for adding a pop of color to your lips. With a creamy and pigmented formula, it provides a vibrant and long-lasting finish.', 100),
65
- url: 'https://cdn.dummyjson.com/product-images/beauty/red-lipstick/1.webp',
66
- thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/red-lipstick/thumbnail.webp',
67
- isProgram: false,
68
- tagText: '',
69
- footerLabel: 'Course'
70
- }
71
- ];
72
- return finalData;
73
- }
74
- catch (error) {
75
- console.error('Error fetching cards data:', error);
76
- throw error;
77
- }
78
- });
79
- export const fetchRecentlyViewedCoursesAndPrograms = () => __awaiter(void 0, void 0, void 0, function* () {
80
- /* TODO [TEMP]: Replace with actual API call to fetch recently viewed courses and programs data.
81
- Reason: The API endpoint is not yet available and the data structure is still being finalized.
82
- Action: Revisit after endpoint is made available.
83
- */
84
- try {
85
- const finalData = [
86
- {
87
- id: 1,
88
- title: optimizedData('Data Analytics Fundamentals', 30),
89
- body: optimizedData('Learn the core concepts of data analytics, including data cleaning, visualization, and basic statistical interpretation.', 100),
90
- url: 'https://cdn.dummyjson.com/product-images/beauty/essence-mascara-lash-princess/1.webp',
91
- thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/essence-mascara-lash-princess/thumbnail.webp',
92
- isProgram: false,
93
- tagText: '',
94
- footerLabel: 'Course'
95
- },
96
- {
97
- id: 2,
98
- title: optimizedData('Product Management Essentials', 30),
99
- body: optimizedData('Review the workflows, tools, and communication patterns used by product managers to guide delivery across teams.', 100),
100
- url: 'https://cdn.dummyjson.com/product-images/beauty/eyeshadow-palette-with-mirror/1.webp',
101
- thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/eyeshadow-palette-with-mirror/thumbnail.webp',
102
- isProgram: false,
103
- tagText: '',
104
- footerLabel: 'Course'
105
- },
106
- {
107
- id: 3,
108
- title: optimizedData('AI Product Strategy', 30),
109
- body: optimizedData('Continue a professional certificate covering AI roadmap planning, experimentation, and responsible product delivery.', 100),
110
- url: 'https://cdn.dummyjson.com/product-images/beauty/powder-canister/1.webp',
111
- thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/powder-canister/thumbnail.webp',
112
- isProgram: true,
113
- tagText: 'Professional Certificate',
114
- footerLabel: '3 Courses'
115
- },
116
- {
117
- id: 4,
118
- title: optimizedData('Red Lipstick', 30),
119
- body: optimizedData('The Red Lipstick is a classic and bold choice for adding a pop of color to your lips. With a creamy and pigmented formula, it provides a vibrant and long-lasting finish.', 100),
120
- url: 'https://cdn.dummyjson.com/product-images/beauty/red-lipstick/1.webp',
121
- thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/red-lipstick/thumbnail.webp',
122
- isProgram: false,
123
- tagText: '',
124
- footerLabel: 'Course'
125
- }
126
- ];
127
- return finalData;
128
- }
129
- catch (error) {
130
- console.error('Error fetching recently viewed courses and programs data:', error);
131
- throw error;
132
- }
133
- });
134
23
  export const getProgramProgressData = (uuid) => __awaiter(void 0, void 0, void 0, function* () {
135
24
  try {
136
25
  const { data } = yield getAuthenticatedHttpClient().get(programProgressUrl(uuid));
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../../../../src/data/services/subs/api.ts"],"names":[],"mappings":"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","sourcesContent":["import { getAuthenticatedHttpClient } from '@openedx/frontend-base';\nimport { programProgressUrl, programsApiUrl } from './urls';\n\nimport { apiKeys } from '@src/data/services/lms/constants';\nimport urls from '@src/data/services/subs/urls';\nimport { stringifyUrl } from '@src/data/services/lms/utils';\n\nexport const initializeSubsList = async (user) => {\n const { data } = await getAuthenticatedHttpClient().get(\n stringifyUrl(urls.getSubsInitApiUrl(), { [apiKeys.user]: user }),\n );\n return data;\n};\n\nexport const initializeCourseList = async (user) => {\n const { data } = await getAuthenticatedHttpClient().get(\n stringifyUrl(urls.getSubsCoursesApiUrl(), { [apiKeys.user]: user }),\n );\n return data;\n};\n\nconst optimizedData = (data: string, size: number) => (data.length > size ? `${data.slice(0, size)}...` : data);\n\nexport const fetchRecommendedCourses = async () => {\n /* TODO [TEMP]: Replace with actual API call to fetch recommended courses data. For now, returning hardcoded data to simulate the API response. Also, built the test case for the same.\n Reason: The API endpoint is not yet available and the data structure is still being finalized.\n Action: Revisit after endpoint is made available.\n */\n\n try {\n const finalData = [\n {\n id: 1,\n title: optimizedData('Essence Mascara Lash Princess', 30),\n body: optimizedData('The Essence Mascara Lash Princess is a popular mascara known for its volumizing and lengthening effects. Achieve dramatic lashes with this long-lasting and cruelty-free formula.', 100),\n url: 'https://cdn.dummyjson.com/product-images/beauty/essence-mascara-lash-princess/1.webp',\n thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/essence-mascara-lash-princess/thumbnail.webp',\n isProgram: false,\n tagText: '',\n footerLabel: 'Course'\n },\n {\n id: 2,\n title: optimizedData('Eyeshadow Palette with Mirror', 30),\n body: optimizedData(\"The Eyeshadow Palette with Mirror offers a versatile range of eyeshadow shades for creating stunning eye looks. With a built-in mirror, it's convenient for on-the-go makeup application.\", 100),\n url: 'https://cdn.dummyjson.com/product-images/beauty/eyeshadow-palette-with-mirror/1.webp',\n thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/eyeshadow-palette-with-mirror/thumbnail.webp',\n isProgram: false,\n tagText: '',\n footerLabel: 'Course'\n },\n {\n id: 3,\n title: optimizedData('Powder Canister', 30),\n body: optimizedData('The Powder Canister is a finely milled setting powder designed to set makeup and control shine. With a lightweight and translucent formula, it provides a smooth and matte finish.', 100),\n url: 'https://cdn.dummyjson.com/product-images/beauty/powder-canister/1.webp',\n thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/powder-canister/thumbnail.webp',\n isProgram: true, // TODO\n tagText: 'Professional Certificate',\n footerLabel: '2 Courses'\n },\n {\n id: 4,\n title: optimizedData('Red Lipstick', 30),\n body: optimizedData('The Red Lipstick is a classic and bold choice for adding a pop of color to your lips. With a creamy and pigmented formula, it provides a vibrant and long-lasting finish.', 100),\n url: 'https://cdn.dummyjson.com/product-images/beauty/red-lipstick/1.webp',\n thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/red-lipstick/thumbnail.webp',\n isProgram: false,\n tagText: '',\n footerLabel: 'Course'\n }\n ];\n\n return finalData;\n } catch (error) {\n console.error('Error fetching cards data:', error);\n throw error;\n }\n};\n\nexport const fetchRecentlyViewedCoursesAndPrograms = async () => {\n /* TODO [TEMP]: Replace with actual API call to fetch recently viewed courses and programs data.\n Reason: The API endpoint is not yet available and the data structure is still being finalized.\n Action: Revisit after endpoint is made available.\n */\n\n try {\n const finalData = [\n {\n id: 1,\n title: optimizedData('Data Analytics Fundamentals', 30),\n body: optimizedData('Learn the core concepts of data analytics, including data cleaning, visualization, and basic statistical interpretation.', 100),\n url: 'https://cdn.dummyjson.com/product-images/beauty/essence-mascara-lash-princess/1.webp',\n thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/essence-mascara-lash-princess/thumbnail.webp',\n isProgram: false,\n tagText: '',\n footerLabel: 'Course'\n },\n {\n id: 2,\n title: optimizedData('Product Management Essentials', 30),\n body: optimizedData('Review the workflows, tools, and communication patterns used by product managers to guide delivery across teams.', 100),\n url: 'https://cdn.dummyjson.com/product-images/beauty/eyeshadow-palette-with-mirror/1.webp',\n thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/eyeshadow-palette-with-mirror/thumbnail.webp',\n isProgram: false,\n tagText: '',\n footerLabel: 'Course'\n },\n {\n id: 3,\n title: optimizedData('AI Product Strategy', 30),\n body: optimizedData('Continue a professional certificate covering AI roadmap planning, experimentation, and responsible product delivery.', 100),\n url: 'https://cdn.dummyjson.com/product-images/beauty/powder-canister/1.webp',\n thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/powder-canister/thumbnail.webp',\n isProgram: true,\n tagText: 'Professional Certificate',\n footerLabel: '3 Courses'\n },\n {\n id: 4,\n title: optimizedData('Red Lipstick', 30),\n body: optimizedData('The Red Lipstick is a classic and bold choice for adding a pop of color to your lips. With a creamy and pigmented formula, it provides a vibrant and long-lasting finish.', 100),\n url: 'https://cdn.dummyjson.com/product-images/beauty/red-lipstick/1.webp',\n thumbnail: 'https://cdn.dummyjson.com/product-images/beauty/red-lipstick/thumbnail.webp',\n isProgram: false,\n tagText: '',\n footerLabel: 'Course'\n }\n ];\n\n return finalData;\n } catch (error) {\n console.error('Error fetching recently viewed courses and programs data:', error);\n throw error;\n }\n};\n\nexport const getProgramProgressData = async (uuid: string) => {\n try {\n const { data } = await getAuthenticatedHttpClient().get(programProgressUrl(uuid));\n return data;\n } catch (error) {\n console.error('Error fetching program progress data:', error);\n throw error;\n }\n};\n\nexport const getProgramsListData = async () => {\n try {\n const { data } = await getAuthenticatedHttpClient().get(programsApiUrl());\n return data;\n } catch (error) {\n console.error('Error fetching program list data:', error);\n throw error;\n }\n};\n"]}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../../../src/data/services/subs/api.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAE5D,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAC3D,OAAO,IAAI,MAAM,8BAA8B,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAO,IAAI,EAAE,EAAE;IAC/C,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,0BAA0B,EAAE,CAAC,GAAG,CACrD,YAAY,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CACjE,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC,CAAA,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAO,IAAI,EAAE,EAAE;IACjD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,0BAA0B,EAAE,CAAC,GAAG,CACrD,YAAY,CAAC,IAAI,CAAC,oBAAoB,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CACpE,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC,CAAA,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAO,IAAY,EAAE,EAAE;IAC3D,IAAI,CAAC;QACH,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,0BAA0B,EAAE,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;QAClF,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,uCAAuC,EAAE,KAAK,CAAC,CAAC;QAC9D,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC,CAAA,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAS,EAAE;IAC5C,IAAI,CAAC;QACH,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,0BAA0B,EAAE,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC;QAC1E,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,KAAK,CAAC,CAAC;QAC1D,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC,CAAA,CAAC","sourcesContent":["import { getAuthenticatedHttpClient } from '@openedx/frontend-base';\nimport { programProgressUrl, programsApiUrl } from './urls';\n\nimport { apiKeys } from '@src/data/services/lms/constants';\nimport urls from '@src/data/services/subs/urls';\nimport { stringifyUrl } from '@src/data/services/lms/utils';\n\nexport const initializeSubsList = async (user) => {\n const { data } = await getAuthenticatedHttpClient().get(\n stringifyUrl(urls.getSubsInitApiUrl(), { [apiKeys.user]: user }),\n );\n return data;\n};\n\nexport const initializeCourseList = async (user) => {\n const { data } = await getAuthenticatedHttpClient().get(\n stringifyUrl(urls.getSubsCoursesApiUrl(), { [apiKeys.user]: user }),\n );\n return data;\n};\n\nexport const getProgramProgressData = async (uuid: string) => {\n try {\n const { data } = await getAuthenticatedHttpClient().get(programProgressUrl(uuid));\n return data;\n } catch (error) {\n console.error('Error fetching program progress data:', error);\n throw error;\n }\n};\n\nexport const getProgramsListData = async () => {\n try {\n const { data } = await getAuthenticatedHttpClient().get(programsApiUrl());\n return data;\n } catch (error) {\n console.error('Error fetching program list data:', error);\n throw error;\n }\n};\n"]}
@@ -1 +1 @@
1
- export { fetchRecommendedCourses, fetchRecentlyViewedCoursesAndPrograms, getProgramProgressData, getProgramsListData, initializeCourseList, initializeSubsList, } from './api';
1
+ export { getProgramProgressData, getProgramsListData, initializeCourseList, initializeSubsList, } from './api';
@@ -1,2 +1,2 @@
1
- export { fetchRecommendedCourses, fetchRecentlyViewedCoursesAndPrograms, getProgramProgressData, getProgramsListData, initializeCourseList, initializeSubsList, } from './api';
1
+ export { getProgramProgressData, getProgramsListData, initializeCourseList, initializeSubsList, } from './api';
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/data/services/subs/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,qCAAqC,EACrC,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,OAAO,CAAC","sourcesContent":["export {\n fetchRecommendedCourses,\n fetchRecentlyViewedCoursesAndPrograms,\n getProgramProgressData,\n getProgramsListData,\n initializeCourseList,\n initializeSubsList,\n} from './api';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/data/services/subs/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,OAAO,CAAC","sourcesContent":["export {\n getProgramProgressData,\n getProgramsListData,\n initializeCourseList,\n initializeSubsList,\n} from './api';\n"]}
@@ -1,15 +1,11 @@
1
- import { UseQueryOptions } from '@tanstack/react-query';
2
- import { fetchRecentlyViewedCoursesAndPrograms } from '../data/services/subs';
3
- type RecentlyViewedCoursesAndProgramsData = Awaited<ReturnType<typeof fetchRecentlyViewedCoursesAndPrograms>>;
4
- type UseRecentlyViewedCoursesAndProgramsOptions = Omit<UseQueryOptions<RecentlyViewedCoursesAndProgramsData>, 'queryKey' | 'queryFn'>;
5
- export declare const useRecentlyViewedCoursesAndProgramsData: (options?: UseRecentlyViewedCoursesAndProgramsOptions) => import("@tanstack/react-query").UseQueryResult<{
6
- id: number;
7
- title: string;
8
- body: string;
9
- url: string;
10
- thumbnail: string;
11
- isProgram: boolean;
12
- tagText: string;
13
- footerLabel: string;
14
- }[], Error>;
15
- export {};
1
+ import { UseQueryResult } from '@tanstack/react-query';
2
+ import { SearchClient } from 'algoliasearch';
3
+ import { ProductType } from '../types/types';
4
+ import { ProductCardItem } from '@src/containers/ProductCard/data/types';
5
+ export declare const searchKeys: {
6
+ all: readonly ["search"];
7
+ programs: (query: string, tags: string[]) => readonly ["search", "programs", string, string[]];
8
+ courses: (query: string, tags: string[], page: number) => readonly ["search", "courses", string, string[], number];
9
+ popular: (productType: ProductType, limit: number) => readonly ["search", "popular", ProductType, number];
10
+ };
11
+ export declare const useRecentlyViewedCoursesAndProgramsData: (searchClient: SearchClient | null, enabled: boolean) => UseQueryResult<ProductCardItem[]>;
@@ -1,6 +1,42 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { useQuery } from '@tanstack/react-query';
2
- import { fetchRecentlyViewedCoursesAndPrograms } from '../data/services/subs';
3
- export const useRecentlyViewedCoursesAndProgramsData = (options) => {
4
- return useQuery(Object.assign({ queryKey: ['recentlyViewedCoursesAndPrograms'], queryFn: fetchRecentlyViewedCoursesAndPrograms, staleTime: 1000 * 60 * 5, retry: 2 }, options));
11
+ import { PRODUCT_TYPE_COURSE } from '../constants';
12
+ import { fetchPopularProducts } from '../utils/algoliaUtils';
13
+ // ─── Query key factories ──────────────────────────────────────────────────────
14
+ // Centralised so cache invalidation is never a typo
15
+ export const searchKeys = {
16
+ all: ['search'],
17
+ programs: (query, tags) => [...searchKeys.all, 'programs', query, tags],
18
+ courses: (query, tags, page) => [...searchKeys.all, 'courses', query, tags, page],
19
+ popular: (productType, limit) => [...searchKeys.all, 'popular', productType, limit],
5
20
  };
21
+ export const useRecentlyViewedCoursesAndProgramsData = (searchClient, enabled) => useQuery({
22
+ queryKey: searchKeys.popular(PRODUCT_TYPE_COURSE, 4),
23
+ queryFn: () => __awaiter(void 0, void 0, void 0, function* () {
24
+ const hits = yield fetchPopularProducts(searchClient, PRODUCT_TYPE_COURSE, 4);
25
+ return hits.map((hit) => {
26
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
27
+ const hitDetails = hit;
28
+ return {
29
+ objectID: hit.objectID,
30
+ title: (_b = (_a = hit.title) !== null && _a !== void 0 ? _a : hit.name) !== null && _b !== void 0 ? _b : 'Untitled',
31
+ primary_description: (_e = (_d = (_c = hitDetails.primary_description) !== null && _c !== void 0 ? _c : hitDetails.short_description) !== null && _d !== void 0 ? _d : hitDetails.description) !== null && _e !== void 0 ? _e : '',
32
+ url: (_h = (_g = (_f = hitDetails.card_image_url) !== null && _f !== void 0 ? _f : hitDetails.image_url) !== null && _g !== void 0 ? _g : hitDetails.marketing_url) !== null && _h !== void 0 ? _h : '',
33
+ thumbnail: (_j = hitDetails.organization_logo_override) !== null && _j !== void 0 ? _j : '',
34
+ footerLabel: (_l = (_k = hitDetails.content_type) !== null && _k !== void 0 ? _k : hitDetails.type) !== null && _l !== void 0 ? _l : 'Course',
35
+ product: 'Course',
36
+ };
37
+ });
38
+ }),
39
+ enabled: !!searchClient && enabled,
40
+ staleTime: 1000 * 60 * 10,
41
+ });
6
42
  //# sourceMappingURL=useRecentlyViewedCoursesAndProgramsData.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useRecentlyViewedCoursesAndProgramsData.js","sourceRoot":"","sources":["../../src/hooks/useRecentlyViewedCoursesAndProgramsData.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAmB,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,qCAAqC,EAAE,MAAM,yBAAyB,CAAC;AAShF,MAAM,CAAC,MAAM,uCAAuC,GAAG,CACrD,OAAoD,EACpD,EAAE;IACF,OAAO,QAAQ,iBACb,QAAQ,EAAE,CAAC,kCAAkC,CAAC,EAC9C,OAAO,EAAE,qCAAqC,EAC9C,SAAS,EAAE,IAAI,GAAG,EAAE,GAAG,CAAC,EACxB,KAAK,EAAE,CAAC,IACL,OAAO,EACV,CAAC;AACL,CAAC,CAAC","sourcesContent":["import { useQuery, UseQueryOptions } from '@tanstack/react-query';\nimport { fetchRecentlyViewedCoursesAndPrograms } from '@src/data/services/subs';\n\ntype RecentlyViewedCoursesAndProgramsData = Awaited<ReturnType<typeof fetchRecentlyViewedCoursesAndPrograms>>;\n\ntype UseRecentlyViewedCoursesAndProgramsOptions = Omit<\n UseQueryOptions<RecentlyViewedCoursesAndProgramsData>,\n 'queryKey' | 'queryFn'\n>;\n\nexport const useRecentlyViewedCoursesAndProgramsData = (\n options?: UseRecentlyViewedCoursesAndProgramsOptions,\n) => {\n return useQuery({\n queryKey: ['recentlyViewedCoursesAndPrograms'],\n queryFn: fetchRecentlyViewedCoursesAndPrograms,\n staleTime: 1000 * 60 * 5,\n retry: 2,\n ...options,\n });\n};\n"]}
1
+ {"version":3,"file":"useRecentlyViewedCoursesAndProgramsData.js","sourceRoot":"","sources":["../../src/hooks/useRecentlyViewedCoursesAndProgramsData.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,QAAQ,EAAkB,MAAM,uBAAuB,CAAC;AAGjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAG/D,iFAAiF;AACjF,oDAAoD;AAEpD,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,GAAG,EAAE,CAAC,QAAQ,CAAU;IACxB,QAAQ,EAAE,CAAC,KAAa,EAAE,IAAc,EAAE,EAAE,CAC1C,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,CAAU;IACvD,OAAO,EAAE,CAAC,KAAa,EAAE,IAAc,EAAE,IAAY,EAAE,EAAE,CACvD,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAU;IAC5D,OAAO,EAAE,CAAC,WAAwB,EAAE,KAAa,EAAE,EAAE,CACnD,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,CAAU;CAC9D,CAAC;AAEF,MAAM,CAAC,MAAM,uCAAuC,GAAG,CACrD,YAAiC,EACjC,OAAgB,EACmB,EAAE,CACrC,QAAQ,CAAC;IACP,QAAQ,EAAE,UAAU,CAAC,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;IACpD,OAAO,EAAE,GAAS,EAAE;QAClB,MAAM,IAAI,GAAG,MAAM,oBAAoB,CAAC,YAAa,EAAE,mBAAmB,EAAE,CAAC,CAAC,CAAC;QAC/E,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAmB,EAAE;;YACvC,MAAM,UAAU,GAAG,GAUlB,CAAC;YAEF,OAAO;gBACL,QAAQ,EAAE,GAAG,CAAC,QAAQ;gBACtB,KAAK,EAAE,MAAA,MAAA,GAAG,CAAC,KAAK,mCAAI,GAAG,CAAC,IAAI,mCAAI,UAAU;gBAC1C,mBAAmB,EAAE,MAAA,MAAA,MAAA,UAAU,CAAC,mBAAmB,mCAAI,UAAU,CAAC,iBAAiB,mCAAI,UAAU,CAAC,WAAW,mCAAI,EAAE;gBACnH,GAAG,EAAE,MAAA,MAAA,MAAA,UAAU,CAAC,cAAc,mCAAI,UAAU,CAAC,SAAS,mCAAI,UAAU,CAAC,aAAa,mCAAI,EAAE;gBACxF,SAAS,EAAE,MAAA,UAAU,CAAC,0BAA0B,mCAAI,EAAE;gBACtD,WAAW,EAAE,MAAA,MAAA,UAAU,CAAC,YAAY,mCAAI,UAAU,CAAC,IAAI,mCAAI,QAAQ;gBACnE,OAAO,EAAE,QAAQ;aAClB,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAA;IACD,OAAO,EAAE,CAAC,CAAC,YAAY,IAAI,OAAO;IAClC,SAAS,EAAE,IAAI,GAAG,EAAE,GAAG,EAAE;CAC1B,CAAC,CAAC","sourcesContent":["import { useQuery, UseQueryResult } from '@tanstack/react-query';\nimport { SearchClient } from 'algoliasearch';\nimport { ProductType } from '@src/types/types';\nimport { PRODUCT_TYPE_COURSE } from '@src/constants';\nimport { fetchPopularProducts } from '@src/utils/algoliaUtils';\nimport { ProductCardItem } from '@src/containers/ProductCard/data/types';\n\n// ─── Query key factories ──────────────────────────────────────────────────────\n// Centralised so cache invalidation is never a typo\n\nexport const searchKeys = {\n all: ['search'] as const,\n programs: (query: string, tags: string[]) =>\n [...searchKeys.all, 'programs', query, tags] as const,\n courses: (query: string, tags: string[], page: number) =>\n [...searchKeys.all, 'courses', query, tags, page] as const,\n popular: (productType: ProductType, limit: number) =>\n [...searchKeys.all, 'popular', productType, limit] as const,\n};\n\nexport const useRecentlyViewedCoursesAndProgramsData = (\n searchClient: SearchClient | null,\n enabled: boolean,\n): UseQueryResult<ProductCardItem[]> =>\n useQuery({\n queryKey: searchKeys.popular(PRODUCT_TYPE_COURSE, 4),\n queryFn: async () => {\n const hits = await fetchPopularProducts(searchClient!, PRODUCT_TYPE_COURSE, 4);\n return hits.map((hit): ProductCardItem => {\n const hitDetails = hit as {\n short_description?: string,\n primary_description?: string,\n description?: string,\n marketing_url?: string,\n image_url?: string,\n card_image_url?: string,\n organization_logo_override?: string,\n content_type?: string,\n type?: string,\n };\n\n return {\n objectID: hit.objectID,\n title: hit.title ?? hit.name ?? 'Untitled',\n primary_description: hitDetails.primary_description ?? hitDetails.short_description ?? hitDetails.description ?? '',\n url: hitDetails.card_image_url ?? hitDetails.image_url ?? hitDetails.marketing_url ?? '',\n thumbnail: hitDetails.organization_logo_override ?? '',\n footerLabel: hitDetails.content_type ?? hitDetails.type ?? 'Course',\n product: 'Course',\n };\n });\n },\n enabled: !!searchClient && enabled,\n staleTime: 1000 * 60 * 10,\n });\n"]}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * types/index.ts
3
+ *
4
+ * Shared TypeScript interfaces and types for the Search feature.
5
+ */
6
+ /** Raw shape returned by Algolia for a product hit. Only the fields we access. */
7
+ export interface AlgoliaProductHit {
8
+ objectID: string;
9
+ title?: string;
10
+ name?: string;
11
+ type?: string;
12
+ content_type?: string;
13
+ institution?: string;
14
+ course_count?: number;
15
+ courseCount?: number;
16
+ card_image_url?: string;
17
+ image_url?: string;
18
+ authoring_organizations?: {
19
+ name: string;
20
+ }[];
21
+ partners?: {
22
+ name: string;
23
+ }[];
24
+ [key: string]: unknown;
25
+ }
26
+ /** Result shape returned by searchProducts / fetchPopularProducts */
27
+ export interface AlgoliaSearchResult {
28
+ hits: AlgoliaProductHit[];
29
+ nbHits: number;
30
+ nbPages: number;
31
+ }
32
+ export type ProductType = 'program' | 'course';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * types/index.ts
3
+ *
4
+ * Shared TypeScript interfaces and types for the Search feature.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG","sourcesContent":["/**\n * types/index.ts\n *\n * Shared TypeScript interfaces and types for the Search feature.\n */\n\n// ─── Algolia raw hit ──────────────────────────────────────────────────────────\n\n/** Raw shape returned by Algolia for a product hit. Only the fields we access. */\nexport interface AlgoliaProductHit {\n objectID: string,\n title?: string,\n name?: string,\n type?: string,\n content_type?: string,\n institution?: string,\n course_count?: number,\n courseCount?: number,\n card_image_url?: string,\n image_url?: string,\n authoring_organizations?: { name: string }[],\n partners?: { name: string }[],\n [key: string]: unknown, // allow arbitrary extra fields\n}\n\n/** Result shape returned by searchProducts / fetchPopularProducts */\nexport interface AlgoliaSearchResult {\n hits: AlgoliaProductHit[],\n nbHits: number,\n nbPages: number,\n}\n\n// ─── Product type union ───────────────────────────────────────────────────────\n\nexport type ProductType = 'program' | 'course';\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * utils/algoliaUtils.ts
3
+ *
4
+ * Algolia utility functions for the Search feature.
5
+ * Mirrors the pattern used by the Skills Builder feature, with full TypeScript types.
6
+ */
7
+ import { SearchClient } from 'algoliasearch';
8
+ import { AlgoliaProductHit, AlgoliaSearchResult, ProductType } from '../types/types';
9
+ /**
10
+ * Creates and memoises an Algolia search client using app config.
11
+ * Matches the pattern used by the Skills Builder feature.
12
+ *
13
+ * @returns [SearchClient | null] tuple — destructure as: const [searchClient] = useAlgoliaSearch()
14
+ */
15
+ export declare const useAlgoliaSearch: () => [SearchClient | null];
16
+ /**
17
+ * Formats an array of values into Algolia facet filter strings.
18
+ *
19
+ * @param facetFilterType Facet attribute name, e.g. 'subjects'
20
+ * @param data Array of values, e.g. ['Leadership', 'AI']
21
+ * @returns e.g. ['subjects:Leadership', 'subjects:AI']
22
+ *
23
+ * @example
24
+ * formatFacetFilterData('subjects', ['Leadership', 'AI'])
25
+ * // → ['subjects:Leadership', 'subjects:AI']
26
+ */
27
+ export declare const formatFacetFilterData: (facetFilterType: string, data: string[]) => string[];
28
+ /**
29
+ * Searches programs or courses from the shared product index.
30
+ *
31
+ * Both product types live in ALGOLIA_PRODUCT_INDEX_NAME, distinguished by the
32
+ * `product` facet. Active tags are OR-ed within their facet and AND-ed with
33
+ * the product-type filter — standard Algolia multi-select pattern.
34
+ *
35
+ * Algolia facetFilters format used here:
36
+ * [ ['product:program'], ['subjects:Leadership', 'subjects:AI'] ]
37
+ * → product = "program" AND (subjects = "Leadership" OR subjects = "AI")
38
+ *
39
+ * @param searchClient Algolia client from useAlgoliaSearch()
40
+ * @param productType 'program' | 'course'
41
+ * @param query Free-text query ('' = browse / rank by popularity)
42
+ * @param activeTags Selected tag values from the filter chips
43
+ * @param facetField Facet attribute to filter tags on (default: 'subjects')
44
+ * @param hitsPerPage Page size
45
+ * @param page 0-indexed page number (Algolia convention)
46
+ */
47
+ export declare const searchProducts: (searchClient: SearchClient, productType: ProductType, query: string, activeTags: string[], facetField?: string, hitsPerPage?: number, page?: number) => Promise<AlgoliaSearchResult>;
48
+ /**
49
+ * Fetches top-ranked products with no query or tag filters.
50
+ * Used for the "Popular programs / courses" fallback in the no-results state.
51
+ *
52
+ * @param searchClient Algolia client
53
+ * @param productType 'program' | 'course'
54
+ * @param hitsPerPage Number of items to fetch
55
+ * @returns Raw Algolia hit objects
56
+ */
57
+ export declare const fetchPopularProducts: (searchClient: SearchClient, productType: ProductType, hitsPerPage: number) => Promise<AlgoliaProductHit[]>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * utils/algoliaUtils.ts
3
+ *
4
+ * Algolia utility functions for the Search feature.
5
+ * Mirrors the pattern used by the Skills Builder feature, with full TypeScript types.
6
+ */
7
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
8
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
9
+ return new (P || (P = Promise))(function (resolve, reject) {
10
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
11
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
12
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
13
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
14
+ });
15
+ };
16
+ import { useMemo } from 'react';
17
+ import { getAppConfig, logError } from '@openedx/frontend-base';
18
+ import { algoliasearch } from 'algoliasearch';
19
+ import { FACET_SUBJECTS, HITS_PER_PAGE_COURSES } from '../constants';
20
+ import { appId } from '../constants';
21
+ // ─── useAlgoliaSearch ─────────────────────────────────────────────────────────
22
+ /**
23
+ * Creates and memoises an Algolia search client using app config.
24
+ * Matches the pattern used by the Skills Builder feature.
25
+ *
26
+ * @returns [SearchClient | null] tuple — destructure as: const [searchClient] = useAlgoliaSearch()
27
+ */
28
+ export const useAlgoliaSearch = () => {
29
+ const config = getAppConfig(appId);
30
+ const algoliaAppId = config.ALGOLIA_APP_ID;
31
+ const algoliaSearchApiKey = config.ALGOLIA_SEARCH_API_KEY;
32
+ const [searchClient] = useMemo(() => {
33
+ if (!algoliaAppId || !algoliaSearchApiKey) {
34
+ logError(new Error('Missing Algolia configuration: ALGOLIA_APP_ID or ALGOLIA_SEARCH_API_KEY'));
35
+ return [null];
36
+ }
37
+ const client = algoliasearch(algoliaAppId, algoliaSearchApiKey);
38
+ return [client];
39
+ }, [algoliaAppId, algoliaSearchApiKey]);
40
+ return [searchClient];
41
+ };
42
+ // ─── formatFacetFilterData ────────────────────────────────────────────────────
43
+ /**
44
+ * Formats an array of values into Algolia facet filter strings.
45
+ *
46
+ * @param facetFilterType Facet attribute name, e.g. 'subjects'
47
+ * @param data Array of values, e.g. ['Leadership', 'AI']
48
+ * @returns e.g. ['subjects:Leadership', 'subjects:AI']
49
+ *
50
+ * @example
51
+ * formatFacetFilterData('subjects', ['Leadership', 'AI'])
52
+ * // → ['subjects:Leadership', 'subjects:AI']
53
+ */
54
+ export const formatFacetFilterData = (facetFilterType, data) => {
55
+ const formattedData = [];
56
+ if (data) {
57
+ data.forEach((item) => formattedData.push(`${facetFilterType}:${item}`));
58
+ }
59
+ return formattedData;
60
+ };
61
+ // ─── searchProducts ───────────────────────────────────────────────────────────
62
+ /**
63
+ * Searches programs or courses from the shared product index.
64
+ *
65
+ * Both product types live in ALGOLIA_PRODUCT_INDEX_NAME, distinguished by the
66
+ * `product` facet. Active tags are OR-ed within their facet and AND-ed with
67
+ * the product-type filter — standard Algolia multi-select pattern.
68
+ *
69
+ * Algolia facetFilters format used here:
70
+ * [ ['product:program'], ['subjects:Leadership', 'subjects:AI'] ]
71
+ * → product = "program" AND (subjects = "Leadership" OR subjects = "AI")
72
+ *
73
+ * @param searchClient Algolia client from useAlgoliaSearch()
74
+ * @param productType 'program' | 'course'
75
+ * @param query Free-text query ('' = browse / rank by popularity)
76
+ * @param activeTags Selected tag values from the filter chips
77
+ * @param facetField Facet attribute to filter tags on (default: 'subjects')
78
+ * @param hitsPerPage Page size
79
+ * @param page 0-indexed page number (Algolia convention)
80
+ */
81
+ export const searchProducts = (searchClient_1, productType_1, query_1, activeTags_1, ...args_1) => __awaiter(void 0, [searchClient_1, productType_1, query_1, activeTags_1, ...args_1], void 0, function* (searchClient, productType, query, activeTags, facetField = FACET_SUBJECTS, hitsPerPage = HITS_PER_PAGE_COURSES, page = 0) {
82
+ const indexName = getAppConfig(appId).ALGOLIA_PRODUCT_INDEX_NAME;
83
+ const tagFilters = formatFacetFilterData(facetField, activeTags);
84
+ // Nested arrays = AND between outer elements, OR within inner arrays
85
+ const facetFilters = [
86
+ [`product:${productType}`],
87
+ ...(tagFilters.length > 0 ? [tagFilters] : []),
88
+ ];
89
+ try {
90
+ const { hits, nbHits, nbPages } = yield searchClient.searchSingleIndex({
91
+ indexName,
92
+ searchParams: {
93
+ query,
94
+ facetFilters,
95
+ hitsPerPage,
96
+ page,
97
+ },
98
+ });
99
+ return {
100
+ hits: hits,
101
+ nbHits: nbHits !== null && nbHits !== void 0 ? nbHits : 0,
102
+ nbPages: nbPages !== null && nbPages !== void 0 ? nbPages : 0,
103
+ };
104
+ }
105
+ catch (error) {
106
+ const err = error instanceof Error ? error : new Error(String(error));
107
+ logError(err);
108
+ return { hits: [], nbHits: 0, nbPages: 0 };
109
+ }
110
+ });
111
+ // ─── fetchPopularProducts ─────────────────────────────────────────────────────
112
+ /**
113
+ * Fetches top-ranked products with no query or tag filters.
114
+ * Used for the "Popular programs / courses" fallback in the no-results state.
115
+ *
116
+ * @param searchClient Algolia client
117
+ * @param productType 'program' | 'course'
118
+ * @param hitsPerPage Number of items to fetch
119
+ * @returns Raw Algolia hit objects
120
+ */
121
+ export const fetchPopularProducts = (searchClient, productType, hitsPerPage) => __awaiter(void 0, void 0, void 0, function* () {
122
+ const { hits } = yield searchProducts(searchClient, productType, '', // empty query → Algolia ranks by popularity
123
+ [], // no tag filters
124
+ FACET_SUBJECTS, hitsPerPage, 0);
125
+ return hits;
126
+ });
127
+ //# sourceMappingURL=algoliaUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"algoliaUtils.js","sourceRoot":"","sources":["../../src/utils/algoliaUtils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * utils/algoliaUtils.ts\n *\n * Algolia utility functions for the Search feature.\n * Mirrors the pattern used by the Skills Builder feature, with full TypeScript types.\n */\n\nimport { useMemo } from 'react';\nimport { getAppConfig, logError } from '@openedx/frontend-base';\nimport { algoliasearch, SearchClient } from 'algoliasearch';\nimport { FACET_SUBJECTS, HITS_PER_PAGE_COURSES } from '@src/constants';\n\nimport {\n AlgoliaProductHit,\n AlgoliaSearchResult,\n ProductType,\n} from '../types/types';\n\nimport { appId } from '@src/constants';\n\n// ─── useAlgoliaSearch ─────────────────────────────────────────────────────────\n\n/**\n * Creates and memoises an Algolia search client using app config.\n * Matches the pattern used by the Skills Builder feature.\n *\n * @returns [SearchClient | null] tuple — destructure as: const [searchClient] = useAlgoliaSearch()\n */\nexport const useAlgoliaSearch = (): [SearchClient | null] => {\n const config = getAppConfig(appId);\n const algoliaAppId = config.ALGOLIA_APP_ID as string | undefined;\n const algoliaSearchApiKey = config.ALGOLIA_SEARCH_API_KEY as string | undefined;\n\n const [searchClient] = useMemo<[SearchClient | null]>(\n () => {\n if (!algoliaAppId || !algoliaSearchApiKey) {\n logError(new Error('Missing Algolia configuration: ALGOLIA_APP_ID or ALGOLIA_SEARCH_API_KEY'));\n return [null];\n }\n\n const client = algoliasearch(algoliaAppId, algoliaSearchApiKey);\n return [client];\n },\n [algoliaAppId, algoliaSearchApiKey],\n );\n\n return [searchClient];\n};\n\n// ─── formatFacetFilterData ────────────────────────────────────────────────────\n\n/**\n * Formats an array of values into Algolia facet filter strings.\n *\n * @param facetFilterType Facet attribute name, e.g. 'subjects'\n * @param data Array of values, e.g. ['Leadership', 'AI']\n * @returns e.g. ['subjects:Leadership', 'subjects:AI']\n *\n * @example\n * formatFacetFilterData('subjects', ['Leadership', 'AI'])\n * // → ['subjects:Leadership', 'subjects:AI']\n */\nexport const formatFacetFilterData = (\n facetFilterType: string,\n data: string[],\n): string[] => {\n const formattedData: string[] = [];\n if (data) {\n data.forEach((item) => formattedData.push(`${facetFilterType}:${item}`));\n }\n return formattedData;\n};\n\n// ─── searchProducts ───────────────────────────────────────────────────────────\n\n/**\n * Searches programs or courses from the shared product index.\n *\n * Both product types live in ALGOLIA_PRODUCT_INDEX_NAME, distinguished by the\n * `product` facet. Active tags are OR-ed within their facet and AND-ed with\n * the product-type filter — standard Algolia multi-select pattern.\n *\n * Algolia facetFilters format used here:\n * [ ['product:program'], ['subjects:Leadership', 'subjects:AI'] ]\n * → product = \"program\" AND (subjects = \"Leadership\" OR subjects = \"AI\")\n *\n * @param searchClient Algolia client from useAlgoliaSearch()\n * @param productType 'program' | 'course'\n * @param query Free-text query ('' = browse / rank by popularity)\n * @param activeTags Selected tag values from the filter chips\n * @param facetField Facet attribute to filter tags on (default: 'subjects')\n * @param hitsPerPage Page size\n * @param page 0-indexed page number (Algolia convention)\n */\nexport const searchProducts = async (\n searchClient: SearchClient,\n productType: ProductType,\n query: string,\n activeTags: string[],\n facetField: string = FACET_SUBJECTS,\n hitsPerPage: number = HITS_PER_PAGE_COURSES,\n page = 0,\n): Promise<AlgoliaSearchResult> => {\n const indexName = getAppConfig(appId).ALGOLIA_PRODUCT_INDEX_NAME as string;\n const tagFilters = formatFacetFilterData(facetField, activeTags);\n\n // Nested arrays = AND between outer elements, OR within inner arrays\n const facetFilters: string[][] = [\n [`product:${productType}`],\n ...(tagFilters.length > 0 ? [tagFilters] : []),\n ];\n\n try {\n const { hits, nbHits, nbPages } = await searchClient.searchSingleIndex({\n indexName,\n searchParams: {\n query,\n facetFilters,\n hitsPerPage,\n page,\n },\n });\n\n return {\n hits: hits as AlgoliaProductHit[],\n nbHits: nbHits ?? 0,\n nbPages: nbPages ?? 0,\n };\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n logError(err);\n return { hits: [], nbHits: 0, nbPages: 0 };\n }\n};\n\n// ─── fetchPopularProducts ─────────────────────────────────────────────────────\n\n/**\n * Fetches top-ranked products with no query or tag filters.\n * Used for the \"Popular programs / courses\" fallback in the no-results state.\n *\n * @param searchClient Algolia client\n * @param productType 'program' | 'course'\n * @param hitsPerPage Number of items to fetch\n * @returns Raw Algolia hit objects\n */\nexport const fetchPopularProducts = async (\n searchClient: SearchClient,\n productType: ProductType,\n hitsPerPage: number,\n): Promise<AlgoliaProductHit[]> => {\n const { hits } = await searchProducts(\n searchClient,\n productType,\n '', // empty query → Algolia ranks by popularity\n [], // no tag filters\n FACET_SUBJECTS,\n hitsPerPage,\n 0,\n );\n return hits;\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edx/frontend-app-subscription-learner-dashboard",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,3 +0,0 @@
1
- import { FC } from 'react';
2
- import './index.scss';
3
- export declare const RecommendedCourse: FC;
@@ -1,16 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Alert } from '@openedx/paragon';
3
- import { useIntl } from '@openedx/frontend-base';
4
- import { ProductCard } from '../ProductCard';
5
- import { useRecommendedCourseData } from '../../hooks/useRecommendedCourseData';
6
- import messages from './messages';
7
- import './index.scss';
8
- export const RecommendedCourse = () => {
9
- const { data = [], isLoading, isError } = useRecommendedCourseData();
10
- const { formatMessage } = useIntl();
11
- if (isError) {
12
- return (_jsx(Alert, { variant: "danger", "data-testid": "recommended-course-error", children: formatMessage(messages.errorText) }));
13
- }
14
- return (_jsx("div", { className: "container", children: _jsx("div", { className: "bg-light p-2", "data-testid": "recommended-course-cards", children: _jsx("div", { className: "row g-3 cards-scroll flex-nowrap flex-md-wrap", children: data.map((item) => (_jsx("div", { className: "col-12 col-md-6 col-xl-3 d-flex card-snap mb-2", children: _jsx(ProductCard, { item: item, isLoading: isLoading }) }, item.id))) }) }) }));
15
- };
16
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/containers/RecommendedCourse/index.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,OAAO,QAAQ,MAAM,YAAY,CAAC;AAClC,OAAO,cAAc,CAAC;AAEtB,MAAM,CAAC,MAAM,iBAAiB,GAAO,GAAG,EAAE;IACxC,MAAM,EAAE,IAAI,GAAG,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,wBAAwB,EAAE,CAAC;IACrE,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,EAAE,CAAC;IAEpC,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CACL,KAAC,KAAK,IAAC,OAAO,EAAC,QAAQ,iBAAa,0BAA0B,YAC3D,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,GAC5B,CACT,CAAC;IACJ,CAAC;IAED,OAAO,CACL,cAAK,SAAS,EAAC,WAAW,YACxB,cAAK,SAAS,EAAC,cAAc,iBAAa,0BAA0B,YAClE,cAAK,SAAS,EAAC,+CAA+C,YAC3D,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAClB,cAAmB,SAAS,EAAC,gDAAgD,YAC3E,KAAC,WAAW,IAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,GAAI,IADzC,IAAI,CAAC,EAAE,CAEX,CACP,CAAC,GACE,GACF,GACF,CACP,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { FC } from 'react';\nimport { Alert } from '@openedx/paragon';\nimport { useIntl } from '@openedx/frontend-base';\n\nimport { ProductCard } from '../ProductCard';\nimport { useRecommendedCourseData } from '@src/hooks/useRecommendedCourseData';\nimport messages from './messages';\nimport './index.scss';\n\nexport const RecommendedCourse: FC = () => {\n const { data = [], isLoading, isError } = useRecommendedCourseData();\n const { formatMessage } = useIntl();\n\n if (isError) {\n return (\n <Alert variant=\"danger\" data-testid=\"recommended-course-error\">\n {formatMessage(messages.errorText)}\n </Alert>\n );\n }\n\n return (\n <div className=\"container\">\n <div className=\"bg-light p-2\" data-testid=\"recommended-course-cards\">\n <div className=\"row g-3 cards-scroll flex-nowrap flex-md-wrap\">\n {data.map((item) => (\n <div key={item.id} className=\"col-12 col-md-6 col-xl-3 d-flex card-snap mb-2\">\n <ProductCard item={item} isLoading={isLoading} />\n </div>\n ))}\n </div>\n </div>\n </div>\n );\n};\n"]}
@@ -1,21 +0,0 @@
1
- .cards-scroll {
2
- -ms-overflow-style: none;
3
- scrollbar-width: none;
4
- }
5
-
6
- .cards-scroll::-webkit-scrollbar {
7
- display: none;
8
- }
9
-
10
- @media (--pgn-size-breakpoint-max-width-sm) {
11
- .cards-scroll {
12
- overflow-x: auto;
13
- display: flex;
14
- scroll-snap-type: x mandatory;
15
- }
16
-
17
- .card-snap {
18
- flex: 0 0 100%;
19
- scroll-snap-align: center;
20
- }
21
- }
@@ -1,8 +0,0 @@
1
- export default messages;
2
- declare namespace messages {
3
- namespace errorText {
4
- let id: string;
5
- let description: string;
6
- let defaultMessage: string;
7
- }
8
- }
@@ -1,10 +0,0 @@
1
- import { defineMessages } from '@openedx/frontend-base';
2
- const messages = defineMessages({
3
- errorText: {
4
- id: 'subs-dash.recommendedCourse.errorText',
5
- description: 'Error text for failing to load recommended courses',
6
- defaultMessage: 'Failed to load recommended courses. Please try again later.',
7
- },
8
- });
9
- export default messages;
10
- //# sourceMappingURL=messages.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../../src/containers/RecommendedCourse/messages.js"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,QAAQ,GAAG,cAAc,CAAC;IAC9B,SAAS,EAAE;QACT,EAAE,EAAE,uCAAuC;QAC3C,WAAW,EAAE,oDAAoD;QACjE,cAAc,EAAE,6DAA6D;KAC9E;CACF,CAAC,CAAC;AAEH,eAAe,QAAQ,CAAC","sourcesContent":["import { defineMessages } from '@openedx/frontend-base';\n\nconst messages = defineMessages({\n errorText: {\n id: 'subs-dash.recommendedCourse.errorText',\n description: 'Error text for failing to load recommended courses',\n defaultMessage: 'Failed to load recommended courses. Please try again later.',\n },\n});\n\nexport default messages;\n"]}
@@ -1,15 +0,0 @@
1
- import { UseQueryOptions } from '@tanstack/react-query';
2
- import { fetchRecommendedCourses } from '../data/services/subs';
3
- type RecommendedCourseData = Awaited<ReturnType<typeof fetchRecommendedCourses>>;
4
- type UseRecommendedCourseOptions = Omit<UseQueryOptions<RecommendedCourseData>, 'queryKey' | 'queryFn'>;
5
- export declare const useRecommendedCourseData: (options?: UseRecommendedCourseOptions) => import("@tanstack/react-query").UseQueryResult<{
6
- id: number;
7
- title: string;
8
- body: string;
9
- url: string;
10
- thumbnail: string;
11
- isProgram: boolean;
12
- tagText: string;
13
- footerLabel: string;
14
- }[], Error>;
15
- export {};
@@ -1,6 +0,0 @@
1
- import { useQuery } from '@tanstack/react-query';
2
- import { fetchRecommendedCourses } from '../data/services/subs';
3
- export const useRecommendedCourseData = (options) => {
4
- return useQuery(Object.assign({ queryKey: ['recommendedCourse'], queryFn: fetchRecommendedCourses, staleTime: 1000 * 60 * 5, retry: 2 }, options));
5
- };
6
- //# sourceMappingURL=useRecommendedCourseData.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useRecommendedCourseData.js","sourceRoot":"","sources":["../../src/hooks/useRecommendedCourseData.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAmB,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AASlE,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,OAAqC,EAAE,EAAE;IAChF,OAAO,QAAQ,iBACb,QAAQ,EAAE,CAAC,mBAAmB,CAAC,EAC/B,OAAO,EAAE,uBAAuB,EAChC,SAAS,EAAE,IAAI,GAAG,EAAE,GAAG,CAAC,EACxB,KAAK,EAAE,CAAC,IACL,OAAO,EACV,CAAC;AACL,CAAC,CAAC","sourcesContent":["import { useQuery, UseQueryOptions } from '@tanstack/react-query';\nimport { fetchRecommendedCourses } from '@src/data/services/subs';\n\ntype RecommendedCourseData = Awaited<ReturnType<typeof fetchRecommendedCourses>>;\n\ntype UseRecommendedCourseOptions = Omit<\n UseQueryOptions<RecommendedCourseData>,\n 'queryKey' | 'queryFn'\n>;\n\nexport const useRecommendedCourseData = (options?: UseRecommendedCourseOptions) => {\n return useQuery({\n queryKey: ['recommendedCourse'],\n queryFn: fetchRecommendedCourses,\n staleTime: 1000 * 60 * 5, // 5 min caching\n retry: 2,\n ...options,\n });\n};\n"]}