Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 15 additions & 63 deletions src/catalog/CatalogPage.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -27,7 +24,6 @@ const CatalogPage = () => {
fetchData,
isFetching,
} = useCourseListSearch();
const isMedium = useMediaQuery({ maxWidth: breakpoints.large.maxWidth });

const {
pageIndex,
Expand Down Expand Up @@ -96,62 +92,18 @@ const CatalogPage = () => {

return (
<Container fluid={false} size="xl" className="pt-5.5 mb-6">
<SubHeader
title={getPageTitle({
intl,
searchString,
courseData,
})}
className={classNames({ 'mx-2.5': isMedium })}
/>
<CourseCatalogIntroSlot searchString={searchString} courseDataResultsLength={courseData?.results?.length} />
{hasCourses ? (
<>
{getConfig().ENABLE_COURSE_DISCOVERY && (
<SearchField
key="search-field"
className={classNames({
'w-auto mx-2.5 mb-0': isMedium,
'mb-4 w-25': !isMedium,
})}
placeholder={intl.formatMessage(messages.searchPlaceholder)}
onChange={(value: string) => {
setSearchInput(value);
}}
onSubmit={(value: string) => {
setSearchInput(value);
handleSearch(value);
}}
/>
)}
<DataTable
showFiltersInSidebar={!isMedium}
numBreakoutFilters={0}
isFilterable={getConfig().ENABLE_COURSE_DISCOVERY}
isSortable
isPaginated
manualFilters
manualPagination
defaultColumnValues={{ Filter: TextFilter }}
itemCount={displayData?.total || totalCourses}
pageSize={DEFAULT_PAGE_SIZE}
<CourseCatalogSearchFieldSlot setSearchInput={setSearchInput} handleSearch={handleSearch} />
<CourseCatalogDataTableSlot
displayData={displayData}
totalCourses={totalCourses}
pageCount={pageCount}
initialState={{ pageSize: DEFAULT_PAGE_SIZE, pageIndex }}
data={displayData?.results}
columns={tableColumns}
fetchData={handleFetchData}
// Using course ID as a unique identifier for DataTable rows.
// This ensures stable keys for React reconciliation, preventing cards from being
// repopulated with different data when filtering, sorting, or paginating.
initialTableOptions={{ getRowId: (row) => row.id }}
>
<DataTable.TableControlBar />
<CardView
CardComponent={CourseCard}
skeletonCardCount={Math.min(displayData?.total ?? DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE)}
/>
<DataTable.EmptyTable content={intl.formatMessage(messages.noResultsFound)} />
<DataTable.TableFooter />
</DataTable>
pageIndex={pageIndex}
tableColumns={tableColumns}
handleFetchData={handleFetchData}
/>
</>
) : (
<AlertNotification
Expand Down
4 changes: 1 addition & 3 deletions src/catalog/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { IntlShape } from '@edx/frontend-platform/i18n';

import type { CourseListSearchResponse } from '@src/data/course-list-search/types';

export interface GetPageTitleProps {
intl: IntlShape;
searchString: string;
courseData: CourseListSearchResponse | undefined;
courseDataResultsLength?: number;
}
4 changes: 2 additions & 2 deletions src/catalog/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ export const compareFilters = (
export const getPageTitle = ({
intl,
searchString,
courseData,
courseDataResultsLength,
}: GetPageTitleProps) => {
if (!searchString) {
return intl.formatMessage(messages.exploreCourses);
}

if ((courseData?.results?.length ?? 0) === 0) {
if ((courseDataResultsLength ?? 0) === 0) {
return intl.formatMessage(messages.noSearchResults, { query: searchString });
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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: () => (
<h1 style={{textAlign: 'center'}}>🦶</h1>
),
},
},
]
}
},
}

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 <Card isLoading />;
}

if (!courseData) {
return null;
}

return (
<Card
as={Link}
to={`/courses/${courseData.id}/about`}
isClickable
>
<Card.Header
title={courseData.data.content.displayName}
subtitle={
<Badge>{courseData.data.org}</Badge>
}
/>
<Card.Section>
{courseData.data.content.overview && (
<p className="text-muted font-size-sm">
{courseData.data.content.overview.substring(0, 150)}...
</p>
)}
</Card.Section>
<Card.Footer textElement={`Language: ${courseData.data.language}`} />
</Card>
);
},
},
},
]
}
},
}

export default config;
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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) => (
<PluginSlot
id="org.openedx.frontend.catalog.course_catalog_page.data_table.course_card"
slotOptions={{
mergeProps: true,
}}
pluginProps={{
courseData,
isLoading,
}}
>
<CourseCard original={courseData} isLoading={isLoading} />
</PluginSlot>
);

export default CourseCatalogDataTableCourseCardSlot;
Original file line number Diff line number Diff line change
@@ -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: () => (
<h1 style={{textAlign: 'center'}}>🦶</h1>
),
},
},
]
}
},
}

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 (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '2rem',
}}
>
{displayData.results.map((course) => (
<CourseCatalogDataTableCourseCardSlot
key={course.id}
original={course}
isLoading={false}
/>
))}
</div>
);
},
},
},
]
}
},
}

export default config;
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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 }) => (
<PluginSlot
id="org.openedx.frontend.catalog.course_catalog_page.data_table.card_view"
slotOptions={{
mergeProps: true,
}}
pluginProps={{
displayData,
}}
>
<CardView
CardComponent={CourseCatalogDataTableCourseCardSlot}
skeletonCardCount={Math.min(displayData?.total ?? DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE)}
/>
</PluginSlot>
);

export default CourseCatalogDataTableCardViewSlot;
Loading