diff --git a/src/catalog/CatalogPage.tsx b/src/catalog/CatalogPage.tsx index cbeb3ce..cfd64f6 100644 --- a/src/catalog/CatalogPage.tsx +++ b/src/catalog/CatalogPage.tsx @@ -1,22 +1,19 @@ import { useEffect, useMemo } from 'react'; -import { - DataTable, Container, SearchField, Alert, breakpoints, - useMediaQuery, TextFilter, CardView, -} from '@openedx/paragon'; +import { Container, Alert } from '@openedx/paragon'; import { ErrorPage } from '@edx/frontend-platform/react'; import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; -import classNames from 'classnames'; import { DEFAULT_PAGE_SIZE } from '@src/data/course-list-search/constants'; import { useCourseListSearch } from '@src/data/course-list-search/hooks'; +import CourseCatalogIntroSlot from '@src/plugin-slots/CourseCatalogIntroSlot'; +import { CourseCatalogDataTableSlot } from '@src/plugin-slots/CourseCatalogDataTableSlots'; +import CourseCatalogSearchFieldSlot from '@src/plugin-slots/CourseCatalogSearchFieldSlot'; import { useDebouncedSearchInput } from './hooks/useDebouncedSearchInput'; -import { - AlertNotification, CourseCard, Loading, SubHeader, -} from '../generic'; +import { AlertNotification, Loading } from '../generic'; import { useCatalog } from './hooks/useCatalog'; import messages from './messages'; -import { transformAggregationsToFilterChoices, getPageTitle } from './utils'; +import { transformAggregationsToFilterChoices } from './utils'; const CatalogPage = () => { const intl = useIntl(); @@ -27,7 +24,6 @@ const CatalogPage = () => { fetchData, isFetching, } = useCourseListSearch(); - const isMedium = useMediaQuery({ maxWidth: breakpoints.large.maxWidth }); const { pageIndex, @@ -96,62 +92,18 @@ const CatalogPage = () => { return ( - + {hasCourses ? ( <> - {getConfig().ENABLE_COURSE_DISCOVERY && ( - { - setSearchInput(value); - }} - onSubmit={(value: string) => { - setSearchInput(value); - handleSearch(value); - }} - /> - )} - + row.id }} - > - - - - - + pageIndex={pageIndex} + tableColumns={tableColumns} + handleFetchData={handleFetchData} + /> ) : ( { if (!searchString) { return intl.formatMessage(messages.exploreCourses); } - if ((courseData?.results?.length ?? 0) === 0) { + if ((courseDataResultsLength ?? 0) === 0) { return intl.formatMessage(messages.noSearchResults, { query: searchString }); } diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/README.md b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/README.md new file mode 100644 index 0000000..fb7cb6a --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/README.md @@ -0,0 +1,113 @@ +# Course catalog page data table course card slot + +### Slot ID: `org.openedx.frontend.catalog.course_catalog_page.data_table.course_card` + +## Description + +This slot is used to replace/modify/hide the entire Course catalog page data table course card. + +### Plugin Props: + +* `courseData` - Object. The course object containing course information such as id, display name, organization, number, image URL, and other course metadata. +* `isLoading` - Boolean. Indicates whether the course card is currently in a loading state. + +## Examples + +### Default content + +![Course catalog page data table course card slot with default content](./images/screenshot_default.png) + +### Replaced with custom component + +![🦶 in Course catalog page data table course card slot](./images/screenshot_custom.png) + +The following `env.config.tsx` will replace the Course catalog page data table course card entirely (in this case with a centered `h1` tag) + +```tsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.data_table.course_card': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_data_table_course_card_component', + type: DIRECT_PLUGIN, + RenderWidget: () => ( +

🦶

+ ), + }, + }, + ] + } + }, +} + +export default config; +``` + +### Custom component with plugin props + +![Custom course card component in Course catalog page data table course card slot](./images/screenshot_custom_with_card.png) + +The following `env.config.tsx` example demonstrates how to replace the Course catalog page data table course card slot with a custom component that uses the plugin props (`original` and `isLoading`). In this case, it creates a custom card component that displays course information in a different format. + +```tsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { Card, Badge } from '@openedx/paragon'; +import { Link } from 'react-router-dom'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.data_table.course_card': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_data_table_course_card_component', + type: DIRECT_PLUGIN, + RenderWidget: ({ courseData, isLoading }) => { + if (isLoading) { + return ; + } + + if (!courseData) { + return null; + } + + return ( + + {courseData.data.org} + } + /> + + {courseData.data.content.overview && ( +

+ {courseData.data.content.overview.substring(0, 150)}... +

+ )} +
+ +
+ ); + }, + }, + }, + ] + } + }, +} + +export default config; +``` diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/images/screenshot_custom.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/images/screenshot_custom.png new file mode 100644 index 0000000..cb9202e Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/images/screenshot_custom.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/images/screenshot_custom_with_card.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/images/screenshot_custom_with_card.png new file mode 100644 index 0000000..4389c06 Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/images/screenshot_custom_with_card.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/images/screenshot_default.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/images/screenshot_default.png new file mode 100644 index 0000000..c6b6e0b Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/images/screenshot_default.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/index.tsx b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/index.tsx new file mode 100644 index 0000000..1053609 --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/index.tsx @@ -0,0 +1,21 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; + +import { CourseCard } from '@src/generic'; +import { CourseCardProps } from '@src/generic/course-card/types'; + +const CourseCatalogDataTableCourseCardSlot = ({ original: courseData, isLoading }: CourseCardProps) => ( + + + +); + +export default CourseCatalogDataTableCourseCardSlot; diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/README.md b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/README.md new file mode 100644 index 0000000..0cec279 --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/README.md @@ -0,0 +1,98 @@ +# Course catalog page data table card view slot + +### Slot ID: `org.openedx.frontend.catalog.course_catalog_page.data_table.card_view` + +## Description + +This slot is used to replace/modify/hide the entire Course catalog page data table card view section. + +## Examples + +### Default content + +![Course catalog page data table card view slot with default content](./images/screenshot_default.png) + +### Replaced with custom component + +![🦶 in Course catalog page data table card view slot](./images/screenshot_custom.png) + +The following `env.config.tsx` will replace the Course catalog page data table card view section entirely (in this case with a centered `h1` tag) + +```tsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.data_table.card_view': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_data_table_footer_component', + type: DIRECT_PLUGIN, + RenderWidget: () => ( +

🦶

+ ), + }, + }, + ] + } + }, +} + +export default config; +``` + +### Custom component with plugin props + +![Custom grid layout in Course catalog page data table card view slot](./images/screenshot_with_custom_grid.png) + +The following `env.config.tsx` example demonstrates how to replace the Course catalog page data table card view slot with a custom component that uses the plugin props (`displayData`). In this case, it creates a custom grid layout (2 columns) while using the standard course card component through the plugin slot system. + +```tsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import CourseCatalogDataTableCourseCardSlot from '@src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.data_table.card_view': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_data_table_card_view_component', + type: DIRECT_PLUGIN, + RenderWidget: ({ displayData }) => { + if (!displayData || !displayData.results) { + return null; + } + + return ( +
+ {displayData.results.map((course) => ( + + ))} +
+ ); + }, + }, + }, + ] + } + }, +} + +export default config; +``` diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/images/screenshot_custom.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/images/screenshot_custom.png new file mode 100644 index 0000000..cafbcd4 Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/images/screenshot_custom.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/images/screenshot_default.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/images/screenshot_default.png new file mode 100644 index 0000000..53a3326 Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/images/screenshot_default.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/images/screenshot_with_custom_grid.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/images/screenshot_with_custom_grid.png new file mode 100644 index 0000000..fb98092 Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/images/screenshot_with_custom_grid.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/index.tsx b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/index.tsx new file mode 100644 index 0000000..48658eb --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/index.tsx @@ -0,0 +1,25 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { CardView } from '@openedx/paragon'; + +import { DEFAULT_PAGE_SIZE } from '@src/data/course-list-search/constants'; +import { CourseListSearchResponse } from '@src/data/course-list-search/types'; +import CourseCatalogDataTableCourseCardSlot from './CourseCatalogDataTableCourseCardSlot'; + +const CourseCatalogDataTableCardViewSlot = ({ displayData }: { displayData?: CourseListSearchResponse }) => ( + + + +); + +export default CourseCatalogDataTableCardViewSlot; diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/README.md b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/README.md new file mode 100644 index 0000000..4599048 --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/README.md @@ -0,0 +1,95 @@ +# Course catalog page data table control bar slot + +### Slot ID: `org.openedx.frontend.catalog.course_catalog_page.data_table.control_bar` + +## Description + +This slot is used to replace/modify/hide the entire Course catalog page data table control bar. + +### Plugin Props: + +* `currentPageResultsCount` - Number. The current number of results displayed on the current page. +* `totalResultsCount` - Number. The total number of search results available. + +## Examples + +### Default content + +![Course catalog page data table control bar slot with default content](./images/screenshot_default.png) + +### Replaced with custom component + +![🦶 in Course catalog page data table control bar slot](./images/screenshot_custom.png) + +The following `env.config.tsx` will replace the Course catalog page data table control bar entirely (in this case with a centered `h1` tag) + +```tsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.data_table.control_bar': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_data_table_control_bar_component', + type: DIRECT_PLUGIN, + RenderWidget: () => ( +

🦶

+ ), + }, + }, + ] + } + }, +} + +export default config; +``` + +### Custom component with plugin props + +![Alert component in Course catalog page data table control bar slot](./images/screenshot_custom_with_alert.png) + +The following `env.config.tsx` example demonstrates how to replace the Course catalog page data table control bar slot with a custom component that uses the plugin props (`currentPageResultsCount` and `totalResultsCount`). In this case, it creates an alert component with results count information. + +```tsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { Alert, Stack, Chip } from '@openedx/paragon'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.data_table.control_bar': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_data_table_control_bar_component', + type: DIRECT_PLUGIN, + RenderWidget: ({ currentPageResultsCount, totalResultsCount }) => { + return ( + + Results information + + + Results on page: {currentPageResultsCount} + + + Total results: {totalResultsCount} + + + + ); + }, + }, + }, + ] + } + }, +} + +export default config; +``` \ No newline at end of file diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/images/screenshot_custom.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/images/screenshot_custom.png new file mode 100644 index 0000000..303f4b1 Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/images/screenshot_custom.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/images/screenshot_custom_with_alert.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/images/screenshot_custom_with_alert.png new file mode 100644 index 0000000..9789f95 Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/images/screenshot_custom_with_alert.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/images/screenshot_default.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/images/screenshot_default.png new file mode 100644 index 0000000..53a3326 Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/images/screenshot_default.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/index.tsx b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/index.tsx new file mode 100644 index 0000000..af30648 --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/index.tsx @@ -0,0 +1,24 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { DataTable } from '@openedx/paragon'; + +import type { CourseCatalogDataTableControlBarSlotProps } from './types'; + +const CourseCatalogDataTableControlBarSlot = ({ + currentPageResultsCount, + totalResultsCount, +}: CourseCatalogDataTableControlBarSlotProps) => ( + + + +); + +export default CourseCatalogDataTableControlBarSlot; diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/types.ts b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/types.ts new file mode 100644 index 0000000..d447e38 --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/types.ts @@ -0,0 +1,4 @@ +export interface CourseCatalogDataTableControlBarSlotProps { + currentPageResultsCount: number; + totalResultsCount: number; +} diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/README.md b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/README.md new file mode 100644 index 0000000..65824de --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/README.md @@ -0,0 +1,146 @@ +# Course catalog page data table slot + +### Slot ID: `org.openedx.frontend.catalog.course_catalog_page.data_table` + +## Description + +This slot is used to replace/modify/hide the entire Course catalog page data table. + +### Plugin Props: + +* `displayData` - Object. The course list search response data containing search results, total count, aggregations, and other metadata. +* `totalCourses` - Number. The total number of courses available in the catalog. +* `pageCount` - Number. The total number of pages available for pagination, calculated based on total courses and page size. +* `pageIndex` - Number. The zero-based index of the currently active page in the data table pagination. +* `tableColumns` - Array. The column definitions for the data table, including headers, accessors, filters, and filter choices. Generated from course aggregations. +* `handleFetchData` - Function. Handles fetching data for the data table when pagination, sorting, or filtering changes. + +## Examples + +### Default content + +![Course catalog page data table slot with default content](./images/screenshot_default.png) + +### Replaced with custom component + +![🦶 in Course catalog page data table slot](./images/screenshot_custom.png) + +The following `env.config.tsx` will replace the Course catalog page data table entirely (in this case with a centered `h1` tag) + +```tsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.data_table': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_data_table_course_card_component', + type: DIRECT_PLUGIN, + RenderWidget: () => ( +

🦶

+ ), + }, + }, + ] + } + }, +} + +export default config; +``` + +### Custom component with plugin props + +![Custom data table component with statistics and aggregations in Course catalog page data table slot](./images/screenshot_custom_with_stats.png) + +The following `env.config.tsx` example demonstrates how to replace the Course catalog page data table slot with a custom component that uses the plugin props. In this case, it creates a custom data table with statistics panel showing total courses, current page information, and aggregation counts (organizations and languages). + +```tsx +import { useState } from 'react'; +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { DataTable, TextFilter, CardView, Alert, Stack, Chip, SearchField, Badge } from '@openedx/paragon'; +import { DEFAULT_PAGE_SIZE } from '@src/data/course-list-search/constants'; +import { CourseCatalogDataTableCourseCardSlot } from '@src/plugin-slots/CourseCatalogDataTableSlots'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.data_table': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_data_table_component', + type: DIRECT_PLUGIN, + RenderWidget: ({ + displayData, + totalCourses, + pageCount, + pageIndex, + tableColumns, + handleFetchData, + }) => { + const [searchValue, setSearchValue] = useState(''); + const coursesCount = displayData?.results?.length ?? 0; + const total = displayData?.total ?? totalCourses; + const currentPage = pageIndex + 1; + const aggs = displayData?.aggs; + + const orgCount = aggs?.org?.terms ? Object.keys(aggs.org.terms).length : 0; + const languageCount = aggs?.language?.terms ? Object.keys(aggs.language.terms).length : 0; + + return ( + + + + + Total courses: {total} + Showing: {coursesCount} + Page: {currentPage} of {pageCount} + + {(orgCount > 0 || languageCount > 0) && ( + + {orgCount > 0 && Organizations: {orgCount}} + {languageCount > 0 && Languages: {languageCount}} + + )} + + + + row.id }} + > + + + + ); + }, + }, + }, + ] + } + }, +} + +export default config; +``` diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/images/screenshot_custom.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/images/screenshot_custom.png new file mode 100644 index 0000000..9c4c922 Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/images/screenshot_custom.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/images/screenshot_custom_with_stats.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/images/screenshot_custom_with_stats.png new file mode 100644 index 0000000..8e833c9 Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/images/screenshot_custom_with_stats.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/images/screenshot_default.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/images/screenshot_default.png new file mode 100644 index 0000000..4750d9b Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/images/screenshot_default.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/index.tsx b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/index.tsx new file mode 100644 index 0000000..21da64a --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/index.tsx @@ -0,0 +1,75 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { + breakpoints, DataTable, useMediaQuery, TextFilter, +} from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { getConfig } from '@edx/frontend-platform'; +import { DEFAULT_PAGE_SIZE } from '@src/data/course-list-search/constants'; + +import messages from '@src/catalog/messages'; +import type { CourseCatalogDataTableSlotProps } from './types'; + +import CourseCatalogDataTableControlBarSlot from '../CourseCatalogDataTableControlBarSlot'; +import CourseCatalogDataTableCardViewSlot from '../CourseCatalogDataTableCardViewSlot'; +import CourseCatalogDataTableTableFooterSlot from '../CourseCatalogDataTableTableFooterSlot'; + +const CourseCatalogDataTableSlot = ({ + displayData, + totalCourses, + pageCount, + pageIndex, + tableColumns, + handleFetchData, +}: CourseCatalogDataTableSlotProps) => { + const intl = useIntl(); + const isMedium = useMediaQuery({ maxWidth: breakpoints.large.maxWidth }); + + return ( + + row.id }} + > + + + + + + + ); +}; + +export default CourseCatalogDataTableSlot; diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/types.ts b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/types.ts new file mode 100644 index 0000000..d350e01 --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/types.ts @@ -0,0 +1,24 @@ +import type { CourseListSearchResponse, DataTableParams } from '@src/data/course-list-search/types'; + +export interface TableColumnFilterChoice { + name: string; + number: number; + value: string; +} + +export interface TableColumn { + Header: string; + accessor: string; + Filter: React.ComponentType; + filter: string; + filterChoices: TableColumnFilterChoice[]; +} + +export interface CourseCatalogDataTableSlotProps { + displayData?: CourseListSearchResponse; + totalCourses: number; + pageCount: number; + pageIndex: number; + tableColumns: TableColumn[]; + handleFetchData: (params: DataTableParams) => void; +} diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/README.md b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/README.md new file mode 100644 index 0000000..8ec6ac6 --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/README.md @@ -0,0 +1,45 @@ +# Course catalog page data table footer slot + +### Slot ID: `org.openedx.frontend.catalog.course_catalog_page.data_table.table_footer` + +## Description + +This slot is used to replace/modify/hide the entire Course catalog page data table footer. + +## Examples + +### Default content + +![Course catalog page data table footer slot with default content](./images/screenshot_default.png) + +### Replaced with custom component + +![🦶 in Course catalog page data table footer slot](./images/screenshot_custom.png) + +The following `env.config.tsx` will replace the Course catalog page data table footer entirely (in this case with a centered `h1` tag) + +```tsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.data_table.table_footer': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_data_table_footer_component', + type: DIRECT_PLUGIN, + RenderWidget: () => ( +

🦶

+ ), + }, + }, + ] + } + }, +} + +export default config; +``` diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/images/screenshot_custom.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/images/screenshot_custom.png new file mode 100644 index 0000000..8e47cb4 Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/images/screenshot_custom.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/images/screenshot_default.png b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/images/screenshot_default.png new file mode 100644 index 0000000..0500a68 Binary files /dev/null and b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/images/screenshot_default.png differ diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/index.tsx b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/index.tsx new file mode 100644 index 0000000..3a594a9 --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/index.tsx @@ -0,0 +1,15 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { DataTable } from '@openedx/paragon'; + +const CourseCatalogDataTableTableFooterSlot = () => ( + + + +); + +export default CourseCatalogDataTableTableFooterSlot; diff --git a/src/plugin-slots/CourseCatalogDataTableSlots/index.tsx b/src/plugin-slots/CourseCatalogDataTableSlots/index.tsx new file mode 100644 index 0000000..c3602cc --- /dev/null +++ b/src/plugin-slots/CourseCatalogDataTableSlots/index.tsx @@ -0,0 +1,13 @@ +import CourseCatalogDataTableSlot from './CourseCatalogDataTableSlot'; +import CourseCatalogDataTableControlBarSlot from './CourseCatalogDataTableControlBarSlot'; +import CourseCatalogDataTableCardViewSlot from './CourseCatalogDataTableCardViewSlot'; +import CourseCatalogDataTableTableFooterSlot from './CourseCatalogDataTableTableFooterSlot'; +import CourseCatalogDataTableCourseCardSlot from './CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot'; + +export { + CourseCatalogDataTableSlot, + CourseCatalogDataTableControlBarSlot, + CourseCatalogDataTableCardViewSlot, + CourseCatalogDataTableTableFooterSlot, + CourseCatalogDataTableCourseCardSlot, +}; diff --git a/src/plugin-slots/CourseCatalogIntroSlot/README.md b/src/plugin-slots/CourseCatalogIntroSlot/README.md new file mode 100644 index 0000000..2d0a234 --- /dev/null +++ b/src/plugin-slots/CourseCatalogIntroSlot/README.md @@ -0,0 +1,101 @@ +# Course catalog page intro slot + +### Slot ID: `org.openedx.frontend.catalog.course_catalog_page.intro` + +## Description + +This slot is used to replace/modify/hide the entire Course catalog page intro section. + +### Plugin Props: + +* `searchString` - String. The current search query string entered by the user in the course catalog search field. +* `courseData` - Object. The course list search response data containing search results, total count, aggregations, and other metadata. + +## Examples + +### Default content + +![Course catalog page intro section slot with default content](./images/screenshot_default.png) + +### Replaced with custom component + +![🦶 in Course catalog page intro section slot](./images/screenshot_custom.png) + +The following `env.config.tsx` will replace the Course catalog page intro section entirely (in this case with a centered `h1` tag) + +```tsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.intro': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_intro_component', + type: DIRECT_PLUGIN, + RenderWidget: () => ( +

🦶

+ ), + }, + }, + ] + } + }, +} + +export default config; +``` + +### Custom component with plugin props + +![Alert component in Course catalog page intro section slot](./images/screenshot_custom_with_alert.png) + +The following `env.config.tsx` example demonstrates how to replace the Course catalog page intro slot with a custom component that uses the plugin props (`searchString` and `courseData`). In this case, it creates an alert component with page information. + +```tsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { Alert, Stack, Chip } from '@openedx/paragon'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.intro': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_intro_component', + type: DIRECT_PLUGIN, + RenderWidget: ({ searchString, courseData }) => { + const totalCourses = courseData?.total ?? 0; + const resultsCount = courseData?.results?.length ?? 0; + + return ( + + Search information + + + Search query: {searchString || '(none)'} + + + Total courses: {totalCourses} + + + Found on page: {resultsCount} + + + + ); + }, + }, + }, + ] + } + }, +} + +export default config; +``` diff --git a/src/plugin-slots/CourseCatalogIntroSlot/images/screenshot_custom.png b/src/plugin-slots/CourseCatalogIntroSlot/images/screenshot_custom.png new file mode 100644 index 0000000..80d791f Binary files /dev/null and b/src/plugin-slots/CourseCatalogIntroSlot/images/screenshot_custom.png differ diff --git a/src/plugin-slots/CourseCatalogIntroSlot/images/screenshot_custom_with_alert.png b/src/plugin-slots/CourseCatalogIntroSlot/images/screenshot_custom_with_alert.png new file mode 100644 index 0000000..713828f Binary files /dev/null and b/src/plugin-slots/CourseCatalogIntroSlot/images/screenshot_custom_with_alert.png differ diff --git a/src/plugin-slots/CourseCatalogIntroSlot/images/screenshot_default.png b/src/plugin-slots/CourseCatalogIntroSlot/images/screenshot_default.png new file mode 100644 index 0000000..53a3326 Binary files /dev/null and b/src/plugin-slots/CourseCatalogIntroSlot/images/screenshot_default.png differ diff --git a/src/plugin-slots/CourseCatalogIntroSlot/index.tsx b/src/plugin-slots/CourseCatalogIntroSlot/index.tsx new file mode 100644 index 0000000..4d8abc3 --- /dev/null +++ b/src/plugin-slots/CourseCatalogIntroSlot/index.tsx @@ -0,0 +1,37 @@ +import classNames from 'classnames'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { breakpoints, useMediaQuery } from '@openedx/paragon'; + +import { getPageTitle } from '@src/catalog/utils'; +import { SubHeader } from '@src/generic'; +import type { CourseCatalogIntroSlotProps } from './types'; + +const CourseCatalogIntroSlot = ({ + searchString, + courseDataResultsLength, +}: CourseCatalogIntroSlotProps) => { + const intl = useIntl(); + const isMedium = useMediaQuery({ maxWidth: breakpoints.medium.maxWidth }); + + return ( + + + + ); +}; + +export default CourseCatalogIntroSlot; diff --git a/src/plugin-slots/CourseCatalogIntroSlot/types.ts b/src/plugin-slots/CourseCatalogIntroSlot/types.ts new file mode 100644 index 0000000..acabd32 --- /dev/null +++ b/src/plugin-slots/CourseCatalogIntroSlot/types.ts @@ -0,0 +1,4 @@ +export interface CourseCatalogIntroSlotProps { + searchString: string; + courseDataResultsLength?: number; +} diff --git a/src/plugin-slots/CourseCatalogSearchFieldSlot/README.md b/src/plugin-slots/CourseCatalogSearchFieldSlot/README.md new file mode 100644 index 0000000..820e27c --- /dev/null +++ b/src/plugin-slots/CourseCatalogSearchFieldSlot/README.md @@ -0,0 +1,118 @@ +# Course catalog page search field slot + +### Slot ID: `org.openedx.frontend.catalog.course_catalog_page.search_field` + +## Description + +This slot is used to replace/modify/hide the entire Course catalog page search field. + +### Plugin Props: + +* `setSearchInput` - Function. Sets the search input value in the component state. +* `handleSearch` - Function. Handles the search submission and triggers the search operation. + +## Examples + +### Default content + +![Course catalog page search field slot with default content](./images/screenshot_default.png) + +### Replaced with custom component + +![🦶 in Course catalog page search field slot](./images/screenshot_custom.png) + +The following `env.config.tsx` will replace the Course catalog page search field entirely (in this case with a centered `h1` tag) + +```tsx +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.search_field': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_search_field_component', + type: DIRECT_PLUGIN, + RenderWidget: () => ( +

🦶

+ ), + }, + }, + ] + } + }, +} + +export default config; +``` + +### Custom component with plugin props + +![Custom search field with popular searches in Course catalog page search field slot](./images/screenshot_custom_with_form_control.png) + +The following `env.config.tsx` example demonstrates how to replace the Course catalog page search field slot with a custom component that uses the plugin props (`setSearchInput` and `handleSearch`). In this case, it creates a search field with floating label and quick access to popular search queries via clickable chips. + +```tsx +import { useState } from 'react'; +import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; +import { Stack, Chip, Form } from '@openedx/paragon'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.catalog.course_catalog_page.search_field': { + keepDefault: false, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_course_catalog_page_search_field_component', + type: DIRECT_PLUGIN, + RenderWidget: ({ setSearchInput, handleSearch }) => { + const [searchValue, setSearchValue] = useState(''); + const popularSearches = ['Python', 'JavaScript', 'Data Science', 'Web Development']; + + const handleQuickSearch = (query: string) => { + setSearchValue(query); + setSearchInput(query); + handleSearch(query); + }; + + return ( + + + { + const value = e.target.value; + setSearchValue(value); + setSearchInput(value); + }} + /> + + + Popular: + {popularSearches.map((query) => ( + handleQuickSearch(query)} + > + {query} + + ))} + + + ); + }, + }, + }, + ] + } + }, +} + +export default config; +``` diff --git a/src/plugin-slots/CourseCatalogSearchFieldSlot/images/screenshot_custom.png b/src/plugin-slots/CourseCatalogSearchFieldSlot/images/screenshot_custom.png new file mode 100644 index 0000000..6e246a2 Binary files /dev/null and b/src/plugin-slots/CourseCatalogSearchFieldSlot/images/screenshot_custom.png differ diff --git a/src/plugin-slots/CourseCatalogSearchFieldSlot/images/screenshot_custom_with_form_control.png b/src/plugin-slots/CourseCatalogSearchFieldSlot/images/screenshot_custom_with_form_control.png new file mode 100644 index 0000000..fab40bb Binary files /dev/null and b/src/plugin-slots/CourseCatalogSearchFieldSlot/images/screenshot_custom_with_form_control.png differ diff --git a/src/plugin-slots/CourseCatalogSearchFieldSlot/images/screenshot_default.png b/src/plugin-slots/CourseCatalogSearchFieldSlot/images/screenshot_default.png new file mode 100644 index 0000000..1b6353e Binary files /dev/null and b/src/plugin-slots/CourseCatalogSearchFieldSlot/images/screenshot_default.png differ diff --git a/src/plugin-slots/CourseCatalogSearchFieldSlot/index.tsx b/src/plugin-slots/CourseCatalogSearchFieldSlot/index.tsx new file mode 100644 index 0000000..b7204a9 --- /dev/null +++ b/src/plugin-slots/CourseCatalogSearchFieldSlot/index.tsx @@ -0,0 +1,49 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { breakpoints, SearchField, useMediaQuery } from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import classNames from 'classnames'; +import { getConfig } from '@edx/frontend-platform'; + +import messages from '@src/catalog/messages'; +import type { CourseCatalogSearchFieldSlotProps } from './types'; + +const CourseCatalogSearchFieldSlot = ({ + setSearchInput, + handleSearch, +}: CourseCatalogSearchFieldSlotProps) => { + const intl = useIntl(); + const isMedium = useMediaQuery({ maxWidth: breakpoints.large.maxWidth }); + + return ( + + {getConfig().ENABLE_COURSE_DISCOVERY && ( + { + setSearchInput(value); + }} + onSubmit={(value: string) => { + setSearchInput(value); + handleSearch(value); + }} + /> + )} + + ); +}; + +export default CourseCatalogSearchFieldSlot; diff --git a/src/plugin-slots/CourseCatalogSearchFieldSlot/types.ts b/src/plugin-slots/CourseCatalogSearchFieldSlot/types.ts new file mode 100644 index 0000000..d74b617 --- /dev/null +++ b/src/plugin-slots/CourseCatalogSearchFieldSlot/types.ts @@ -0,0 +1,4 @@ +export interface CourseCatalogSearchFieldSlotProps { + setSearchInput: (value: string) => void; + handleSearch: (value: string) => void; +} diff --git a/src/plugin-slots/README.md b/src/plugin-slots/README.md index 8e7263f..14c4a33 100644 --- a/src/plugin-slots/README.md +++ b/src/plugin-slots/README.md @@ -17,3 +17,10 @@ * [`org.openedx.frontend.catalog.course_about_page.course_image`](./CourseAboutCourseImageSlot/) * [`org.openedx.frontend.catalog.course_about_page.course_media`](./CourseAboutCourseMediaSlot/) * [`org.openedx.frontend.catalog.course_about_page.sidebar`](./CourseAboutSidebarSlot/) +* [`org.openedx.frontend.catalog.course_catalog_page.intro`](./CourseCatalogIntroSlot/) +* [`org.openedx.frontend.catalog.course_catalog_page.data_table.course_card`](./CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/CourseCatalogDataTableCourseCardSlot/) +* [`org.openedx.frontend.catalog.course_catalog_page.data_table.card_view`](./CourseCatalogDataTableSlots/CourseCatalogDataTableCardViewSlot/) +* [`org.openedx.frontend.catalog.course_catalog_page.data_table.control_bar`](./CourseCatalogDataTableSlots/CourseCatalogDataTableControlBarSlot/) +* [`org.openedx.frontend.catalog.course_catalog_page.search_field`](./CourseCatalogSearchFieldSlot/) +* [`org.openedx.frontend.catalog.course_catalog_page.data_table`](./CourseCatalogDataTableSlots/CourseCatalogDataTableSlot/) +* [`org.openedx.frontend.catalog.course_catalog_page.data_table.table_footer`](./CourseCatalogDataTableSlots/CourseCatalogDataTableTableFooterSlot/)