-
Notifications
You must be signed in to change notification settings - Fork 3
Product page: list prices and actual prices #3346
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
base: main
Are you sure you want to change the base?
Changes from all commits
1fba178
cfb16c7
68a1860
587760e
b980ee7
cfa88c4
afbd104
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,14 @@ | |
| textDecoration: "underline", | ||
| }) | ||
|
|
||
| const SecondaryUnderlinedLink = styled(UnderlinedLink)(({ theme }) => ({ | ||
| ...theme.typography.body3, | ||
| color: theme.custom.colors.silverGrayDark, | ||
| [theme.breakpoints.down("sm")]: { | ||
| ...theme.typography.body4, | ||
| }, | ||
| })) | ||
|
|
||
| const InfoRow = styled.div(({ theme }) => ({ | ||
| width: "100%", | ||
| display: "flex", | ||
|
|
@@ -378,6 +386,142 @@ | |
| color: theme.custom.colors.silverGrayDark, | ||
| })) | ||
|
|
||
| const ProgramPaySection = styled.div(({ theme }) => ({ | ||
| display: "flex", | ||
| flexDirection: "column", | ||
| alignItems: "flex-start", | ||
| gap: "4px", | ||
| width: "346px", | ||
| alignSelf: "stretch", | ||
| flex: "none", | ||
| color: theme.custom.colors.darkGray2, | ||
| })) | ||
|
|
||
| const ProgramPayLabel = styled.span(({ theme }) => ({ | ||
| ...theme.typography.body4, | ||
| fontSize: "12px", | ||
| lineHeight: "16px", | ||
| fontWeight: theme.typography.fontWeightMedium, | ||
|
Contributor
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. This should just be We should only need custom typography very rarely. (I don't see the letterspacing thing in figma, but i may have missed it.) |
||
| color: theme.custom.colors.silverGrayDark, | ||
| textTransform: "uppercase", | ||
| letterSpacing: "0.04em", | ||
| width: "100%", | ||
| alignSelf: "stretch", | ||
| })) | ||
|
|
||
| const ProgramPayContent = styled.div(({ theme }) => ({ | ||
| display: "flex", | ||
| flexDirection: "column", | ||
| alignItems: "flex-start", | ||
| gap: "12px", | ||
| width: "100%", | ||
| maxWidth: "320px", | ||
| [theme.breakpoints.down("sm")]: { | ||
| maxWidth: "100%", | ||
| }, | ||
| })) | ||
|
|
||
| const ProgramPriceLine = styled.div(({ theme }) => ({ | ||
| display: "flex", | ||
| alignItems: "flex-end", | ||
| gap: "4px", | ||
| flexWrap: "wrap", | ||
| color: theme.custom.colors.darkGray2, | ||
| })) | ||
|
|
||
| const ProgramPriceAmount = styled.span(({ theme }) => ({ | ||
| ...theme.typography.h3, | ||
| fontWeight: theme.typography.fontWeightBold, | ||
| lineHeight: "36px", | ||
| })) | ||
|
|
||
| const ProgramPriceSuffix = styled.span(({ theme }) => ({ | ||
| ...theme.typography.subtitle1, | ||
| fontWeight: theme.typography.fontWeightMedium, | ||
| fontSize: "18px", | ||
| lineHeight: "26px", | ||
| color: theme.custom.colors.silverGrayDark, | ||
| })) | ||
|
|
||
| const ProgramDiscountBlock = styled.div(({ theme }) => ({ | ||
| display: "flex", | ||
| flexDirection: "column", | ||
| justifyContent: "center", | ||
| alignItems: "flex-start", | ||
| gap: "8px", | ||
| color: theme.custom.colors.darkGray2, | ||
| })) | ||
|
|
||
| const ProgramSavingsText = styled.span(({ theme }) => ({ | ||
| ...theme.typography.subtitle2, | ||
| color: "#008000", | ||
| fontSize: "16px", | ||
| lineHeight: "24px", | ||
| fontWeight: theme.typography.fontWeightBold, | ||
| })) | ||
|
|
||
| const ProgramListPriceText = styled.span(({ theme }) => ({ | ||
| ...theme.typography.body2, | ||
| fontSize: "14px", | ||
| lineHeight: "18px", | ||
| fontWeight: theme.typography.fontWeightRegular, | ||
|
Comment on lines
+465
to
+467
Contributor
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. These 3 overrides are unnecessary. 14/18/regular is normal |
||
| color: theme.custom.colors.silverGrayDark, | ||
| })) | ||
|
|
||
| const ProgramListPriceAmount = styled.span(({ theme }) => ({ | ||
| fontStyle: "normal", | ||
| fontSize: "14px", | ||
| lineHeight: "18px", | ||
|
Comment on lines
+472
to
+474
Contributor
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. |
||
| letterSpacing: "0", | ||
| verticalAlign: "bottom", | ||
| textDecoration: "line-through", | ||
| fontWeight: theme.typography.fontWeightMedium, | ||
| })) | ||
|
|
||
| const ProgramPriceDivider = styled.div(({ theme }) => ({ | ||
| width: "100%", | ||
| maxWidth: "346px", | ||
| borderTop: `1px solid ${theme.custom.colors.lightGray2}`, | ||
| marginBottom: "20px", | ||
| flex: "none", | ||
| alignSelf: "stretch", | ||
| })) | ||
|
|
||
| const ProgramStartForFreeBox = styled.div((_theme) => ({ | ||
| display: "flex", | ||
| alignItems: "center", | ||
| gap: "8px", | ||
| padding: "8px 16px", | ||
| borderRadius: "8px", | ||
| background: | ||
| "linear-gradient(0deg, rgba(255, 255, 255, 0.94), rgba(255, 255, 255, 0.94)), #004D1A", | ||
| })) | ||
|
|
||
| const ProgramStartForFreeIcon = styled.svg(() => ({ | ||
| width: "24px", | ||
| height: "24px", | ||
| flexShrink: 0, | ||
| path: { | ||
| fill: "#008000", | ||
| }, | ||
| })) | ||
|
|
||
| const ProgramStartForFreeTextContainer = styled.span(({ theme }) => ({ | ||
| display: "flex", | ||
| alignItems: "center", | ||
| gap: "4px", | ||
| ...theme.typography.body2, | ||
| })) | ||
|
|
||
| const ProgramStartForFreeTextStrong = styled.span(({ theme }) => ({ | ||
| color: "#008000", | ||
| fontWeight: theme.typography.fontWeightMedium, | ||
| })) | ||
|
|
||
| const ProgramStartForFreeTextRegular = styled.span(({ theme }) => ({ | ||
| color: theme.custom.colors.darkGray2, | ||
| })) | ||
|
|
||
| const CertificateBoxRoot = styled.div(({ theme }) => ({ | ||
| width: "100%", | ||
| backgroundColor: theme.custom.colors.lightGray1, | ||
|
|
@@ -657,15 +801,16 @@ | |
| program: V2ProgramDetail | ||
| } & HTMLAttributes<HTMLDivElement> | ||
|
|
||
| const getTotalRequiredCourses = (program: V2ProgramDetail) => { | ||
| const parsedReqs = parseReqTree(program.req_tree) | ||
| return parsedReqs.reduce((sum, req) => sum + req.requiredCount, 0) | ||
| } | ||
|
|
||
| const RequirementsRow: React.FC<ProgramInfoRowProps> = ({ | ||
| program, | ||
| ...others | ||
| }) => { | ||
| const parsedReqs = parseReqTree(program.req_tree) | ||
| const totalRequired = parsedReqs.reduce( | ||
| (sum, req) => sum + req.requiredCount, | ||
| 0, | ||
| ) | ||
| const totalRequired = getTotalRequiredCourses(program) | ||
| if (totalRequired === 0) return null | ||
|
|
||
| // Always say "Courses" here. Whether a child program should be labeled | ||
|
|
@@ -757,44 +902,9 @@ | |
| ) | ||
| } | ||
|
|
||
| const PROGRAM_CERT_INFO_HREF = | ||
| "https://mitxonline.zendesk.com/hc/en-us/articles/28158506908699-What-is-the-Certificate-Track-What-are-Course-and-Program-Certificates" | ||
|
|
||
| const ProgramCertificateBox: React.FC<{ program: V2ProgramDetail }> = ({ | ||
| program, | ||
| }) => { | ||
| const price = program.products[0]?.price | ||
| if (!price) return null | ||
| return ( | ||
| <CertificateBoxRoot> | ||
| <InfoRowInner flexWrap="nowrap"> | ||
| <span> | ||
| <UnderlinedLink | ||
| href={PROGRAM_CERT_INFO_HREF} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| color="black" | ||
| > | ||
| <InfoLabel>Earn a certificate</InfoLabel> | ||
| </UnderlinedLink> | ||
| : {formatPrice(price, { avoidCents: true })} | ||
| </span> | ||
| </InfoRowInner> | ||
| {program.page.financial_assistance_form_url ? ( | ||
| <UnderlinedLink | ||
| color="black" | ||
| href={mitxonlineLegacyUrl(program.page.financial_assistance_form_url)} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| style={{ minWidth: "fit-content" }} | ||
| > | ||
| Financial assistance available | ||
| </UnderlinedLink> | ||
| ) : null} | ||
| </CertificateBoxRoot> | ||
| ) | ||
| } | ||
|
|
||
| type ProgramPriceRowProps = HTMLAttributes<HTMLDivElement> & { | ||
| program: V2ProgramDetail | ||
| } | ||
|
|
@@ -805,33 +915,113 @@ | |
| const enrollmentType = getEnrollmentType(program.enrollment_modes) | ||
| if (enrollmentType === "none") return null | ||
|
|
||
| const paidPrice = | ||
| enrollmentType === "paid" && program.products[0]?.price ? ( | ||
| <> | ||
| {formatPrice(program.products[0].price, { avoidCents: true })}{" "} | ||
| <GrayText>(includes {program.certificate_type})</GrayText> | ||
| </> | ||
| ) : null | ||
| const currentPrice = program.products[0]?.price | ||
| const listPrice = program.page.list_price | ||
|
|
||
| const currentAmount = toNumericPrice(currentPrice) | ||
| const listAmount = toNumericPrice(listPrice) | ||
| const hasSavings = | ||
| currentAmount !== null && listAmount !== null && listAmount > currentAmount | ||
| const savingsAmount = hasSavings ? listAmount - currentAmount : null | ||
|
|
||
| const totalRequired = getTotalRequiredCourses(program) | ||
|
|
||
| const paidSection = currentPrice ? ( | ||
| <ProgramPaySection> | ||
| <ProgramPayLabel>Price</ProgramPayLabel> | ||
| <ProgramPayContent> | ||
| <ProgramPriceLine> | ||
| <ProgramPriceAmount> | ||
| {formatPrice(currentPrice, { avoidCents: true })} | ||
| </ProgramPriceAmount> | ||
| <ProgramPriceSuffix>/ full program</ProgramPriceSuffix> | ||
| </ProgramPriceLine> | ||
| {hasSavings && savingsAmount !== null && listAmount !== null ? ( | ||
| <ProgramDiscountBlock> | ||
| <ProgramSavingsText> | ||
| Save {formatPrice(savingsAmount, { avoidCents: true })} | ||
| </ProgramSavingsText> | ||
| <ProgramListPriceText> | ||
| <ProgramListPriceAmount> | ||
| {formatPrice(listAmount, { avoidCents: true })} | ||
| </ProgramListPriceAmount>{" "} | ||
| total for {totalRequired} {pluralize("course", totalRequired)}{" "} | ||
| purchased separately | ||
| </ProgramListPriceText> | ||
| </ProgramDiscountBlock> | ||
| ) : null} | ||
| {program.page.financial_assistance_form_url ? ( | ||
| <SecondaryUnderlinedLink | ||
| href={mitxonlineLegacyUrl( | ||
| program.page.financial_assistance_form_url, | ||
| )} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| style={{ minWidth: "fit-content" }} | ||
| > | ||
| Financial assistance available | ||
| </SecondaryUnderlinedLink> | ||
| ) : null} | ||
| {enrollmentType === "both" ? ( | ||
| <ProgramStartForFreeBox> | ||
| <ProgramStartForFreeIcon | ||
| width="24" | ||
| height="24" | ||
| viewBox="0 0 22 19" | ||
| fill="none" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| aria-hidden="true" | ||
| > | ||
| <path d="M14 0C16.2091 0 18 1.79086 18 4C18 4.72903 17.8049 5.41251 17.4642 6.00111L22 6V7.99999H20V18C20 18.5523 19.5523 19 19 19H3C2.44772 19 2 18.5523 2 18V7.99999H0V6L4.53577 6.00111C4.19504 5.41251 4 4.72903 4 4C4 1.79086 5.79086 0 8 0C9.19522 0 10.268 0.52421 11.0009 1.35526C11.732 0.52421 12.8048 0 14 0ZM10 7.99999H4V17H10V7.99999ZM18 7.99999H12V17H18V7.99999ZM8 2C6.89543 2 6 2.89543 6 4C6 5.05436 6.81588 5.91816 7.85074 5.99451L8 6H10V4C10 2.99835 9.26372 2.16869 8.30278 2.02277L8.14927 2.00548L8 2ZM14 2C12.9456 2 12.0818 2.81588 12.0055 3.85074L12 4V6H14C15.0543 6 15.9181 5.18412 15.9945 4.14926L16 4C16 2.89543 15.1046 2 14 2Z" /> | ||
| </ProgramStartForFreeIcon> | ||
| <ProgramStartForFreeTextContainer> | ||
| <ProgramStartForFreeTextStrong> | ||
| Start for free | ||
| </ProgramStartForFreeTextStrong> | ||
| <ProgramStartForFreeTextRegular> | ||
| or upgrade to certificate | ||
| </ProgramStartForFreeTextRegular> | ||
| </ProgramStartForFreeTextContainer> | ||
| </ProgramStartForFreeBox> | ||
| ) : null} | ||
| </ProgramPayContent> | ||
| </ProgramPaySection> | ||
| ) : ( | ||
| <InfoLabelValue label="Price" value="Price unavailable" /> | ||
| ) | ||
|
|
||
| return ( | ||
| <InfoRow {...others}> | ||
| <InfoRowIcon> | ||
| <RiPriceTag3Line aria-hidden="true" /> | ||
| </InfoRowIcon> | ||
| <InfoRowInner> | ||
| {enrollmentType === "paid" ? ( | ||
| <InfoLabelValue label="Price" value={paidPrice} /> | ||
| <Stack {...others} gap="0px" width="100%"> | ||
| {enrollmentType === "paid" || enrollmentType === "both" ? ( | ||
| <ProgramPriceDivider /> | ||
| ) : null} | ||
| <InfoRow> | ||
| {enrollmentType === "paid" || enrollmentType === "both" ? ( | ||
| paidSection | ||
| ) : ( | ||
| <InfoLabelValue label="Price" value="Free to Learn" /> | ||
| <> | ||
| <InfoRowIcon> | ||
| <RiPriceTag3Line aria-hidden="true" /> | ||
| </InfoRowIcon> | ||
| <InfoRowInner> | ||
| <InfoLabelValue label="Price" value="Free to Learn" /> | ||
| </InfoRowInner> | ||
| </> | ||
| )} | ||
| {enrollmentType === "both" ? ( | ||
| <ProgramCertificateBox program={program} /> | ||
| ) : null} | ||
| </InfoRowInner> | ||
| </InfoRow> | ||
| </InfoRow> | ||
| </Stack> | ||
| ) | ||
| } | ||
|
|
||
| const toNumericPrice = (value: unknown): number | null => { | ||
| if (typeof value === "number" && Number.isFinite(value)) return value | ||
| if (typeof value === "string") { | ||
| const parsed = Number.parseFloat(value) | ||
| if (Number.isFinite(parsed)) return parsed | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| const ProgramSummary: React.FC<{ | ||
| program: V2ProgramDetail | ||
| /** | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should just be
theme.typography.subtitle3, I thinkWe should only need custom typography very rarely. (I don't see the letterspacing thing in figma, but i may have missed it.)