diff --git a/src/components/Banner.jsx b/src/components/Banner.jsx index 1aeaf63..306616b 100644 --- a/src/components/Banner.jsx +++ b/src/components/Banner.jsx @@ -15,7 +15,7 @@ Banner.defaultProps = { }; Banner.propTypes = { variant: PropTypes.string, - icon: PropTypes.string, + icon: PropTypes.func, children: PropTypes.node.isRequired, }; diff --git a/src/containers/CourseCard/components/Banners/CertificateBanner.jsx b/src/containers/CourseCard/components/Banners/CertificateBanner.jsx index 5b9dd9d..9074d5e 100644 --- a/src/containers/CourseCard/components/Banners/CertificateBanner.jsx +++ b/src/containers/CourseCard/components/Banners/CertificateBanner.jsx @@ -1,68 +1,68 @@ /* eslint-disable max-len */ import React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import { Hyperlink } from '@edx/paragon'; import { CheckCircle } from '@edx/paragon/icons'; -import shapes from 'data/services/lms/shapes'; - +import { selectors } from 'data/redux'; import Banner from 'components/Banner'; +const { cardData } = selectors; + const restrictedMessage = 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting '; -export const CertificateBanner = ({ cardData }) => { - const { - certificates, - courseRun, - enrollment, - grades, - } = cardData; - if (certificates.isRestricted) { - if (enrollment.isAudit) { - return ( - - {restrictedMessage}info@example.com - - ); - } +export const CertificateBanner = ({ courseNumber }) => { + const cardValue = (sel) => useSelector(cardData.cardSelector(sel, courseNumber)); + + const isRestricted = cardValue(cardData.isRestricted); + const isAudit = cardValue(cardData.isAudit); + const isVerified = cardValue(cardData.isVerified); + if (isRestricted) { return ( {restrictedMessage}info@example.com - If you would like a refund on your Certificate of Achievement, please contact our billing address billing@example.com + {isVerified && ( + <> + If you would like a refund on your Certificate of Achievement, please contact our billing address billing@example.com + + )} ); } - if (!grades.isPassing) { - if (enrollment.isAudit) { - return ( - - Grade required to pass the course: {courseRun.minPassingGrade}% - - ); + const isPassing = cardValue(cardData.isPassing); + const minPassingGrade = cardValue(cardData.minPassingGrade); + const isCourseRunFinished = cardValue(cardData.isCourseRunFinished); + const isCertDownloadable = cardValue(cardData.isCertDownloadable); + const isCertEarnedButUnavailable = cardValue(cardData.isCertEarnedButUnavailable); + const certAvailableDate = cardValue(cardData.certAvailableDate); + if (!isPassing) { + if (isAudit) { + return ( Grade required to pass the course: {minPassingGrade}% ); } - if (courseRun.isFinished) { + if (isCourseRunFinished) { return ( - You are not eligible for a certificate. - {' '} - View grades. + You are not eligible for a certificate. View grades. ); } - return ( - Grade required for a certificate: {courseRun.minPassingGrade}% + Grade required for a certificate: {minPassingGrade}% ); } - if (certificates.isDownloadable) { - if (certificates.downloadUrls.preview) { + if (isCertDownloadable) { + const certDownloadUrl = cardValue(cardData.certDownloadUrl); + const certPreviewUrl = cardValue(cardData.certPreviewUrl); + if (certPreviewUrl) { return ( Congratulations. Your certificate is ready. {' '} - View Certificate. + View Certificate. ); } @@ -70,14 +70,14 @@ export const CertificateBanner = ({ cardData }) => { Congratulations. Your certificate is ready. {' '} - Download Certificate. + Download Certificate. ); } - if (certificates.isEarned && !certificates.isAvailable) { + if (isCertEarnedButUnavailable) { return ( - Your grade and certificate will be ready after {certificates.availableDate}. + Your grade and certificate will be ready after {certAvailableDate}. ); } @@ -85,7 +85,7 @@ export const CertificateBanner = ({ cardData }) => { return null; }; CertificateBanner.propTypes = { - cardData: shapes.courseRunCardData.isRequired, + courseNumber: PropTypes.string.isRequired, }; export default CertificateBanner; diff --git a/src/containers/CourseCard/components/Banners/CourseBanner.jsx b/src/containers/CourseCard/components/Banners/CourseBanner.jsx index f39b609..5559423 100644 --- a/src/containers/CourseCard/components/Banners/CourseBanner.jsx +++ b/src/containers/CourseCard/components/Banners/CourseBanner.jsx @@ -1,22 +1,23 @@ /* eslint-disable max-len */ import React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import { Hyperlink } from '@edx/paragon'; -import shapes from 'data/services/lms/shapes'; +import { selectors } from 'data/redux'; import Banner from 'components/Banner'; -export const CourseBanner = ({ cardData }) => { - const { - // course, - enrollment, - courseRun, - } = cardData; - if (enrollment.isVerified) { - return null; - } - const isActive = courseRun.isStarted && !courseRun.isFinished; - const { canUpgrade, isAuditAccessExpired } = enrollment; +const { cardData } = selectors; + +export const CourseBanner = ({ courseNumber }) => { + const cardValue = (sel) => useSelector(cardData.cardSelector(sel, courseNumber)); + const isVerified = cardValue(cardData.isVerified); + if (isVerified) { return null; } + + const isCourseRunActive = cardValue(cardData.isCourseRunActive); + const canUpgrade = cardValue(cardData.canUpgrade); + const isAuditAccessExpired = cardValue(cardData.isAuditAccessExpired); if (isAuditAccessExpired) { if (canUpgrade) { return ( @@ -31,19 +32,20 @@ export const CourseBanner = ({ cardData }) => { ); } - if (isActive && !canUpgrade) { + if (isCourseRunActive && !canUpgrade) { + const courseWebsite = cardValue(cardData.courseWebsite); return ( Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future. {' '} - Explore course details. + Explore course details. ); } return null; }; CourseBanner.propTypes = { - cardData: shapes.courseRunCardData.isRequired, + courseNumber: PropTypes.string.isRequired, }; export default CourseBanner; diff --git a/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx b/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx index 1767c62..1bbee7d 100644 --- a/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx +++ b/src/containers/CourseCard/components/Banners/EntitlementBanner.jsx @@ -1,42 +1,31 @@ import React from 'react'; -import { Hyperlink } from '@edx/paragon'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; -import shapes from 'data/services/lms/shapes'; +import { selectors } from 'data/redux'; import Banner from 'components/Banner'; -export const EntitlementBanner = ({ cardData }) => { - const { entitlements } = cardData; - if (!entitlements.isEntitlement) { - return null; - } - if (entitlements.isExpired) { - return null; - } - if (!entitlements.isFulfilled) { - if (entitlements.canChange) { - return ( - You must select a session to access the course. - ); - } - return ( - The deadline to select a session has passed - ); - } - if (entitlements.canChange) { - return ( - - You can change sessions until {entitlements.changeDeadline}. - {' '} - Change or leave session - - ); - } +const { cardData } = selectors; - return null; +export const EntitlementBanner = ({ courseNumber }) => { + const cardValue = (sel) => useSelector(cardData.cardSelector(sel, courseNumber)); + const isEntitlement = cardValue(cardData.isEntitlement); + if (!isEntitlement) { + return null; + } + const isExpired = cardValue(cardData.isEntitlementExpired); + const isFulfilled = cardValue(cardData.isEntitlementFulfilled); + if (isExpired || isFulfilled) { + return null; + } + const canChange = cardValue(cardData.canChangeEntitlementSession); + return canChange + ? (You must select a session to access the course.) + : (The deadline to select a session has passed); }; EntitlementBanner.propTypes = { - cardData: shapes.courseRunCardData.isRequired, + courseNumber: PropTypes.string.isRequired, }; export default EntitlementBanner; diff --git a/src/containers/CourseCard/components/CourseCardActions.jsx b/src/containers/CourseCard/components/CourseCardActions.jsx index 466136a..40de33e 100644 --- a/src/containers/CourseCard/components/CourseCardActions.jsx +++ b/src/containers/CourseCard/components/CourseCardActions.jsx @@ -1,43 +1,50 @@ import React from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import { Button } from '@edx/paragon'; import { Locked } from '@edx/paragon/icons'; -import shapes from 'data/services/lms/shapes'; +import { selectors } from 'data/redux'; + +const { cardData } = selectors; + +export const CourseCardActions = ({ courseNumber }) => { + const cardValue = (sel) => useSelector(cardData.cardSelector(sel, courseNumber)); + const canUpgrade = cardValue(cardData.canUpgrade); + const isAudit = cardValue(cardData.isAudit); + const isAuditAccessExpired = cardValue(cardData.isAuditAccessExpired); + const isVerified = cardValue(cardData.isVerified); + const isPending = cardValue(cardData.isCourseRunPending); + const isFinished = cardValue(cardData.isCourseRunFinished); -export const CourseCardActions = ({ cardData: { enrollment, courseRun } }) => { let primary; let secondary = null; - if (!enrollment.isVerified) { + if (!isVerified) { secondary = ( ); } - if (courseRun.isPending) { - primary = ( - - ); - } else if (!courseRun.isEnded) { - if (enrollment.isAudit && enrollment.isAuditAccessExpired) { - primary = (); - } else { - primary = (); - } + + if (isPending) { + primary = (); + } else if (!isFinished) { + primary = (isAudit && isAuditAccessExpired) + ? () + : (); } else { primary = (); } return (<>{secondary}{primary}); }; CourseCardActions.propTypes = { - cardData: shapes.courseRunCardData.isRequired, + courseNumber: PropTypes.string.isRequired, }; export default CourseCardActions; diff --git a/src/containers/CourseCard/components/CourseCardMenu/index.jsx b/src/containers/CourseCard/components/CourseCardMenu/index.jsx index d3bef47..f61995d 100644 --- a/src/containers/CourseCard/components/CourseCardMenu/index.jsx +++ b/src/containers/CourseCard/components/CourseCardMenu/index.jsx @@ -1,14 +1,14 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { Dropdown, Icon, IconButton } from '@edx/paragon'; import { MoreVert } from '@edx/paragon/icons'; -import shapes from 'data/services/lms/shapes'; import EmailSettingsModal from 'containers/EmailSettingsModal'; import UnenrollConfirmModal from 'containers/UnenrollConfirmModal'; import hooks from './hooks'; -export const CourseCardMenu = ({ cardData }) => { +export const CourseCardMenu = ({ courseNumber }) => { const { emailSettingsModal, unenrollModal, @@ -34,17 +34,18 @@ export const CourseCardMenu = ({ cardData }) => { ); }; CourseCardMenu.propTypes = { - cardData: shapes.courseRunCardData.isRequired, + courseNumber: PropTypes.string.isRequired, }; export default CourseCardMenu; diff --git a/src/containers/CourseCard/components/RelatedProgramsBadge.jsx b/src/containers/CourseCard/components/RelatedProgramsBadge.jsx index ec86a79..a547cfb 100644 --- a/src/containers/CourseCard/components/RelatedProgramsBadge.jsx +++ b/src/containers/CourseCard/components/RelatedProgramsBadge.jsx @@ -1,12 +1,12 @@ /* eslint-disable quotes */ import React from 'react'; +import PropTypes from 'prop-types'; import { Button, useToggle } from '@edx/paragon'; import { Program } from '@edx/paragon/icons'; -import shapes from 'data/services/lms/shapes'; import RelatedProgramsBadgeModal from 'containers/RelatedProgramsModal'; -export const RelatedProgramsBadge = ({ cardData }) => { +export const RelatedProgramsBadge = ({ courseNumber }) => { const [isOpen, open, closeModal] = useToggle(false); return ( <> @@ -18,12 +18,12 @@ export const RelatedProgramsBadge = ({ cardData }) => { > 2 Related Program - + ); }; RelatedProgramsBadge.propTypes = { - cardData: shapes.courseRunCardData.isRequired, + courseNumber: PropTypes.string.isRequired, }; export default RelatedProgramsBadge; diff --git a/src/containers/CourseCard/index.jsx b/src/containers/CourseCard/index.jsx index 45d039e..1bc4064 100644 --- a/src/containers/CourseCard/index.jsx +++ b/src/containers/CourseCard/index.jsx @@ -1,8 +1,12 @@ import React from 'react'; +import PropTypes from 'prop-types'; + // import PropTypes from 'prop-types'; import { Card } from '@edx/paragon'; -import shapes from 'data/services/lms/shapes'; +import { selectors } from 'data/redux'; + +import { getCardValue } from 'hooks'; import RelatedProgramsBadge from './components/RelatedProgramsBadge'; import CourseCardMenu from './components/CourseCardMenu'; @@ -13,52 +17,48 @@ import { } from './components/Banners'; import CourseCardActions from './components/CourseCardActions'; -export const CourseCard = ({ cardData }) => { - const { - course: { - title, - bannerUrl: imageUrl, - }, - courseRun: { - courseNumber, - accessExpirationDate, - }, - } = cardData; - const providerName = cardData.provider?.name; +const { cardData } = selectors; + +export const CourseCard = ({ courseNumber }) => { + const cardValue = getCardValue(courseNumber); + const title = cardValue(cardData.courseTitle); + const bannerUrl = cardValue(cardData.courseBannerUrl); + const accessExpirationDate = cardValue(cardData.courseRunAccessExpirationDate); + const providerName = cardValue(cardData.providerName); return (
} + actions={} /> {providerName || 'Unkown'} • {courseNumber} • Access expires {accessExpirationDate} } + textElement={} > - +
- - - + + +
); }; CourseCard.propTypes = { - cardData: shapes.courseRunCardData.isRequired, + courseNumber: PropTypes.string.isRequired, }; CourseCard.defaultProps = {}; diff --git a/src/containers/CourseList/index.jsx b/src/containers/CourseList/index.jsx index 29ec03b..b427308 100644 --- a/src/containers/CourseList/index.jsx +++ b/src/containers/CourseList/index.jsx @@ -1,19 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; -import shapes from 'data/services/lms/shapes'; import CourseCard from 'containers/CourseCard'; export const CourseList = ({ courseListData }) => (
- {courseListData.map((cardData) => ( - + {courseListData.map((courseNumber) => ( + ))}
); CourseList.propTypes = { - courseListData: PropTypes.arrayOf(shapes.courseRunCardData).isRequired, + courseListData: PropTypes.arrayOf(PropTypes.string).isRequired, }; export default CourseList; diff --git a/src/containers/EmailSettingsModal/hooks.js b/src/containers/EmailSettingsModal/hooks.js index d42e5b9..9e2f23c 100644 --- a/src/containers/EmailSettingsModal/hooks.js +++ b/src/containers/EmailSettingsModal/hooks.js @@ -2,19 +2,24 @@ import React from 'react'; import { StrictDict } from 'utils'; // import { thunkActions } from 'data/redux'; +import { selectors } from 'data/redux'; +import { getCardValue } from 'hooks'; import * as module from './hooks'; +const { cardData } = selectors; + export const state = StrictDict({ toggle: (val) => React.useState(val), }); export const modalHooks = ({ - cardData, closeModal, + courseNumber, // dispatch, }) => { - const { isEmailEnabled } = cardData.enrollment; + const cardValue = getCardValue(courseNumber); + const isEmailEnabled = cardValue(cardData.isEmailEnabled); const [toggleValue, setToggleValue] = module.state.toggle(isEmailEnabled); const onToggle = React.useCallback(() => setToggleValue(!toggleValue), [toggleValue]); diff --git a/src/containers/EmailSettingsModal/index.jsx b/src/containers/EmailSettingsModal/index.jsx index 6ffcdaa..5c52979 100644 --- a/src/containers/EmailSettingsModal/index.jsx +++ b/src/containers/EmailSettingsModal/index.jsx @@ -11,7 +11,6 @@ import { } from '@edx/paragon'; import { nullMethod } from 'hooks'; -import shapes from 'data/services/lms/shapes'; import hooks from './hooks'; import messages from './messages'; @@ -19,14 +18,14 @@ import messages from './messages'; export const EmailSettingsModal = ({ closeModal, show, - cardData, + courseNumber, }) => { const dispatch = useDispatch(); const { toggleValue, onToggle, save, - } = hooks({ dispatch, closeModal, cardData }); + } = hooks({ dispatch, closeModal, courseNumber }); const { formatMessage } = useIntl(); return ( @@ -34,6 +33,7 @@ export const EmailSettingsModal = ({ isOpen={show} onClose={nullMethod} hasCloseButton={false} + title="" >

{formatMessage(messages.header)}

@@ -52,7 +52,7 @@ export const EmailSettingsModal = ({ ); }; EmailSettingsModal.propTypes = { - cardData: shapes.courseRunCardData.isRequired, + courseNumber: PropTypes.string.isRequired, closeModal: PropTypes.func.isRequired, show: PropTypes.bool.isRequired, }; diff --git a/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx b/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx index c85298c..397dcce 100644 --- a/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx +++ b/src/containers/LearnerDashboardHeader/AuthenticatedUserDropdown.jsx @@ -11,7 +11,7 @@ import messages from './messages'; export const AuthenticatedUserDropdown = ({ intl, username }) => ( <> - + {username} diff --git a/src/containers/LearnerDashboardHeader/index.jsx b/src/containers/LearnerDashboardHeader/index.jsx index 122bb4e..4dba2c2 100644 --- a/src/containers/LearnerDashboardHeader/index.jsx +++ b/src/containers/LearnerDashboardHeader/index.jsx @@ -11,8 +11,6 @@ import messages from './messages'; export const LearnerDashboardHeader = () => { const { authenticatedUser } = useContext(AppContext); - const context = useContext(AppContext); - console.log({ context }); const { formatMessage } = useIntl(); return (
diff --git a/src/containers/RelatedProgramsModal/components/ProgramCard.jsx b/src/containers/RelatedProgramsModal/components/ProgramCard.jsx index 868c944..d60a538 100644 --- a/src/containers/RelatedProgramsModal/components/ProgramCard.jsx +++ b/src/containers/RelatedProgramsModal/components/ProgramCard.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import { @@ -9,7 +10,6 @@ import { } from '@edx/paragon'; import { Program } from '@edx/paragon/icons'; -import shapes from 'data/services/lms/shapes'; import './index.scss'; export const whiteFontWrapper = (node) => ({node}); @@ -20,11 +20,23 @@ export const messages = { defaultMessage: '{numCourses} Courses', description: 'Number of courses in a program, displayed at the bottom of program card', }, + duration: { + id: 'learnerDashboard.programCard.duration', + defaultMessage: '{numWeeks} Weeks', + description: 'Number of weeks in a program, displayed at the bottom of program card', + }, }; export const ProgramCard = ({ data }) => { const { formatMessage } = useIntl(); - const numCoursesMessage = formatMessage(messages.courses, { numCourses: data.numberOfCourses }); + const numCoursesMessage = formatMessage( + messages.courses, + { numCourses: data.numberOfCourses }, + ); + const durationMessage = formatMessage( + messages.duration, + { numWeeks: data.estimatedNumberOfWeeks }, + ); return ( { {data.programType}
- {numCoursesMessage} • {data.estimatedDuration} + {numCoursesMessage} • {durationMessage}
); }; ProgramCard.propTypes = { - data: shapes.programCard.isRequired, + data: PropTypes.shape({ + estimatedNumberOfWeeks: PropTypes.number, + numberOfCourses: PropTypes.number, + bannerUrl: PropTypes.string, + logoUrl: PropTypes.string, + title: PropTypes.string, + provider: PropTypes.string, + programType: PropTypes.string, + programUrl: PropTypes.string, + programTypeUrl: PropTypes.string, + }).isRequired, }; export default ProgramCard; diff --git a/src/containers/RelatedProgramsModal/hooks.js b/src/containers/RelatedProgramsModal/hooks.js index d42e5b9..b5f716c 100644 --- a/src/containers/RelatedProgramsModal/hooks.js +++ b/src/containers/RelatedProgramsModal/hooks.js @@ -1,36 +1,24 @@ -import React from 'react'; +import { selectors } from 'data/redux'; +import { getCardValue } from 'hooks'; -import { StrictDict } from 'utils'; -// import { thunkActions } from 'data/redux'; +const { cardData } = selectors; +const { programs } = cardData; -import * as module from './hooks'; - -export const state = StrictDict({ - toggle: (val) => React.useState(val), -}); - -export const modalHooks = ({ - cardData, - closeModal, - // dispatch, +export const programsModalData = ({ + courseNumber, }) => { - const { isEmailEnabled } = cardData.enrollment; - const [toggleValue, setToggleValue] = module.state.toggle(isEmailEnabled); - - const onToggle = React.useCallback(() => setToggleValue(!toggleValue), [toggleValue]); - const save = React.useCallback( - () => { - console.log('save email settings'); - closeModal(); - }, - [], - ); - + const cardValue = getCardValue(courseNumber); return { - onToggle, - save, - toggleValue, + courseTitle: cardValue(cardData.courseTitle), + relatedPrograms: cardValue(cardData.relatedPrograms).map(program => ({ + estimatedNumberOfWeeks: programs.estimatedNumberOfWeeks(program), + numberOfCourses: programs.numberOfCourses(program), + programType: programs.programType(program), + programTypeUrl: programs.programTypeUrl(program), + provider: programs.provider(program), + title: programs.title(program), + })), }; }; -export default modalHooks; +export default programsModalData; diff --git a/src/containers/RelatedProgramsModal/index.jsx b/src/containers/RelatedProgramsModal/index.jsx index 66093d6..f896b28 100644 --- a/src/containers/RelatedProgramsModal/index.jsx +++ b/src/containers/RelatedProgramsModal/index.jsx @@ -5,14 +5,18 @@ import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import { CardGrid, ModalDialog } from '@edx/paragon'; -import shapes from 'data/services/lms/shapes'; - import ProgramCard from './components/ProgramCard'; import messages from './messages'; +import programsData from './hooks'; import './index.scss'; -export const RelatedProgramsModal = ({ isOpen, closeModal, cardData }) => { +export const RelatedProgramsModal = ({ + isOpen, + closeModal, + courseNumber, +}) => { const { formatMessage } = useIntl(); + const { courseTitle, relatedPrograms } = programsData({ courseNumber }); return ( { {formatMessage(messages.header)} - {cardData.course.title} + {courseTitle}

{formatMessage(messages.description)}

- {cardData.relatedPrograms.map(programData => )} + {relatedPrograms.map((programData) => ( + + ))}
@@ -43,7 +49,7 @@ export const RelatedProgramsModal = ({ isOpen, closeModal, cardData }) => { RelatedProgramsModal.propTypes = { isOpen: PropTypes.bool.isRequired, closeModal: PropTypes.func.isRequired, - cardData: shapes.courseRunCardData.isRequired, + courseNumber: PropTypes.string.isRequired, }; export default RelatedProgramsModal; diff --git a/src/containers/UnenrollConfirmModal/hooks.js b/src/containers/UnenrollConfirmModal/hooks.js index b1d76d4..dd674a3 100644 --- a/src/containers/UnenrollConfirmModal/hooks.js +++ b/src/containers/UnenrollConfirmModal/hooks.js @@ -13,34 +13,34 @@ export const state = StrictDict({ submittedReason: (val) => React.useState(val), }); -export const modalHooks = ({ closeModal, dispatch }) => { - const [isConfirmed, setIsConfirmed] = module.state.confirmed(false); +export const modalStates = StrictDict({ + confirm: 'confirm', + reason: 'reason', + finished: 'finished', +}); + +export const unenrollReasons = () => { const [selectedReason, setSelectedReason] = module.state.selectedReason(null); const [submittedReason, setSubmittedReason] = module.state.submittedReason(null); - const [customOption, setCustomOption] = module.state.customReason(''); const [isSkipped, setIsSkipped] = module.state.isSkipped(false); + const [customOption, setCustomOption] = module.state.customReason(''); - const confirm = React.useCallback(() => setIsConfirmed(true), []); - - const close = () => { - closeModal(); - setIsConfirmed(false); - setSelectedReason(null); - setSubmittedReason(null); - setCustomOption(''); - setIsSkipped(false); - }; - - const reason = { - value: submittedReason, - skip: React.useCallback(() => setIsSkipped(true), [isSkipped]), - isSkipped, - selectOption: React.useCallback((e) => setSelectedReason(e.target.value), []), + return { + clear: React.useCallback(() => { + setSelectedReason(null); + setSubmittedReason(null); + setCustomOption(''); + setIsSkipped(false); + }, []), customOption: { value: customOption, onChange: React.useCallback((e) => setCustomOption(e.target.value), []), }, + isSkipped, + isSubmitted: submittedReason !== null || isSkipped, selected: selectedReason, + selectOption: React.useCallback((e) => setSelectedReason(e.target.value), []), + skip: React.useCallback(() => setIsSkipped(true), [isSkipped]), submit: React.useCallback(() => { if (selectedReason === 'custom') { setSubmittedReason(customOption); @@ -48,8 +48,28 @@ export const modalHooks = ({ closeModal, dispatch }) => { setSubmittedReason(selectedReason); } }, [customOption, selectedReason]), - isSubmitted: submittedReason !== null || isSkipped, + value: submittedReason, }; +}; + +export const modalHooks = ({ closeModal, dispatch }) => { + const [isConfirmed, setIsConfirmed] = module.state.confirmed(false); + + const confirm = React.useCallback(() => setIsConfirmed(true), []); + + const reason = unenrollReasons(); + const close = () => { + closeModal(); + setIsConfirmed(false); + reason.clear(); + }; + + let modalState; + if (isConfirmed) { + modalState = reason.isSubmitted ? modalStates.finished : modalState.reason; + } else { + modalState = modalStates.confirm; + } const closeAndRefresh = React.useCallback(() => { dispatch(thunkActions.app.refreshList()); @@ -62,6 +82,7 @@ export const modalHooks = ({ closeModal, dispatch }) => { reason, closeAndRefresh, close, + modalState, }; }; diff --git a/src/containers/UnenrollConfirmModal/index.jsx b/src/containers/UnenrollConfirmModal/index.jsx index 8696a4b..9c33bee 100644 --- a/src/containers/UnenrollConfirmModal/index.jsx +++ b/src/containers/UnenrollConfirmModal/index.jsx @@ -12,7 +12,7 @@ import ConfirmPane from './components/ConfirmPane'; import ReasonPane from './components/ReasonPane'; import FinishedPane from './components/FinishedPane'; -import hooks from './hooks'; +import hooks, { modalStates } from './hooks'; export const UnenrollConfirmModal = ({ closeModal, @@ -20,26 +20,28 @@ export const UnenrollConfirmModal = ({ }) => { const dispatch = useDispatch(); const { - isConfirmed, confirm, reason, closeAndRefresh, close, + modalState, } = hooks({ dispatch, closeModal }); return (
- {!isConfirmed && } - {isConfirmed && !reason.isSubmitted && } - {isConfirmed && reason.isSubmitted && ( - + {(modalState === modalStates.confirm) && ( + + )} + {(modalState === modalStates.finished) && ( + + )} + {(modalState === modalStates.reason) && ( + )}
diff --git a/src/data/redux/app/reducer.js b/src/data/redux/app/reducer.js index e2d6cbb..fc8b7e2 100644 --- a/src/data/redux/app/reducer.js +++ b/src/data/redux/app/reducer.js @@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit'; const initialState = { enrollments: [], + courseData: {}, entitlements: [], }; @@ -11,7 +12,17 @@ const app = createSlice({ name: 'app', initialState, reducers: { - loadEnrollments: (state, { payload }) => ({ ...state, enrollments: payload }), + loadEnrollments: (state, { payload }) => ({ + ...state, + enrollments: payload.map(curr => curr.courseRun.courseNumber), + courseData: payload.reduce( + (obj, curr) => ({ + ...obj, + [curr.courseRun.courseNumber]: curr, + }), + {}, + ), + }), loadEntitlements: (state, { payload }) => ({ ...state, entitlements: payload }), }, }); diff --git a/src/data/redux/app/selectors.js b/src/data/redux/app/selectors.js index 2c57c8f..e8eb745 100644 --- a/src/data/redux/app/selectors.js +++ b/src/data/redux/app/selectors.js @@ -12,8 +12,17 @@ const mkSimpleSelector = (cb) => createSelector([module.appSelector], cb); export const simpleSelectors = { enrollments: mkSimpleSelector(app => app.enrollments), entitlements: mkSimpleSelector(app => app.entitlements), + courseData: mkSimpleSelector(app => app.courseData), }; +export const courseCardData = (state, courseNumber) => ( + module.simpleSelectors.courseData(state)[courseNumber] +); + +export const cardSelector = (sel, courseNumber) => state => sel(state, courseNumber); + export default StrictDict({ ...simpleSelectors, + courseCardData, + cardSelector, }); diff --git a/src/data/redux/cardData/index.js b/src/data/redux/cardData/index.js new file mode 100644 index 0000000..9d6493d --- /dev/null +++ b/src/data/redux/cardData/index.js @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { default as selectors } from './selectors'; diff --git a/src/data/redux/cardData/selectors.js b/src/data/redux/cardData/selectors.js new file mode 100644 index 0000000..e1450b0 --- /dev/null +++ b/src/data/redux/cardData/selectors.js @@ -0,0 +1,67 @@ +import { keyStore, StrictDict } from 'utils'; + +import app from 'data/redux/app/selectors'; +// import * as module from './selectors'; + +const mkCardSelector = (sel) => (state, courseNumber) => ( + sel(app.courseCardData(state, courseNumber)) +); + +export const fieldSelectors = { + courseTitle: data => data.course.title, + courseBannerUrl: data => data.course.bannerUrl, + courseRunAccessExpirationDate: data => data.courseRun.accessExpirationDate, + courseWebsite: data => data.course.website, + providerName: data => data.provider?.name, + isVerified: data => data.enrollment.isVerified, + isAudit: data => data.enrollment.isAudit, + isAuditAccessExpired: data => data.enrollment.isAuditAccessExpired, + isCourseRunPending: data => data.courseRun.isPending, + isCourseRunStarted: data => data.courseRun.isStarted, + isCourseRunFinished: data => data.courseRun.isFinished, + isEmailEnabled: data => data.enrollment.isEmailEnabled, + canUpgrade: data => data.enrollment.canUpgrade, + isRestricted: data => data.certificates.isRestricted, + isPassing: data => data.grades.isPassing, + minPassingGrade: data => data.courseRun.minPassingGrade, + isCertDownloadable: data => data.certificates.isDownloadable, + certDownloadUrl: data => data.certificates.downloadUrls.download, + certPreviewUrl: data => data.certificates.downloadUrls.preview, + isCertEarnedButUnavailable: ({ certificates: { isEarned, isAvailable } }) => ( + isEarned && !isAvailable + ), + certAvailableDate: data => data.certificates.availableDate, + isEntitlement: data => data.entitlements.isEntitlement, + isEntitlementFulfilled: data => data.entitlements.isFulfilled, + isEntitlementExpired: data => data.entitlements.isExpired, + canChangeEntitlementSession: data => data.entitlements.canChange, + entitlementSessions: data => data.entitlements.availableSessions, + relatedPrograms: data => data.relatedPrograms, +}; +fieldSelectors.isCourseRunActive = data => ( + fieldSelectors.isCourseRunStarted(data) && !fieldSelectors.isCourseRunFinished(data) +); + +export const programs = StrictDict({ + estimatedNumberOfWeeks: data => data.estimatedNumberOfWeeks, + numberOfCourses: data => data.numberOfCourses, + programType: data => data.programType, + programTypeUrl: data => data.programTypeUrl, + provider: data => data.provider, + title: data => data.title, +}); + +export const fieldKeys = keyStore(fieldSelectors); + +export const cardSelectors = Object.keys(fieldSelectors).reduce( + (obj, key) => ({ ...obj, [key]: mkCardSelector(fieldSelectors[key]) }), + {}, +); + +export const cardSelector = (sel, courseNumber) => (state) => sel(state, courseNumber); + +export default StrictDict({ + cardSelector, + ...cardSelectors, + programs, +}); diff --git a/src/data/redux/index.js b/src/data/redux/index.js index 24e079e..09dc5b9 100644 --- a/src/data/redux/index.js +++ b/src/data/redux/index.js @@ -4,16 +4,21 @@ import { StrictDict } from 'utils'; import * as app from './app'; import * as requests from './requests'; +import * as cardData from './cardData'; export { default as thunkActions } from './thunkActions'; const modules = { app, requests, + cardData, }; const moduleProps = (propName) => Object.keys(modules).reduce( - (obj, moduleKey) => ({ ...obj, [moduleKey]: modules[moduleKey][propName] }), + (obj, moduleKey) => { + const value = modules[moduleKey][propName]; + return value ? { ...obj, [moduleKey]: value } : obj; + }, {}, ); diff --git a/src/data/services/lms/shapes.js b/src/data/services/lms/shapes.js index 0344575..5df0455 100644 --- a/src/data/services/lms/shapes.js +++ b/src/data/services/lms/shapes.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import { StrictDict } from 'utils'; +import { StrictDict, keyStore } from 'utils'; export const shapes = StrictDict({ course: PropTypes.shape({ @@ -21,10 +21,6 @@ export const shapes = StrictDict({ isStarted: PropTypes.bool, minPassingGrade: PropTypes.number, }), - credit: PropTypes.shape({ - isPurchased: PropTypes.bool, - requestStatus: PropTypes.string, - }), certificates: PropTypes.shape({ availableDate: PropTypes.string, downloadUrls: PropTypes.shape({ @@ -43,7 +39,12 @@ export const shapes = StrictDict({ isVerified: PropTypes.bool, }), entitlement: PropTypes.shape({ - isEntitlement: PropTypes.bool, + isEntitlement: PropTypes.bool.isRequired, + availableSessions: PropTypes.shape({ + startDate: PropTypes.string, + endDate: PropTypes.string, + courseNumber: PropTypes.string, + }), canChange: PropTypes.bool, isFulfilled: PropTypes.bool, isRefundable: PropTypes.bool, @@ -63,7 +64,7 @@ export const shapes = StrictDict({ programType: PropTypes.string, programTypeUrl: PropTypes.string, numberOfCourses: PropTypes.number, - estimatedDuration: PropTypes.string, + estimatedNumberOfWeeks: PropTypes.number, }), }); @@ -71,7 +72,6 @@ shapes.courseRunCardData = PropTypes.shape({ course: shapes.course, provider: shapes.provider, courseRun: shapes.courseRun, - credit: shapes.credit, certificates: shapes.certificates, enrollment: shapes.enrollment, entitlement: shapes.entitlement, @@ -79,4 +79,16 @@ shapes.courseRunCardData = PropTypes.shape({ relatedPrograms: PropTypes.arrayOf(shapes.programCard), }); +export const keys = StrictDict({ + cardData: keyStore(shapes.courseRunCardData), + course: keyStore(shapes.course), + provider: keyStore(shapes.provider), + courseRun: keyStore(shapes.courseRun), + certificates: keyStore(shapes.certificates), + enrollment: keyStore(shapes.enrollment), + entitlement: keyStore(shapes.entitlement), + grades: keyStore(shapes.grades), + program: keyStore(shapes.programCard), +}); + export default shapes; diff --git a/src/hooks.js b/src/hooks.js index 32d7a0e..9caad1e 100644 --- a/src/hooks.js +++ b/src/hooks.js @@ -1,3 +1,11 @@ +import { useSelector } from 'react-redux'; + +import { selectors } from 'data/redux'; + +const { cardData } = selectors; + +export const getCardValue = (courseNumber) => (sel) => useSelector(cardData.cardSelector(sel, courseNumber)); + export const nullMethod = () => ({}); export default {