diff --git a/src/containers/ProgramDashboard/ProgramProgress/ProgramProgress.tsx b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgress.tsx new file mode 100644 index 000000000..9984b77ca --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgress.tsx @@ -0,0 +1,89 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet'; +import { useParams } from 'react-router-dom'; +import { Col, Container, Row } from '@openedx/paragon'; +import { logError } from '@edx/frontend-platform/logging'; +import { camelCaseObject } from '@edx/frontend-platform/utils'; +import { getProgramProgressData } from '../data/api'; +import { ProgramProgressContext, ProgramProgressContextValueType } from './ProgramProgressProvider.tsx'; +import ProgramProgressCourses from './ProgramProgressCourses'; +import ProgramProgressHeader from './ProgramProgressHeader'; +import ProgramProgressSidebar from './ProgramProgressSidebar'; +import ProgramProgressInfo from './ProgramProgressInfo'; + +import './index.scss'; + +// TODO: write tests for this file once all child components are done +const ProgramProgress: React.FC = () => { + const { + programProgressData, + setProgramProgressData, + } = useContext(ProgramProgressContext); + + const [programProgressEndpointError, setProgramProgressEndpointError] = useState(false); + const hasProgramProgressData : Boolean = programProgressData?.courseData + && programProgressData.programData + && programProgressData.urls; + + const { uuid } = useParams() as { uuid: string }; + useEffect(() => { + getProgramProgressData(uuid) + .then(responseData => { + setProgramProgressData(camelCaseObject(responseData.data)); + }) + .catch(err => { + logError(err); + setProgramProgressEndpointError(true); + }); + }, [uuid, setProgramProgressData]); + + if (programProgressEndpointError) { + return ( +
Not found page
+ ); + } + + if (!hasProgramProgressData) { + return ( +
Loading...
+ ); + } + + const programData = programProgressData?.programData; + const courseData = programProgressData?.courseData; + + const totalCoursesInProgram = (courseData.notStarted?.length || 0) + + (courseData.completed?.length || 0) + + (courseData.inProgress?.length || 0); + + const allCoursesCompleted = !courseData.notStarted?.length + && !courseData.inProgress?.length + && courseData.completed?.length; + + return ( + <> + + + + + + + + + + + + + + + ); +}; + +export default ProgramProgress; diff --git a/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressHeader.test.tsx b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressHeader.test.tsx new file mode 100644 index 000000000..03177b5de --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressHeader.test.tsx @@ -0,0 +1,95 @@ +import { render, screen, RenderResult } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import ProgramProgressHeader from './ProgramProgressHeader'; +import { getProgramIcon } from '../data/util'; + +jest.mock('../data/util', () => ({ + getProgramIcon: jest.fn(), +})); + +const mockProgramType = 'Degree'; +const mockProgramTitle = 'Full Stack Development Degree'; +const mockProgramIconUrl = 'icon-degree.svg'; + +const mockOrganizations = [ + { + uuid: 'org-1-uuid', + key: 'UniversityAx', + name: 'University A', + logoImageUrl: 'logo-a.png', + certificateLogoImageUrl: 'cert-logo-a.png', + }, + { + uuid: 'org-2-uuid', + key: 'TechInstituteBx', + name: 'Tech Institute B', + logoImageUrl: 'logo-b.png', + certificateLogoImageUrl: null, + }, +]; + +const defaultProps = { + programTitle: mockProgramTitle, + programType: mockProgramType, + authoringOrganizations: mockOrganizations, +}; + +const renderComponent = (props = {}): RenderResult => { + (getProgramIcon as jest.Mock).mockReturnValue(mockProgramIconUrl); + + return render( + + ; + , + ); +}; + +describe('ProgramProgressHeader', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the program title and uses getProgramIcon correctly', () => { + renderComponent(); + + expect(screen.getByRole('heading', { name: mockProgramTitle })).toBeInTheDocument(); + + expect(getProgramIcon).toHaveBeenCalledWith(mockProgramType); + + const programIcon = screen.getByAltText(`${mockProgramType} icon`); + expect(programIcon).toBeInTheDocument(); + expect(programIcon).toHaveAttribute('src', mockProgramIconUrl); + }); + + it('renders the institutions header text', () => { + renderComponent(); + + expect(screen.getByText('Institutions')).toBeInTheDocument(); + }); + + it('renders all organization logos and uses certificateLogoImageUrl first', () => { + renderComponent(); + + const orgImages = screen.getAllByRole('img', { name: /logo/i }); + expect(orgImages).toHaveLength(mockOrganizations.length); + + const logoA = screen.getByAltText("University A's logo"); + expect(logoA).toHaveAttribute('src', mockOrganizations[0].certificateLogoImageUrl); + + const logoB = screen.getByAltText("Tech Institute B's logo"); + expect(logoB).toHaveAttribute('src', mockOrganizations[1].logoImageUrl); + }); + + it('does NOT render the institutions column if authoringOrganizations is empty', () => { + renderComponent({ authoringOrganizations: [] }); + + expect(screen.queryByText('Institutions')).not.toBeInTheDocument(); + }); + + it('does NOT render the institutions column if authoringOrganizations is null', () => { + renderComponent({ authoringOrganizations: null }); + + expect(screen.queryByText('Institutions')).not.toBeInTheDocument(); + }); +}); diff --git a/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressHeader.tsx b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressHeader.tsx index 574173dc4..8f5c23bda 100644 --- a/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressHeader.tsx +++ b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressHeader.tsx @@ -1,25 +1,44 @@ import React from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Row, Col } from '@openedx/paragon'; import { ProgramProgressHeaderProps } from '../data/types'; import { getProgramIcon } from '../data/util'; +import messages from './messages'; + const ProgramProgressHeader: React.FC = ({ programTitle, programType, authoringOrganizations, }) => { + const { formatMessage } = useIntl(); const programIcon = getProgramIcon(programType); return ( -
-

- {programTitle} -

-

- {programType} -

-

- {authoringOrganizations && authoringOrganizations.length} -

- {`${programType} -
+ + + {`${programType} +

+ {programTitle} +

+ + {authoringOrganizations && authoringOrganizations?.length > 0 && ( + +
+ {formatMessage(messages.programProgressInstitutions)} +
+
+ {authoringOrganizations.map(org => ( + {`${org.name}'s + ))} +
+ + )} +
); }; diff --git a/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressInfo.test.tsx b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressInfo.test.tsx new file mode 100644 index 000000000..ba581adb1 --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressInfo.test.tsx @@ -0,0 +1,43 @@ +import { render, RenderResult, screen } from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; + +import ProgramProgressInfo from './ProgramProgressInfo'; +import messages from './messages'; + +jest.mock('./UpgradeButton', () => ({ + UpgradeAllButton: 'UpgradeAllButton', +})); + +const defaultProps = { + allCoursesCompleted: true, + totalCoursesInProgram: 3, +}; + +const renderComponent = (props = {}): RenderResult => render( + + + , +); + +describe('ProramProgressInfo', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the correct text when all courses have been completed for a program', () => { + renderComponent(); + + expect(screen.getByText(messages.programProgressCompleteHeader.defaultMessage)).toBeInTheDocument(); + expect(screen.getByText(messages.programProgressCompleteText.defaultMessage)).toBeInTheDocument(); + }); + + it('renders the correct text when the user has NOT completed all of the courses in a program', () => { + renderComponent({ + ...defaultProps, + allCoursesCompleted: false, + }); + + expect(screen.getByText(messages.programProgressIncompleteHeader.defaultMessage)).toBeInTheDocument(); + expect(screen.getByTestId('program-incomplete-info-text')).toBeInTheDocument(); + }); +}); diff --git a/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressInfo.tsx b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressInfo.tsx index c4fb6b814..6c62fa165 100644 --- a/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressInfo.tsx +++ b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressInfo.tsx @@ -1,20 +1,36 @@ +/* eslint-disable max-len */ import React from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { ProgramProgressInfoProps } from '../data/types'; +import messages from './messages'; + +import { UpgradeAllButton } from './UpgradeButton'; +// TODO: for review: this component could be wrapped in a PluginSlot to allow us to inject the UpgradeAllButton while using the default content const ProgramProgressInfo: React.FC = ({ allCoursesCompleted, totalCoursesInProgram, -}) => ( - allCoursesCompleted - ? ( -
- Render all courses completed text -
- ) - : ( -
- Render upgrade button and info about the {totalCoursesInProgram} courses in this program -
- ) -); +}) => { + const { formatMessage } = useIntl(); + return ( +
+ {allCoursesCompleted + ? ( + <> +

{formatMessage(messages.programProgressCompleteHeader)}

+

{formatMessage(messages.programProgressCompleteText)}

+ + ) + : ( + <> +

{formatMessage(messages.programProgressIncompleteHeader)}

+

+ {formatMessage(messages.programProgressIncompleteText, { totalCoursesInProgram })} +

+ + + )} +
+ ); +}; export default ProgramProgressInfo; diff --git a/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressProvider.tsx.tsx b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressProvider.tsx.tsx new file mode 100644 index 000000000..489832915 --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramProgress/ProgramProgressProvider.tsx.tsx @@ -0,0 +1,52 @@ +import React, { + createContext, ReactNode, useMemo, useState, +} from 'react'; +import { ProgramProgressData } from '../data/types'; + +export interface ProgramProgressProviderProps { + children: ReactNode, +} + +export interface ProgramProgressContextValueType { + programProgressData: ProgramProgressData + setProgramProgressData: (data: ProgramProgressData) => void, +} + +const defaultContextValue: ProgramProgressContextValueType = { + programProgressData: { + urls: { + programListingUrl: undefined, + trackSelectionUrl: undefined, + commerceApiUrl: undefined, + buyButtonUrl: undefined, + programRecordUrl: undefined, + }, + programData: null, + courseData: null, + }, + setProgramProgressData: () => {}, +}; + +export const ProgramProgressContext = createContext(defaultContextValue); + +export const ProgramProgressContextProvider: React.FC = ({ children }) => { + const [programProgressData, setProgramProgressData] = useState(defaultContextValue.programProgressData); + + const memoValue = useMemo(():ProgramProgressContextValueType => ({ + programProgressData, + setProgramProgressData, + }), [programProgressData, setProgramProgressData]); + + return ( + + {children} + + ); +}; + +export default { + ProgramProgressContextProvider, + ProgramProgressContext, +}; diff --git a/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/UpgradeAllButton.test.tsx b/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/UpgradeAllButton.test.tsx new file mode 100644 index 000000000..a281a6a8d --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/UpgradeAllButton.test.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { render, RenderResult, screen } from '@testing-library/react'; +import UpgradeAllButton from './UpgradeAllButton'; +import { ProgramProgressContext } from '../ProgramProgressProvider.tsx'; +import messages from './messages'; + +const mockBuyButtonUrl = 'http://test-buy-url.com'; +const mockCurrency = 'USD'; +const mockDiscountedPrice = 500.00; +const mockListPrice = 800.00; + +const defaultContextValue = { + programProgressData: { + urls: { + programListingUrl: undefined, + trackSelectionUrl: undefined, + commerceApiUrl: undefined, + buyButtonUrl: mockBuyButtonUrl, + programRecordUrl: undefined, + }, + programData: { + discountData: null, + }, + courseData: {}, + }, + setProgramProgressData: () => {}, +}; + +const renderComponent = (contextValue): RenderResult => render( + + + + + , +); + +describe('UpgradeAllButton', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('renders the button with the correct text and link in the NOT discounted state', () => { + const contextValue = { + ...defaultContextValue, + programProgressData: { + ...defaultContextValue.programProgressData, + programData: { + discountData: { + currency: mockCurrency, + isDiscounted: false, + totalInclTaxExclDiscounts: mockListPrice, + totalInclTax: mockListPrice, + }, + }, + }, + }; + + renderComponent(contextValue); + + const button = screen.getByTestId('upgrade-all-button'); + + expect(button).toHaveTextContent(messages.upgradeAllRemainingCoursesButtonText.defaultMessage); + + expect(button).toHaveAttribute('href', mockBuyButtonUrl); + + expect(screen.getByText(`$${mockListPrice.toFixed(2)}`)).toBeInTheDocument(); + }); + + it('renders the button with the correct text and link in the discounted state', () => { + const contextValue = { + ...defaultContextValue, + programProgressData: { + ...defaultContextValue.programProgressData, + programData: { + discountData: { + currency: mockCurrency, + isDiscounted: true, + totalInclTaxExclDiscounts: mockListPrice, + totalInclTax: mockDiscountedPrice, + }, + }, + }, + }; + + renderComponent(contextValue); + + const button = screen.getByTestId('upgrade-all-button'); + + expect(button).toHaveTextContent(messages.upgradeAllRemainingCoursesButtonText.defaultMessage); + + expect(button).toHaveAttribute('href', mockBuyButtonUrl); + + expect(screen.getByText(`$${mockListPrice.toFixed(2)}`)).toBeInTheDocument(); + expect(screen.getByText(`$${mockDiscountedPrice.toFixed(2)}`)).toBeInTheDocument(); + }); +}); diff --git a/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/UpgradeAllButton.tsx b/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/UpgradeAllButton.tsx new file mode 100644 index 000000000..c22fba49f --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/UpgradeAllButton.tsx @@ -0,0 +1,78 @@ +import React, { useContext } from 'react'; +import { Button } from '@openedx/paragon'; +import { FormattedNumber, useIntl } from '@edx/frontend-platform/i18n'; +import messages from './messages'; +import './index.scss'; +import { ProgramProgressContext, ProgramProgressContextValueType } from '../ProgramProgressProvider.tsx'; + +const UpgradeAllButton: React.FC = () => { + const { formatMessage } = useIntl(); + const { programProgressData } = useContext(ProgramProgressContext); + const { urls } = programProgressData; + + const getAllRemainingCoursesPrice = () => { + const { discountData } = programProgressData.programData; + + if (discountData) { + const { + currency, + isDiscounted, + totalInclTaxExclDiscounts, + totalInclTax, + } = discountData; + + if (isDiscounted) { + return ( + <> + + + + + + + + ); + } + return ( + + + + ); + } + return null; + }; + + return ( + + ); +}; + +export default UpgradeAllButton; diff --git a/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/index.scss b/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/index.scss new file mode 100644 index 000000000..0f0a8ad47 --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/index.scss @@ -0,0 +1,7 @@ +.upgrade-all-button { + gap: calc(var(--pgn-spacing-spacer-base) * 0.25); +} + +.list-price { + text-decoration: line-through; +} \ No newline at end of file diff --git a/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/index.tsx b/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/index.tsx new file mode 100644 index 000000000..9b270665d --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/index.tsx @@ -0,0 +1,5 @@ +import UpgradeAllButton from './UpgradeAllButton'; + +export { + UpgradeAllButton, +}; diff --git a/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/messages.ts b/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/messages.ts new file mode 100644 index 000000000..01a40d215 --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramProgress/UpgradeButton/messages.ts @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + upgradeAllRemainingCoursesButtonText: { + defaultMessage: 'Upgrade All Remaining Courses', + id: 'upgrade.course.all.button', + description: 'Button text for ', + }, +}); + +export default messages; diff --git a/src/containers/ProgramDashboard/ProgramProgress/index.scss b/src/containers/ProgramDashboard/ProgramProgress/index.scss new file mode 100644 index 000000000..426ab0c5b --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramProgress/index.scss @@ -0,0 +1,8 @@ +.program-title { + font-weight: normal; + font-size: 2em; +} + +.program-icon { + width: 300px; +} \ No newline at end of file diff --git a/src/containers/ProgramDashboard/ProgramProgress/index.tsx b/src/containers/ProgramDashboard/ProgramProgress/index.tsx index bcc4bcd90..3821db847 100644 --- a/src/containers/ProgramDashboard/ProgramProgress/index.tsx +++ b/src/containers/ProgramDashboard/ProgramProgress/index.tsx @@ -1,83 +1,11 @@ -import React, { useEffect, useState } from 'react'; -import { Helmet } from 'react-helmet'; -import { useParams } from 'react-router-dom'; -import { Col, Container, Row } from '@openedx/paragon'; -import { logError } from '@edx/frontend-platform/logging'; -import { camelCaseObject } from '@edx/frontend-platform/utils'; -import { getProgramProgressData } from '../data/api'; -import { ProgramProgressData } from '../data/types'; -import ProgramProgressCourses from './ProgramProgressCourses'; -import ProgramProgressHeader from './ProgramProgressHeader'; -import ProgramProgressSidebar from './ProgramProgressSidebar'; -import ProgramProgressInfo from './ProgramProgressInfo'; +import React from 'react'; +import { ProgramProgressContextProvider } from './ProgramProgressProvider.tsx'; +import ProgramProgress from './ProgramProgress'; -const ProgramProgress: React.FC = () => { - const [programProgressData, setProgramProgressData] = useState(); - const [programProgressEndpointError, setProgramProgressEndpointError] = useState(false); - const hasProgramProgressData : Boolean = programProgressData?.courseData - && programProgressData.programData - && programProgressData.urls; +const ProgramProgressWithProvider = () => ( + + + +); - // TODO: for review: https://stackoverflow.com/questions/75706357/react-useparams-returns-string-undefined - const { uuid } = useParams() as { uuid: string }; - useEffect(() => { - getProgramProgressData(uuid) - .then(responseData => { - setProgramProgressData(camelCaseObject(responseData.data)); - }) - .catch(err => { - logError(err); - setProgramProgressEndpointError(true); - }); - }, [uuid]); - - if (programProgressEndpointError) { - return ( -
Not found page
- ); - } - - if (!hasProgramProgressData) { - return ( -
Loading...
- ); - } - - const programData = programProgressData?.programData; - const courseData = programProgressData?.courseData; - - const totalCoursesInProgram = (courseData.notStarted?.length || 0) - + (courseData.completed?.length || 0) - + (courseData.inProgress?.length || 0); - - const allCoursesCompleted = !courseData.notStarted?.length - && !courseData.inProgress?.length - && courseData.completed?.length; - - return ( - <> - - - - - - - - - - - - - - - ); -}; - -export default ProgramProgress; +export default ProgramProgressWithProvider; diff --git a/src/containers/ProgramDashboard/ProgramProgress/messages.ts b/src/containers/ProgramDashboard/ProgramProgress/messages.ts new file mode 100644 index 000000000..ddcc91e36 --- /dev/null +++ b/src/containers/ProgramDashboard/ProgramProgress/messages.ts @@ -0,0 +1,31 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + programProgressInstitutions: { + defaultMessage: 'Institutions', + id: 'program.progress.institutions', + description: 'Label text for organization image logos', + }, + programProgressCompleteHeader: { + defaultMessage: 'Congratulations!', + id: 'program.progress.complete.header', + description: 'header text for the program progress page when all courses are complete', + }, + programProgressCompleteText: { + defaultMessage: 'You have successfully completed all the requirements for the Adventures and the Final Frontier Professional Certificate.', + id: 'program.progress.complete.text', + description: 'text to display when a user has completed all of the courses for the program', + }, + programProgressIncompleteHeader: { + defaultMessage: 'Your Program Journey', + id: 'program.progress.incomplete.header', + description: 'header text to display when the user has remaining incomplete courses in the program', + }, + programProgressIncompleteText: { + defaultMessage: 'Track and plan your progress through the {totalCoursesInProgram} courses in this program. To complete the program, you must earn a verified certificate for each course.', + id: 'program.progress.incomplete.text', + description: 'text to diplay when a user has not completed all of the courses for a program', + }, +}); + +export default messages; diff --git a/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.test.tsx b/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.test.tsx index 4793ba39b..7d5aee480 100644 --- a/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.test.tsx +++ b/src/containers/ProgramDashboard/ProgramsList/ExploreProgramsCTA.test.tsx @@ -18,7 +18,7 @@ describe('ExploreProgramsCTA', () => { }); const renderComponent = () => render( - + , ); diff --git a/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.test.tsx b/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.test.tsx index b9cbc091a..43c9c80d8 100644 --- a/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.test.tsx +++ b/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, RenderResult } from '@testing-library/react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import ProgramListCard from './ProgramListCard'; import { ProgramData } from '../data/types'; @@ -18,26 +18,45 @@ const mockBaseProgram = { large: { url: 'banner-large.jpg', width: 1440, height: 480 }, }, authoringOrganizations: [ - { key: 'test-key', logoImageUrl: 'test-logo.png' }, + { + uuid: 'org-uuid-1', + key: 'test-key', + name: 'test-org-1', + logoImageUrl: 'test-logo.png', + certificateLogoImageUrl: 'test-cert-logo.png', + }, ], progress: { inProgress: 1, notStarted: 2, completed: 3, }, + discountData: {}, }; const mockMultipleOrgProgram = { ...mockBaseProgram, authoringOrganizations: [ - { key: 'MIT', logoImageUrl: 'mit-logo.png' }, - { key: 'HU', logoImageUrl: 'harvard-logo.png' }, + { + uuid: 'org-uuid-1', + name: 'MIT', + key: 'MITx', + logoImageUrl: 'mit-logo.png', + certificateLogoImageUrl: 'mit-cert-logo-1.png', + }, + { + uuid: 'org-uuid-2', + name: 'Harvard', + key: 'Harvardx', + logoImageUrl: 'harvard-logo.png', + certificateLogoImageUrl: 'harvard-cert-logo-2.png', + }, ], }; describe('ProgramListCard', () => { - const renderComponent = (programData: ProgramData = mockBaseProgram) => render( - + const renderComponent = (programData: ProgramData = mockBaseProgram): RenderResult => render( + , ); diff --git a/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.tsx b/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.tsx index 9ef066405..d8b195703 100644 --- a/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.tsx +++ b/src/containers/ProgramDashboard/ProgramsList/ProgramListCard.tsx @@ -6,7 +6,7 @@ import { Card, Row, } from '@openedx/paragon'; -import { ProgramCardProps, AuthoringOrganization } from '../data/types'; +import { ProgramCardProps } from '../data/types'; import ProgressCategoryBubbles from './ProgressCategoryBubbles'; const ProgramListCard: React.FC = ({ @@ -26,7 +26,7 @@ const ProgramListCard: React.FC = ({ }; }, []); - const getBannerImageURL = () => { + const getBannerImageURL = (): string => { let imageURL = ''; // We need to check that the breakpoint value exists before using it // Otherwise TypeScript will flag it as it can potentially be undefined in Paragon @@ -42,22 +42,16 @@ const ProgramListCard: React.FC = ({ return imageURL; }; - // Set key and logoImageUrl to empty strings for fallback image or instances where there are multiple organizations - let authoringOrganization : AuthoringOrganization = { - key: '', - logoImageUrl: '', + const getOrgImageUrl = (): string => { + // Otherwise use the logoImageUrl and key for the organization + if (program.authoringOrganizations?.length === 1 && program.authoringOrganizations[0].logoImageUrl) { + return program.authoringOrganizations[0].logoImageUrl; + } + return ''; }; - // Otherwise use the logoImageUrl and key for the organization - if (program.authoringOrganizations?.length === 1 && program.authoringOrganizations[0].logoImageUrl) { - authoringOrganization = { - logoImageUrl: program.authoringOrganizations[0].logoImageUrl, - key: program.authoringOrganizations[0].key, - }; - } return ( = ({ src={getBannerImageURL() || cardFallbackImg} srcAlt={`program card image for ${program.title}`} fallbackSrc={cardFallbackImg} - logoSrc={authoringOrganization?.logoImageUrl} - logoAlt={authoringOrganization?.key} - className="banner-image" + logoSrc={getOrgImageUrl()} + logoAlt={program.authoringOrganizations && program.authoringOrganizations[0]?.key} /> diff --git a/src/containers/ProgramDashboard/ProgramsList/ProgressCategoryBubbles.test.tsx b/src/containers/ProgramDashboard/ProgramsList/ProgressCategoryBubbles.test.tsx index 1bc5c5bd9..2aaffac4e 100644 --- a/src/containers/ProgramDashboard/ProgramsList/ProgressCategoryBubbles.test.tsx +++ b/src/containers/ProgramDashboard/ProgramsList/ProgressCategoryBubbles.test.tsx @@ -6,7 +6,7 @@ import ProgressCategoryBubbles from './ProgressCategoryBubbles'; describe('ProgressCategoryBubbles', () => { it('renders the correct values for each category', () => { render( - + , ); diff --git a/src/containers/ProgramDashboard/ProgramsList/index.test.tsx b/src/containers/ProgramDashboard/ProgramsList/index.test.tsx index 5122079aa..fb28f7c59 100644 --- a/src/containers/ProgramDashboard/ProgramsList/index.test.tsx +++ b/src/containers/ProgramDashboard/ProgramsList/index.test.tsx @@ -43,7 +43,7 @@ describe('ProgramsList', () => { }); const renderComponent = () => render( - + , ); diff --git a/src/containers/ProgramDashboard/ProgramsList/index.tsx b/src/containers/ProgramDashboard/ProgramsList/index.tsx index 9b0df9fa4..4b2ad133e 100644 --- a/src/containers/ProgramDashboard/ProgramsList/index.tsx +++ b/src/containers/ProgramDashboard/ProgramsList/index.tsx @@ -29,6 +29,7 @@ const ProgramsList: React.FC = () => { // TODO: add error handling alert component }, []); + // TODO: add handling of no programs data return ( <> @@ -36,11 +37,11 @@ const ProgramsList: React.FC = () => { {formatMessage(messages.programDashboardPageTitle)} - +
{formatMessage(messages.programsListHeaderText)}
- + {programsData.map(program => ( diff --git a/src/containers/ProgramDashboard/ProgramsList/messages.ts b/src/containers/ProgramDashboard/ProgramsList/messages.ts index eca4ba21a..851289658 100644 --- a/src/containers/ProgramDashboard/ProgramsList/messages.ts +++ b/src/containers/ProgramDashboard/ProgramsList/messages.ts @@ -12,7 +12,7 @@ const messages = defineMessages({ description: 'Header text for the programs list', }, exploreCoursesCTAText: { - defaultMessage: 'Browse recently launched courses and see what's new in your favorite subjects', + defaultMessage: 'Browse recently launched courses and see what\'s new in your favorite subjects', id: 'explore.courses.cta.text', description: 'Call-to-action text for the explore courses component', }, diff --git a/src/containers/ProgramDashboard/data/types.d.ts b/src/containers/ProgramDashboard/data/types.d.ts index d756a4cb0..7b162200d 100644 --- a/src/containers/ProgramDashboard/data/types.d.ts +++ b/src/containers/ProgramDashboard/data/types.d.ts @@ -11,6 +11,7 @@ export interface ProgramData { }, authoringOrganizations?: AuthoringOrganization[], progress: Progress, + discountData: any, } export interface ImageData { @@ -20,8 +21,11 @@ export interface ImageData { } export interface AuthoringOrganization { + uuid: string, key: string, + name: string, logoImageUrl: string, + certificateLogoImageUrl: string | null, } export interface Progress { @@ -37,11 +41,11 @@ export interface ProgramCardProps { // Program Progress types export interface ProgramProgressData { urls: { - program_listing_url: string | null, - track_selection_url: string | null, - commerce_api_url: string | null, - buy_button_url: string | null, - program_record_url: string | null + programListingUrl: string | undefined, + trackSelectionUrl: string | undefined, + commerceApiUrl: string | undefined, + buyButtonUrl: string | undefined, + programRecordUrl: string | undefined }, courseData: any, programData: any