Skip to content

Commit 42faa63

Browse files
committed
Program Savings Price update
1 parent 6837733 commit 42faa63

1 file changed

Lines changed: 146 additions & 22 deletions

File tree

frontends/main/src/app-pages/ProductPages/ProductSummary.tsx

Lines changed: 146 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,83 @@ const GrayText = styled.span(({ theme }) => ({
378378
color: theme.custom.colors.silverGrayDark,
379379
}))
380380

381+
const ProgramPaySection = styled.div(({ theme }) => ({
382+
display: "flex",
383+
flexDirection: "column",
384+
alignItems: "flex-start",
385+
gap: "4px",
386+
width: "100%",
387+
maxWidth: "346px",
388+
color: theme.custom.colors.darkGray2,
389+
}))
390+
391+
const ProgramPayLabel = styled.span(({ theme }) => ({
392+
...theme.typography.body4,
393+
fontWeight: theme.typography.fontWeightMedium,
394+
color: theme.custom.colors.silverGrayDark,
395+
textTransform: "uppercase",
396+
letterSpacing: "0.04em",
397+
}))
398+
399+
const ProgramPayContent = styled.div(({ theme }) => ({
400+
display: "flex",
401+
flexDirection: "column",
402+
alignItems: "flex-start",
403+
gap: "12px",
404+
width: "100%",
405+
maxWidth: "320px",
406+
[theme.breakpoints.down("sm")]: {
407+
maxWidth: "100%",
408+
},
409+
}))
410+
411+
const ProgramPriceLine = styled.div(({ theme }) => ({
412+
display: "flex",
413+
alignItems: "flex-end",
414+
gap: "4px",
415+
flexWrap: "wrap",
416+
color: theme.custom.colors.darkGray2,
417+
}))
418+
419+
const ProgramPriceAmount = styled.span(({ theme }) => ({
420+
...theme.typography.h3,
421+
fontWeight: theme.typography.fontWeightBold,
422+
lineHeight: "36px",
423+
}))
424+
425+
const ProgramPriceSuffix = styled.span(({ theme }) => ({
426+
...theme.typography.subtitle1,
427+
fontWeight: theme.typography.fontWeightMedium,
428+
color: theme.custom.colors.silverGrayDark,
429+
}))
430+
431+
const ProgramDiscountBlock = styled.div(({ theme }) => ({
432+
display: "flex",
433+
flexDirection: "column",
434+
alignItems: "flex-start",
435+
gap: "4px",
436+
color: theme.custom.colors.darkGray2,
437+
}))
438+
439+
const ProgramSavingsText = styled.span(({ theme }) => ({
440+
...theme.typography.subtitle2,
441+
color: theme.custom.colors.green,
442+
}))
443+
444+
const ProgramListPriceText = styled.span(({ theme }) => ({
445+
...theme.typography.body2,
446+
color: theme.custom.colors.silverGrayDark,
447+
textDecoration: "line-through",
448+
}))
449+
450+
const ProgramPriceDivider = styled.div(({ theme }) => ({
451+
width: "100%",
452+
maxWidth: "346px",
453+
borderTop: `1px solid ${theme.custom.colors.lightGray2}`,
454+
flex: "none",
455+
alignSelf: "stretch",
456+
}))
457+
381458
const CertificateBoxRoot = styled.div(({ theme }) => ({
382459
width: "100%",
383460
backgroundColor: theme.custom.colors.lightGray1,
@@ -796,42 +873,89 @@ const ProgramCertificateBox: React.FC<{ program: V2ProgramDetail }> = ({
796873
}
797874

798875
type ProgramPriceRowProps = HTMLAttributes<HTMLDivElement> & {
799-
program: V2ProgramDetail
876+
program: V2ProgramDetail & {
877+
// Temporary local extension while list_price rolls into upstream API typings.
878+
list_price?: number | string | null
879+
}
800880
}
801881
const ProgramPriceRow: React.FC<ProgramPriceRowProps> = ({
802882
program,
803883
...others
804884
}) => {
805885
const enrollmentType = getEnrollmentType(program.enrollment_modes)
806-
if (enrollmentType === "none") return null
807-
808-
const paidPrice =
809-
enrollmentType === "paid" && program.products[0]?.price ? (
810-
<>
811-
{formatPrice(program.products[0].price, { avoidCents: true })}{" "}
812-
<GrayText>(includes {program.certificate_type})</GrayText>
813-
</>
814-
) : null
886+
// if (enrollmentType === "none") return null
887+
888+
const currentPrice = program.products[0]?.price
889+
const listPrice = program.list_price
890+
891+
const currentAmount = toNumericPrice(currentPrice)
892+
const listAmount = toNumericPrice(listPrice)
893+
const hasSavings =
894+
currentAmount !== null && listAmount !== null && listAmount > currentAmount
895+
const savingsAmount = hasSavings ? listAmount - currentAmount : null
896+
897+
const paidSection =
898+
enrollmentType === "paid" && currentPrice ? (
899+
<ProgramPaySection>
900+
<ProgramPayLabel>Price</ProgramPayLabel>
901+
<ProgramPayContent>
902+
<ProgramPriceLine>
903+
<ProgramPriceAmount>
904+
{formatPrice(currentPrice, { avoidCents: true })}
905+
</ProgramPriceAmount>
906+
<ProgramPriceSuffix>/ full program</ProgramPriceSuffix>
907+
</ProgramPriceLine>
908+
{hasSavings && savingsAmount !== null && listAmount !== null ? (
909+
<ProgramDiscountBlock>
910+
<ProgramSavingsText>
911+
Save {formatPrice(savingsAmount, { avoidCents: true })}
912+
</ProgramSavingsText>
913+
<ProgramListPriceText>
914+
{formatPrice(listAmount, { avoidCents: true })} total for
915+
courses purchased separately
916+
</ProgramListPriceText>
917+
</ProgramDiscountBlock>
918+
) : null}
919+
<GrayText>(includes {program.certificate_type})</GrayText>
920+
</ProgramPayContent>
921+
</ProgramPaySection>
922+
) : (
923+
<InfoLabelValue label="Price" value="Price unavailable" />
924+
)
815925

816926
return (
817-
<InfoRow {...others}>
818-
<InfoRowIcon>
819-
<RiPriceTag3Line aria-hidden="true" />
820-
</InfoRowIcon>
821-
<InfoRowInner>
927+
<Stack {...others} gap="8px" width="100%">
928+
{enrollmentType === "paid" ? <ProgramPriceDivider /> : null}
929+
<InfoRow>
822930
{enrollmentType === "paid" ? (
823-
<InfoLabelValue label="Price" value={paidPrice} />
931+
paidSection
824932
) : (
825-
<InfoLabelValue label="Price" value="Free to Learn" />
933+
<>
934+
<InfoRowIcon>
935+
<RiPriceTag3Line aria-hidden="true" />
936+
</InfoRowIcon>
937+
<InfoRowInner>
938+
<InfoLabelValue label="Price" value="Free to Learn" />
939+
{enrollmentType === "both" ? (
940+
<ProgramCertificateBox program={program} />
941+
) : null}
942+
</InfoRowInner>
943+
</>
826944
)}
827-
{enrollmentType === "both" ? (
828-
<ProgramCertificateBox program={program} />
829-
) : null}
830-
</InfoRowInner>
831-
</InfoRow>
945+
</InfoRow>
946+
</Stack>
832947
)
833948
}
834949

950+
const toNumericPrice = (value: unknown): number | null => {
951+
if (typeof value === "number" && Number.isFinite(value)) return value
952+
if (typeof value === "string") {
953+
const parsed = Number.parseFloat(value)
954+
if (Number.isFinite(parsed)) return parsed
955+
}
956+
return null
957+
}
958+
835959
const ProgramSummary: React.FC<{
836960
program: V2ProgramDetail
837961
/**

0 commit comments

Comments
 (0)