Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
310 changes: 250 additions & 60 deletions frontends/main/src/app-pages/ProductPages/ProductSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Copy link
Copy Markdown
Contributor

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 think

We should only need custom typography very rarely. (I don't see the letterspacing thing in figma, but i may have missed it.)

Copy link
Copy Markdown
Contributor

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 think

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 overrides are unnecessary. 14/18/regular is normal body2.

color: theme.custom.colors.silverGrayDark,
}))

const ProgramListPriceAmount = styled.span(({ theme }) => ({
fontStyle: "normal",
fontSize: "14px",
lineHeight: "18px",
Comment on lines +472 to +474
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should be able to drop font style/size/weight and lien height and use subtitle2 here

Image

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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -757,44 +902,9 @@
)
}

const PROGRAM_CERT_INFO_HREF =

Check failure on line 905 in frontends/main/src/app-pages/ProductPages/ProductSummary.tsx

View workflow job for this annotation

GitHub Actions / javascript-tests

'PROGRAM_CERT_INFO_HREF' is assigned a value but never used. Allowed unused vars must match /^_/u
"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
}
Expand All @@ -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
/**
Expand Down
Loading