From 35ed0a1117efb1817e926c182865b8e553f6f2db Mon Sep 17 00:00:00 2001 From: PKulkoRaccoonGang Date: Fri, 27 Jun 2025 09:02:33 +0300 Subject: [PATCH] feat: added DataTable for Catalog page --- src/generic/course-card/CourseCard.test.tsx | 2 +- src/generic/course-card/index.tsx | 22 ++-- src/generic/course-card/types.ts | 2 +- src/generic/sub-header/index.tsx | 6 +- src/generic/sub-header/types.ts | 1 + src/index.scss | 12 ++ "src/\321\201atalog/CatalogPage.tsx" | 108 ++++++++++++------ .../\321\201atalog/constants.ts" | 0 .../data/__tests__/courseDiscovery.test.tsx" | 2 +- "src/\321\201atalog/data/api.ts" | 2 +- "src/\321\201atalog/data/hooks.ts" | 2 +- "src/\321\201atalog/data/types.ts" | 4 +- "src/\321\201atalog/messages.ts" | 15 +++ "src/\321\201atalog/types.ts" | 12 ++ "src/\321\201atalog/utils.ts" | 22 ++++ 15 files changed, 158 insertions(+), 54 deletions(-) rename "src/\321\201atalog/data/constants.ts" => "src/\321\201atalog/constants.ts" (100%) create mode 100644 "src/\321\201atalog/types.ts" create mode 100644 "src/\321\201atalog/utils.ts" diff --git a/src/generic/course-card/CourseCard.test.tsx b/src/generic/course-card/CourseCard.test.tsx index 68cc48b0..bc085e5e 100644 --- a/src/generic/course-card/CourseCard.test.tsx +++ b/src/generic/course-card/CourseCard.test.tsx @@ -8,7 +8,7 @@ import messages from './messages'; describe('CourseCard', () => { const renderComponent = (course = mockCourseResponse) => render( - , + , ); it('renders course information correctly', () => { diff --git a/src/generic/course-card/index.tsx b/src/generic/course-card/index.tsx index a61e6a25..3cfb81a6 100644 --- a/src/generic/course-card/index.tsx +++ b/src/generic/course-card/index.tsx @@ -12,32 +12,32 @@ import noOrgImg from '../../assets/no-org-image.svg'; // TODO: Determine the final design for the course Card component. // Issue: https://github.com/openedx/frontend-app-catalog/issues/10 -export const CourseCard = ({ course }: CourseCardProps) => { +export const CourseCard = ({ original }: CourseCardProps) => { const intl = useIntl(); const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth }); - const formattedDate = course?.data?.start - ? intl.formatDate(new Date(course.data.start), DATE_FORMAT_OPTIONS) + const formattedDate = original?.data?.start + ? intl.formatDate(new Date(original.data.start), DATE_FORMAT_OPTIONS) : ''; return ( -

{course.data.content.displayName}

-

{course.data.org}

+

{original.data.content.displayName}

+

{original.data.org}

{formattedDate && ( {intl.formatMessage(messages.startDate, { diff --git a/src/generic/course-card/types.ts b/src/generic/course-card/types.ts index 7383c2ce..559f0205 100644 --- a/src/generic/course-card/types.ts +++ b/src/generic/course-card/types.ts @@ -26,5 +26,5 @@ export interface Course { } export interface CourseCardProps { - course: Course; + original: Course; } diff --git a/src/generic/sub-header/index.tsx b/src/generic/sub-header/index.tsx index e8286c65..c2b06569 100644 --- a/src/generic/sub-header/index.tsx +++ b/src/generic/sub-header/index.tsx @@ -1,7 +1,9 @@ +import classNames from 'classnames'; + import { SubHeaderProps } from './types'; -export const SubHeader = ({ title }: SubHeaderProps) => ( -
+export const SubHeader = ({ title, className }: SubHeaderProps) => ( +

{title}

); diff --git a/src/generic/sub-header/types.ts b/src/generic/sub-header/types.ts index 861c7937..f2807984 100644 --- a/src/generic/sub-header/types.ts +++ b/src/generic/sub-header/types.ts @@ -1,3 +1,4 @@ export interface SubHeaderProps { title: string; + className?: string; } diff --git a/src/index.scss b/src/index.scss index 3898633f..5b47a22c 100644 --- a/src/index.scss +++ b/src/index.scss @@ -2,3 +2,15 @@ @import "~@edx/frontend-component-header/dist/index"; @import "~@edx/frontend-component-footer/dist/footer"; + +.pgn__data-table-layout-sidebar { + min-width: 386px; +} + +.pgn__data-table-layout-wrapper { + margin-bottom: 20px; +} + +.pgn__searchfield { + width: 386px; +} \ No newline at end of file diff --git "a/src/\321\201atalog/CatalogPage.tsx" "b/src/\321\201atalog/CatalogPage.tsx" index 972cf8b1..8efdb810 100644 --- "a/src/\321\201atalog/CatalogPage.tsx" +++ "b/src/\321\201atalog/CatalogPage.tsx" @@ -1,17 +1,24 @@ import { - CardGrid, Container, Layout, Alert, + Container, Alert, SearchField, DataTable, TextFilter, + CardView, CheckboxFilter, useMediaQuery, breakpoints, } 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 { - AlertNotification, CourseCard, Loading, SubHeader, + AlertNotification, + CourseCard, + Loading, + SubHeader, } from '../generic'; import { useCourseDiscovery } from './data/hooks'; +import { transformResultsForTable } from './utils'; import messages from './messages'; +import { DEFAULT_PAGE_INDEX, DEFAULT_PAGE_SIZE } from './constants'; -const GRID_LAYOUT = { xl: [{ span: 9 }, { span: 3 }] }; +const enableCourseDiscovery = false; const CatalogPage = () => { const intl = useIntl(); @@ -20,6 +27,7 @@ const CatalogPage = () => { isLoading, isError, } = useCourseDiscovery(); + const isMedium = useMediaQuery({ maxWidth: breakpoints.large.maxWidth }); if (isLoading) { return ( @@ -45,39 +53,69 @@ const CatalogPage = () => { return ( - - - - {totalCourses === 0 ? ( - - ) : ( - - {courseData?.results?.map(course => ( - - ))} - - )} - - - {totalCourses > 0 && ( - - )} - - + {totalCourses > 0 ? ( + <> + {}} + onClear={() => {}} + placeholder={intl.formatMessage(messages.searchPlaceholder)} + /> + + + + + + + + ) : ( + + )} ); }; diff --git "a/src/\321\201atalog/data/constants.ts" "b/src/\321\201atalog/constants.ts" similarity index 100% rename from "src/\321\201atalog/data/constants.ts" rename to "src/\321\201atalog/constants.ts" diff --git "a/src/\321\201atalog/data/__tests__/courseDiscovery.test.tsx" "b/src/\321\201atalog/data/__tests__/courseDiscovery.test.tsx" index fa8086b6..7be87fb4 100644 --- "a/src/\321\201atalog/data/__tests__/courseDiscovery.test.tsx" +++ "b/src/\321\201atalog/data/__tests__/courseDiscovery.test.tsx" @@ -4,9 +4,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { renderHook, waitFor } from '../../../setupTest'; import { mockCourseDiscoveryResponse } from '../../__mocks__'; +import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_INDEX } from '../../constants'; import { fetchCourseDiscovery } from '../api'; import { useCourseDiscovery } from '../hooks'; -import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_INDEX } from '../constants'; import { getCourseDiscoveryUrl } from '../urls'; jest.mock('@edx/frontend-platform/auth', () => ({ diff --git "a/src/\321\201atalog/data/api.ts" "b/src/\321\201atalog/data/api.ts" index 8ac59c85..7c17b7c7 100644 --- "a/src/\321\201atalog/data/api.ts" +++ "b/src/\321\201atalog/data/api.ts" @@ -1,7 +1,7 @@ import { camelCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_INDEX } from './constants'; +import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_INDEX } from '../constants'; import { getCourseDiscoveryUrl } from './urls'; import { CourseDiscoveryResponse } from './types'; diff --git "a/src/\321\201atalog/data/hooks.ts" "b/src/\321\201atalog/data/hooks.ts" index 1e0bea8a..204d2f9f 100644 --- "a/src/\321\201atalog/data/hooks.ts" +++ "b/src/\321\201atalog/data/hooks.ts" @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { fetchCourseDiscovery } from './api'; -import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_INDEX } from './constants'; +import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_INDEX } from '../constants'; import { CourseDiscoveryResponse } from './types'; /** diff --git "a/src/\321\201atalog/data/types.ts" "b/src/\321\201atalog/data/types.ts" index 90e8be2a..816a3818 100644 --- "a/src/\321\201atalog/data/types.ts" +++ "b/src/\321\201atalog/data/types.ts" @@ -2,13 +2,15 @@ export interface CourseDiscoveryResponse { count: number; results: { id: string; - title: string; + index: string; + type: string; data: { id: string; course: string; start: string; imageUrl: string; org: string; + orgImageUrl?: string; content: { displayName: string; overview?: string; diff --git "a/src/\321\201atalog/messages.ts" "b/src/\321\201atalog/messages.ts" index 12fd7469..e02b0fd8 100644 --- "a/src/\321\201atalog/messages.ts" +++ "b/src/\321\201atalog/messages.ts" @@ -21,6 +21,21 @@ const messages = defineMessages({ defaultMessage: 'Viewing {totalCourses} courses', description: 'Total courses heading', }, + searchPlaceholder: { + id: 'category.catalog.search-placeholder', + defaultMessage: 'Search for a course', + description: 'Search placeholder', + }, + exploreCourses: { + id: 'category.catalog.explore-courses', + defaultMessage: 'Explore courses', + description: 'Explore courses', + }, + noResultsFound: { + id: 'category.catalog.no-results-found', + defaultMessage: 'No results found', + description: 'No results found', + }, }); export default messages; diff --git "a/src/\321\201atalog/types.ts" "b/src/\321\201atalog/types.ts" new file mode 100644 index 00000000..172daec5 --- /dev/null +++ "b/src/\321\201atalog/types.ts" @@ -0,0 +1,12 @@ +import { CourseDiscoveryResponse } from './data/types'; + +export interface TransformedCourseItem { + id: string; + famous_for: string; + language: string; + modes: string[]; + org: string; + data: CourseDiscoveryResponse['results'][0]['data']; + index?: string; + type?: string; +} diff --git "a/src/\321\201atalog/utils.ts" "b/src/\321\201atalog/utils.ts" new file mode 100644 index 00000000..f1e0e73c --- /dev/null +++ "b/src/\321\201atalog/utils.ts" @@ -0,0 +1,22 @@ +import type { TransformedCourseItem } from './types'; +import type { CourseDiscoveryResponse } from './data/types'; + +/** + * Transforms course discovery results into a format suitable for DataTable display. + */ +export const transformResultsForTable = (results: CourseDiscoveryResponse['results'] | undefined): TransformedCourseItem[] => { + if (!results?.length) { + return []; + } + + return results.map(item => ({ + id: item.id, + famous_for: item.data.content.displayName, + language: item.data.language, + modes: item.data.modes, + org: item.data.org, + data: item.data, + index: item.index, + type: item.type, + })); +};