-
Notifications
You must be signed in to change notification settings - Fork 11
feat: [FC-86] created course list for Catalog page #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
963cc60
6b6aeb2
2115194
a31e663
9de322a
ef57ba1
9783880
9646221
818d350
d74d889
13a9d86
3e02040
ce917cb
e498645
4bbbf81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| export const mockCourseResponse = { | ||
| id: 'course-v1:edX+DemoX+Demo_Course', | ||
| data: { | ||
| id: 'course-v1:edX+DemoX+Demo_Course', | ||
| course: 'Demo Course', | ||
| start: '2024-04-01T00:00:00Z', | ||
| imageUrl: '/asset-v1:edX+DemoX+Demo_Course+type@asset+block@course_image.jpg', | ||
| org: 'edX', | ||
| orgImg: '/asset-v1:edX+DemoX+Demo_Course+type@asset+block@org_image.jpg', | ||
| content: { | ||
| displayName: 'Demonstration Course', | ||
| overview: 'Course overview', | ||
| number: 'DemoX', | ||
| }, | ||
| number: 'DemoX', | ||
| modes: ['audit', 'verified'], | ||
| language: 'en', | ||
| catalogVisibility: 'both', | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { mockCourseResponse } from './course'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| type StaticImageData = string; | ||
|
|
||
| declare module '*.png' { | ||
| const content: StaticImageData; | ||
| export default content; | ||
| } | ||
|
|
||
| declare module '*.jpg' { | ||
| const content: StaticImageData; | ||
| export default content; | ||
| } | ||
|
|
||
| declare module '*.jpeg' { | ||
| const content: StaticImageData; | ||
| export default content; | ||
| } | ||
|
|
||
| declare module '*.svg' { | ||
| const content: StaticImageData; | ||
| export default content; | ||
| } | ||
|
|
||
| declare module '*.gif' { | ||
| const content: StaticImageData; | ||
| export default content; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { render, screen } from '../../setupTest'; | ||
| import { AlertNotificationProps } from './types'; | ||
| import { AlertNotification } from '.'; | ||
|
|
||
| const renderComponent = (props: AlertNotificationProps) => render(<AlertNotification {...props} />); | ||
|
|
||
| describe('AlertNotification', () => { | ||
| it('renders with default props', () => { | ||
| renderComponent({ | ||
| title: 'Test Title', | ||
| message: 'Test Message', | ||
| variant: 'info', | ||
| }); | ||
|
|
||
| expect(screen.getByText('Test Title')).toBeInTheDocument(); | ||
| expect(screen.getByText('Test Message')).toBeInTheDocument(); | ||
| expect(screen.getByRole('alert')).toHaveClass('alert-info'); | ||
| }); | ||
|
|
||
| it('renders with custom variant', () => { | ||
| renderComponent({ | ||
| title: 'Warning Title', | ||
| message: 'Warning Message', | ||
| variant: 'warning', | ||
| }); | ||
|
|
||
| expect(screen.getByRole('alert')).toHaveClass('alert-warning'); | ||
| }); | ||
|
|
||
| it('displays the info icon', () => { | ||
| renderComponent({ | ||
| title: 'Alert with Icon', | ||
| message: 'Has info icon', | ||
| variant: 'info', | ||
| }); | ||
|
|
||
| expect(screen.getByRole('alert').querySelector('svg')).toBeInTheDocument(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { Alert as BaseAlert } from '@openedx/paragon'; | ||
| import { Info as InfoIcon } from '@openedx/paragon/icons'; | ||
|
|
||
| import { AlertNotificationProps } from './types'; | ||
|
|
||
| export const AlertNotification = ({ | ||
| variant = 'info', title, message, | ||
| }: AlertNotificationProps) => ( | ||
| <BaseAlert variant={variant} icon={InfoIcon}> | ||
| <BaseAlert.Heading>{title}</BaseAlert.Heading> | ||
| <p>{message}</p> | ||
| </BaseAlert> | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export interface AlertNotificationProps { | ||
| variant?: 'info' | 'warning'; | ||
| title: string; | ||
| message: string; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import { getConfig } from '@edx/frontend-platform'; | ||
|
|
||
| import { mockCourseResponse } from '../../__mocks__'; | ||
| import { render, screen } from '../../setupTest'; | ||
| import { CourseCard } from '.'; | ||
|
|
||
| import messages from './messages'; | ||
|
|
||
| describe('CourseCard', () => { | ||
| const renderComponent = (course = mockCourseResponse) => render( | ||
| <CourseCard course={course} />, | ||
| ); | ||
|
|
||
| it('renders course information correctly', () => { | ||
| renderComponent(); | ||
|
|
||
| expect(screen.getByText(mockCourseResponse.data.content.displayName)).toBeInTheDocument(); | ||
| expect(screen.getByText(mockCourseResponse.data.org)).toBeInTheDocument(); | ||
| expect(screen.getByText('Starts: Apr 1, 2024')).toBeInTheDocument(); | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: RTL generally recommend using the they encourage to do: Again this is optional, not a requirement.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If RTL generally recommend, then I also support this idea 💯 |
||
|
|
||
| it('renders course image with correct src and fallback', () => { | ||
| renderComponent(); | ||
|
|
||
| const image = screen.getByAltText(mockCourseResponse.data.content.displayName); | ||
| expect(image).toHaveAttribute('src', `${getConfig().LMS_BASE_URL}${mockCourseResponse.data.imageUrl}`); | ||
| }); | ||
|
|
||
| it('renders organization logo with correct src and fallback', () => { | ||
| renderComponent(); | ||
|
|
||
| const logo = screen.getByAltText(mockCourseResponse.data.org); | ||
| expect(logo).toHaveAttribute('src', `${getConfig().LMS_BASE_URL}${mockCourseResponse.data.orgImg}`); | ||
| }); | ||
|
|
||
| it('formats the link destination correctly', () => { | ||
| renderComponent(); | ||
|
|
||
| const link = screen.getByRole('link'); | ||
| expect(link).toHaveAttribute('href', `/courses/${mockCourseResponse.id}/about`); | ||
| }); | ||
|
|
||
| it('handles missing start date gracefully', () => { | ||
| const courseWithoutStart = { | ||
| ...mockCourseResponse, | ||
| data: { | ||
| ...mockCourseResponse.data, | ||
| start: '', | ||
| }, | ||
| }; | ||
| renderComponent(courseWithoutStart); | ||
|
|
||
| expect(screen.queryByText(messages.startDate.defaultMessage.replace('{startDate}', courseWithoutStart.data.start))).not.toBeInTheDocument(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export const DATE_FORMAT_OPTIONS = { month: 'short', day: 'numeric', year: 'numeric' } as const; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { Link } from 'react-router-dom'; | ||
| import { Card, useMediaQuery, breakpoints } from '@openedx/paragon'; | ||
| import { useIntl } from '@edx/frontend-platform/i18n'; | ||
|
|
||
| import { CourseCardProps } from './types'; | ||
| import messages from './messages'; | ||
| import { getFullImageUrl } from './utils'; | ||
| import { DATE_FORMAT_OPTIONS } from './constants'; | ||
|
|
||
| import noCourseImg from '../../assets/no-course-image.svg'; | ||
| 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) => { | ||
| 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) | ||
| : ''; | ||
|
|
||
| return ( | ||
| <Card | ||
| as={Link} | ||
| to={`/courses/${course.id}/about`} | ||
| className={`course-card ${isExtraSmall ? 'w-100' : 'course-card-desktop'}`} | ||
| isClickable | ||
| > | ||
| <Card.ImageCap | ||
| src={getFullImageUrl(course.data.imageUrl)} | ||
| fallbackSrc={noCourseImg} | ||
| srcAlt={course.data.content.displayName} | ||
| logoSrc={course.data.orgImg ? getFullImageUrl(course.data.orgImg) : undefined} | ||
| fallbackLogoSrc={!course.data.orgImg && noOrgImg} | ||
| logoAlt={course.data.org} | ||
| /> | ||
| <Card.Section> | ||
| <h3 className="m-0">{course.data.content.displayName}</h3> | ||
| <p className="m-0">{course.data.org}</p> | ||
| {formattedDate && ( | ||
| <span> | ||
| {intl.formatMessage(messages.startDate, { | ||
| startDate: formattedDate, | ||
| })} | ||
| </span> | ||
| )} | ||
| </Card.Section> | ||
| </Card> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { defineMessages } from '@edx/frontend-platform/i18n'; | ||
|
|
||
| const messages = defineMessages({ | ||
| startDate: { | ||
| id: 'generic.course-card.start-date', | ||
| defaultMessage: 'Starts: {startDate}', | ||
| description: 'Start date', | ||
| }, | ||
| }); | ||
|
|
||
| export default messages; |
Uh oh!
There was an error while loading. Please reload this page.