@edx/frontend-app-subscription-learner-dashboard 1.12.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.
- package/dist/app.js +4 -1
- package/dist/app.js.map +1 -1
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -1
- package/dist/containers/ProductCard/index.js +4 -1
- package/dist/containers/ProductCard/index.js.map +1 -1
- package/dist/containers/RecentlyViewedPanel/RecentlyViewedCourseProgram/index.js +1 -1
- package/dist/containers/RecentlyViewedPanel/RecentlyViewedCourseProgram/index.js.map +1 -1
- package/dist/containers/RecentlyViewedPanel/index.js +3 -1
- package/dist/containers/RecentlyViewedPanel/index.js.map +1 -1
- package/dist/data/services/subs/api.d.ts +0 -20
- package/dist/data/services/subs/api.js +0 -111
- package/dist/data/services/subs/api.js.map +1 -1
- package/dist/data/services/subs/index.d.ts +1 -1
- package/dist/data/services/subs/index.js +1 -1
- package/dist/data/services/subs/index.js.map +1 -1
- package/dist/hooks/useRecentlyViewedCoursesAndProgramsData.d.ts +11 -15
- package/dist/hooks/useRecentlyViewedCoursesAndProgramsData.js +39 -3
- package/dist/hooks/useRecentlyViewedCoursesAndProgramsData.js.map +1 -1
- package/dist/types/types.d.ts +32 -0
- package/dist/types/types.js +7 -0
- package/dist/types/types.js.map +1 -0
- package/dist/utils/algoliaUtils.d.ts +57 -0
- package/dist/utils/algoliaUtils.js +127 -0
- package/dist/utils/algoliaUtils.js.map +1 -0
- package/package.json +3 -2
- package/dist/containers/RecommendedCourse/index.d.ts +0 -3
- package/dist/containers/RecommendedCourse/index.js +0 -16
- package/dist/containers/RecommendedCourse/index.js.map +0 -1
- package/dist/containers/RecommendedCourse/index.scss +0 -21
- package/dist/containers/RecommendedCourse/messages.d.ts +0 -8
- package/dist/containers/RecommendedCourse/messages.js +0 -10
- package/dist/containers/RecommendedCourse/messages.js.map +0 -1
- package/dist/hooks/useRecommendedCourseData.d.ts +0 -15
- package/dist/hooks/useRecommendedCourseData.js +0 -6
- 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;
|
|
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"]}
|
package/dist/constants.d.ts
CHANGED
|
@@ -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
|
package/dist/constants.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"
|
|
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.
|
|
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,
|
|
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.
|
|
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,
|
|
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
|
|
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;
|
|
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 {
|
|
1
|
+
export { getProgramProgressData, getProgramsListData, initializeCourseList, initializeSubsList, } from './api';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
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,
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export declare const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 {
|
|
3
|
-
|
|
4
|
-
|
|
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,
|
|
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 @@
|
|
|
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.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"react": "^18",
|
|
80
80
|
"react-dom": "^18",
|
|
81
81
|
"react-router": "^6",
|
|
82
|
-
"react-router-dom": "^6"
|
|
82
|
+
"react-router-dom": "^6",
|
|
83
|
+
"algoliasearch": "^5.52.1"
|
|
83
84
|
}
|
|
84
85
|
}
|
|
@@ -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,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"]}
|