From 1fba178a6fb8cb444899f576e66c2a8f8ff528db Mon Sep 17 00:00:00 2001 From: Anna Date: Fri, 15 May 2026 08:59:12 -0400 Subject: [PATCH 1/7] Program Savings Price update --- .../app-pages/ProductPages/ProductSummary.tsx | 168 +++++++++++++++--- 1 file changed, 146 insertions(+), 22 deletions(-) diff --git a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx index 7e4e4a594c..901749f5dd 100644 --- a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx @@ -378,6 +378,83 @@ const GrayText = styled.span(({ theme }) => ({ color: theme.custom.colors.silverGrayDark, })) +const ProgramPaySection = styled.div(({ theme }) => ({ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + gap: "4px", + width: "100%", + maxWidth: "346px", + color: theme.custom.colors.darkGray2, +})) + +const ProgramPayLabel = styled.span(({ theme }) => ({ + ...theme.typography.body4, + fontWeight: theme.typography.fontWeightMedium, + color: theme.custom.colors.silverGrayDark, + textTransform: "uppercase", + letterSpacing: "0.04em", +})) + +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, + color: theme.custom.colors.silverGrayDark, +})) + +const ProgramDiscountBlock = styled.div(({ theme }) => ({ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + gap: "4px", + color: theme.custom.colors.darkGray2, +})) + +const ProgramSavingsText = styled.span(({ theme }) => ({ + ...theme.typography.subtitle2, + color: theme.custom.colors.green, +})) + +const ProgramListPriceText = styled.span(({ theme }) => ({ + ...theme.typography.body2, + color: theme.custom.colors.silverGrayDark, + textDecoration: "line-through", +})) + +const ProgramPriceDivider = styled.div(({ theme }) => ({ + width: "100%", + maxWidth: "346px", + borderTop: `1px solid ${theme.custom.colors.lightGray2}`, + flex: "none", + alignSelf: "stretch", +})) + const CertificateBoxRoot = styled.div(({ theme }) => ({ width: "100%", backgroundColor: theme.custom.colors.lightGray1, @@ -796,42 +873,89 @@ const ProgramCertificateBox: React.FC<{ program: V2ProgramDetail }> = ({ } type ProgramPriceRowProps = HTMLAttributes & { - program: V2ProgramDetail + program: V2ProgramDetail & { + // Temporary local extension while list_price rolls into upstream API typings. + list_price?: number | string | null + } } const ProgramPriceRow: React.FC = ({ program, ...others }) => { 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 })}{" "} - (includes {program.certificate_type}) - - ) : null + // if (enrollmentType === "none") return null + + const currentPrice = program.products[0]?.price + const listPrice = program.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 paidSection = + enrollmentType === "paid" && currentPrice ? ( + + Price + + + + {formatPrice(currentPrice, { avoidCents: true })} + + / full program + + {hasSavings && savingsAmount !== null && listAmount !== null ? ( + + + Save {formatPrice(savingsAmount, { avoidCents: true })} + + + {formatPrice(listAmount, { avoidCents: true })} total for + courses purchased separately + + + ) : null} + (includes {program.certificate_type}) + + + ) : ( + + ) return ( - - - - + + {enrollmentType === "paid" ? : null} + {enrollmentType === "paid" ? ( - + paidSection ) : ( - + <> + + + + + {enrollmentType === "both" ? ( + + ) : null} + + )} - {enrollmentType === "both" ? ( - - ) : null} - - + + ) } +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 /** From cfb16c7e78f84ef0bb02a158d59ae18edb1a8819 Mon Sep 17 00:00:00 2001 From: Anna Date: Fri, 15 May 2026 10:16:08 -0400 Subject: [PATCH 2/7] Remove certificate text --- frontends/main/src/app-pages/ProductPages/ProductSummary.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx index 901749f5dd..b87bba6321 100644 --- a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx @@ -886,7 +886,7 @@ const ProgramPriceRow: React.FC = ({ // if (enrollmentType === "none") return null const currentPrice = program.products[0]?.price - const listPrice = program.list_price + const listPrice = program.page.list_price const currentAmount = toNumericPrice(currentPrice) const listAmount = toNumericPrice(listPrice) @@ -916,7 +916,6 @@ const ProgramPriceRow: React.FC = ({ ) : null} - (includes {program.certificate_type}) ) : ( From 68a18606af785f9a82ca612b586a0658bb74bd21 Mon Sep 17 00:00:00 2001 From: Anna Date: Fri, 15 May 2026 10:22:19 -0400 Subject: [PATCH 3/7] fix --- .../main/src/app-pages/ProductPages/ProductSummary.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx index b87bba6321..382ed53340 100644 --- a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx @@ -873,17 +873,14 @@ const ProgramCertificateBox: React.FC<{ program: V2ProgramDetail }> = ({ } type ProgramPriceRowProps = HTMLAttributes & { - program: V2ProgramDetail & { - // Temporary local extension while list_price rolls into upstream API typings. - list_price?: number | string | null - } + program: V2ProgramDetail } const ProgramPriceRow: React.FC = ({ program, ...others }) => { const enrollmentType = getEnrollmentType(program.enrollment_modes) - // if (enrollmentType === "none") return null + if (enrollmentType === "none") return null const currentPrice = program.products[0]?.price const listPrice = program.page.list_price From 587760e11d77c929429de77309e009ad55e51c6c Mon Sep 17 00:00:00 2001 From: Anna Date: Fri, 15 May 2026 11:11:57 -0400 Subject: [PATCH 4/7] small changes --- .../app-pages/ProductPages/ProductSummary.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx index 382ed53340..640b4e806a 100644 --- a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx @@ -390,6 +390,7 @@ const ProgramPaySection = styled.div(({ theme }) => ({ const ProgramPayLabel = styled.span(({ theme }) => ({ ...theme.typography.body4, + fontSize: "12px", fontWeight: theme.typography.fontWeightMedium, color: theme.custom.colors.silverGrayDark, textTransform: "uppercase", @@ -439,6 +440,7 @@ const ProgramDiscountBlock = styled.div(({ theme }) => ({ const ProgramSavingsText = styled.span(({ theme }) => ({ ...theme.typography.subtitle2, color: theme.custom.colors.green, + fontSize: "16px", })) const ProgramListPriceText = styled.span(({ theme }) => ({ @@ -451,6 +453,7 @@ const ProgramPriceDivider = styled.div(({ theme }) => ({ width: "100%", maxWidth: "346px", borderTop: `1px solid ${theme.custom.colors.lightGray2}`, + marginBottom: "20px", flex: "none", alignSelf: "stretch", })) @@ -734,15 +737,16 @@ type ProgramInfoRowProps = { program: V2ProgramDetail } & HTMLAttributes +const getTotalRequiredCourses = (program: V2ProgramDetail) => { + const parsedReqs = parseReqTree(program.req_tree) + return parsedReqs.reduce((sum, req) => sum + req.requiredCount, 0) +} + const RequirementsRow: React.FC = ({ 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 @@ -891,6 +895,8 @@ const ProgramPriceRow: React.FC = ({ currentAmount !== null && listAmount !== null && listAmount > currentAmount const savingsAmount = hasSavings ? listAmount - currentAmount : null + const totalRequired = getTotalRequiredCourses(program) + const paidSection = enrollmentType === "paid" && currentPrice ? ( @@ -908,8 +914,9 @@ const ProgramPriceRow: React.FC = ({ Save {formatPrice(savingsAmount, { avoidCents: true })} - {formatPrice(listAmount, { avoidCents: true })} total for - courses purchased separately + {formatPrice(listAmount, { avoidCents: true })} total for{" "} + {totalRequired} {pluralize("course", totalRequired)} purchased + separately ) : null} @@ -920,7 +927,7 @@ const ProgramPriceRow: React.FC = ({ ) return ( - + {enrollmentType === "paid" ? : null} {enrollmentType === "paid" ? ( From b980ee7d760f3c7be0fed1411b2722559fdc86cd Mon Sep 17 00:00:00 2001 From: Anna Date: Fri, 15 May 2026 12:11:09 -0400 Subject: [PATCH 5/7] adding the Start For Free --- frontends/main/public/shopping-bag-icon.svg | 6 + .../app-pages/ProductPages/ProductSummary.tsx | 141 ++++++++++++------ 2 files changed, 101 insertions(+), 46 deletions(-) create mode 100644 frontends/main/public/shopping-bag-icon.svg diff --git a/frontends/main/public/shopping-bag-icon.svg b/frontends/main/public/shopping-bag-icon.svg new file mode 100644 index 0000000000..73cf9a189b --- /dev/null +++ b/frontends/main/public/shopping-bag-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx index 640b4e806a..3e5547f271 100644 --- a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx @@ -383,18 +383,22 @@ const ProgramPaySection = styled.div(({ theme }) => ({ flexDirection: "column", alignItems: "flex-start", gap: "4px", - width: "100%", - maxWidth: "346px", + 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, color: theme.custom.colors.silverGrayDark, textTransform: "uppercase", letterSpacing: "0.04em", + width: "100%", + alignSelf: "stretch", })) const ProgramPayContent = styled.div(({ theme }) => ({ @@ -426,29 +430,40 @@ const ProgramPriceAmount = styled.span(({ theme }) => ({ 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: "4px", + gap: "8px", color: theme.custom.colors.darkGray2, })) const ProgramSavingsText = styled.span(({ theme }) => ({ ...theme.typography.subtitle2, - color: theme.custom.colors.green, + 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.fontWeightMedium, color: theme.custom.colors.silverGrayDark, - textDecoration: "line-through", })) +const ProgramListPriceAmount = styled.span({ + textDecoration: "line-through", +}) + const ProgramPriceDivider = styled.div(({ theme }) => ({ width: "100%", maxWidth: "346px", @@ -458,6 +473,41 @@ const ProgramPriceDivider = styled.div(({ theme }) => ({ 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, @@ -841,40 +891,6 @@ const ProgramPaceRow: React.FC< 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 ( - - - - - Earn a certificate - - : {formatPrice(price, { avoidCents: true })} - - - {program.page.financial_assistance_form_url ? ( - - Financial assistance available - - ) : null} - - ) -} type ProgramPriceRowProps = HTMLAttributes & { program: V2ProgramDetail @@ -898,7 +914,7 @@ const ProgramPriceRow: React.FC = ({ const totalRequired = getTotalRequiredCourses(program) const paidSection = - enrollmentType === "paid" && currentPrice ? ( + currentPrice ? ( Price @@ -914,12 +930,48 @@ const ProgramPriceRow: React.FC = ({ Save {formatPrice(savingsAmount, { avoidCents: true })} - {formatPrice(listAmount, { avoidCents: true })} total for{" "} + + {formatPrice(listAmount, { avoidCents: true })} + {" "} + total for{" "} {totalRequired} {pluralize("course", totalRequired)} purchased separately ) : null} + {enrollmentType === "both" ? ( + + + + + Start for free + + + or upgrade to certificate + + + + ) : null} + {program.page.financial_assistance_form_url ? ( + + Financial assistance available + + ) : null} ) : ( @@ -928,9 +980,9 @@ const ProgramPriceRow: React.FC = ({ return ( - {enrollmentType === "paid" ? : null} + {enrollmentType === "paid" || enrollmentType === "both" ? : null} - {enrollmentType === "paid" ? ( + {enrollmentType === "paid" || enrollmentType === "both" ? ( paidSection ) : ( <> @@ -939,9 +991,6 @@ const ProgramPriceRow: React.FC = ({ - {enrollmentType === "both" ? ( - - ) : null} )} From cfa88c4af3615eb8d661d0e3ee252fab513a55b2 Mon Sep 17 00:00:00 2001 From: Anna Date: Fri, 15 May 2026 13:34:42 -0400 Subject: [PATCH 6/7] addressing comments UI --- frontends/main/public/shopping-bag-icon.svg | 6 --- .../app-pages/ProductPages/ProductSummary.tsx | 41 ++++++++++++------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/frontends/main/public/shopping-bag-icon.svg b/frontends/main/public/shopping-bag-icon.svg index 73cf9a189b..e69de29bb2 100644 --- a/frontends/main/public/shopping-bag-icon.svg +++ b/frontends/main/public/shopping-bag-icon.svg @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx index 3e5547f271..1c19e1c4f6 100644 --- a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx @@ -38,6 +38,14 @@ const UnderlinedLink = styled(ResponsiveLink)({ 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", @@ -456,13 +464,19 @@ const ProgramListPriceText = styled.span(({ theme }) => ({ ...theme.typography.body2, fontSize: "14px", lineHeight: "18px", - fontWeight: theme.typography.fontWeightMedium, + fontWeight: theme.typography.fontWeightRegular, color: theme.custom.colors.silverGrayDark, })) -const ProgramListPriceAmount = styled.span({ +const ProgramListPriceAmount = styled.span(({ theme }) => ({ + fontStyle: "normal", + fontSize: "14px", + lineHeight: "18px", + letterSpacing: "0", + verticalAlign: "bottom", textDecoration: "line-through", -}) + fontWeight: theme.typography.fontWeightMedium, +})) const ProgramPriceDivider = styled.div(({ theme }) => ({ width: "100%", @@ -939,6 +953,16 @@ const ProgramPriceRow: React.FC = ({ ) : null} + {program.page.financial_assistance_form_url ? ( + + Financial assistance available + + ) : null} {enrollmentType === "both" ? ( = ({ ) : null} - {program.page.financial_assistance_form_url ? ( - - Financial assistance available - - ) : null} ) : ( From afbd1043697ca868cc0de89a0bf3bcad90a3a6d0 Mon Sep 17 00:00:00 2001 From: Anna Date: Fri, 15 May 2026 14:31:39 -0400 Subject: [PATCH 7/7] format --- .../app-pages/ProductPages/ProductSummary.tsx | 131 +++++++++--------- 1 file changed, 66 insertions(+), 65 deletions(-) diff --git a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx index 1c19e1c4f6..f37c034bbc 100644 --- a/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProductSummary.tsx @@ -501,7 +501,7 @@ const ProgramStartForFreeIcon = styled.svg(() => ({ width: "24px", height: "24px", flexShrink: 0, - "path": { + path: { fill: "#008000", }, })) @@ -905,7 +905,6 @@ const ProgramPaceRow: React.FC< 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" - type ProgramPriceRowProps = HTMLAttributes & { program: V2ProgramDetail } @@ -927,73 +926,75 @@ const ProgramPriceRow: React.FC = ({ const totalRequired = getTotalRequiredCourses(program) - const paidSection = - currentPrice ? ( - - Price - - - - {formatPrice(currentPrice, { avoidCents: true })} - - / full program - - {hasSavings && savingsAmount !== null && listAmount !== null ? ( - - - Save {formatPrice(savingsAmount, { avoidCents: true })} - - - - {formatPrice(listAmount, { avoidCents: true })} - {" "} - total for{" "} - {totalRequired} {pluralize("course", totalRequired)} purchased - separately - - - ) : null} - {program.page.financial_assistance_form_url ? ( - + Price + + + + {formatPrice(currentPrice, { avoidCents: true })} + + / full program + + {hasSavings && savingsAmount !== null && listAmount !== null ? ( + + + Save {formatPrice(savingsAmount, { avoidCents: true })} + + + + {formatPrice(listAmount, { avoidCents: true })} + {" "} + total for {totalRequired} {pluralize("course", totalRequired)}{" "} + purchased separately + + + ) : null} + {program.page.financial_assistance_form_url ? ( + + Financial assistance available + + ) : null} + {enrollmentType === "both" ? ( + + - ) : null} - {enrollmentType === "both" ? ( - - - - - Start for free - - - or upgrade to certificate - - - - ) : null} - - - ) : ( - - ) + + + + + Start for free + + + or upgrade to certificate + + + + ) : null} + + + ) : ( + + ) return ( - {enrollmentType === "paid" || enrollmentType === "both" ? : null} + {enrollmentType === "paid" || enrollmentType === "both" ? ( + + ) : null} {enrollmentType === "paid" || enrollmentType === "both" ? ( paidSection