Advertise parent programs on course product pages#3029
Advertise parent programs on course product pages#3029ChristopherChudzicki wants to merge 29 commits intomainfrom
Conversation
Reorder the sidebar card so the enroll button appears after the summary info rows rather than before them, matching the Figma design. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the "Course is part of the following programs" info row with a ProgramBundleUpsell component that fetches each parent program's details and shows a bundle pricing card with a "View Program" link, ordered after the enroll button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…borders/padding Replace three separate DOM structures (desktop/tablet/mobile) in the product page info box with a single HTML structure using CSS grid for responsive layout. Fixes program bundle upsell border on tablet by resetting bleed margins and adding direct border; adjusts SummaryRoot padding per breakpoint; adds AskTIM placeholder button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… AskTIM placeholder - Split SummaryRoot into SummaryCard (border/shadow wrapper) and SummaryContent (padded metadata area) so the bundle upsell can span edge-to-edge without negative margins - Delete unused ProductNavbar.tsx - Add temporary AskTIM button placeholder using smoot-design Button - Add TODO comment for hardcoded 1.2x strike price - Add comment explaining per-program detail fetches Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…psell - Remove grid-based tablet layout (RightCol, ActionsArea) from template - Enrollment button desktop-only via DesktopEnrollArea; AskTIM outside card - SummaryContent is now just a padding wrapper; gap/layout owned by SummaryRows - SummaryRows: flex-column on desktop/mobile, column-count:2 on tablet - Bundle upsell: row layout on tablet (text left, button right), skip if no price - Add separator borders between multiple upsell items Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Sticky position on SummaryCol so AskTIM scrolls with the card - align-self: flex-start required for sticky in flex parent - Replace magic top offset with HEADER_HEIGHT constant - Add column-rule divider between tablet metadata columns Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use avoidCents for bundle price formatting (whole dollars)
- Show required course count in upsell title ("Get all 5 ...")
- Bold program title, normal weight for rest of upsell text
- Replace strikethrough price with static "(19% off)" discount
- Replace "Want a program certificate?" with "Best value"
- Make AskTIM button full width, match Figma shadow opacity
- Adjust bundle spacing to match Figma (8px header gap, 24px item gap)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace DesktopEnrollArea (md-only) with EnrollArea (always visible)
- Tablet enrollment button: max-width 50%, center-aligned
- Update ProgramBundleUpsell tests for new copy ("Best value", course
count, avoidCents price, "(19% off)") and add no-price test case
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ProgramBundleUpsell waits for all queries to load and filters to
programs with prices before rendering, preventing an empty "Best
value" header
- AskTIM button text uses productNoun prop ("course" or "program")
- enrollButton is now a required prop on ProductPageTemplate
- Remove console.log placeholder from AskTIM onClick
- Clean up BundleUpsellContainer border styles per breakpoint
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use monthsFromNow() helper so tests don't rot as calendar time passes. Past dates for "Anytime" logic use negative offsets; ordering tests use spaced-out future offsets; display-only tests let the factory decide. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ises Add test verifying the upsell renders nothing when the program detail fetch fails. Use Promise.withResolvers() to control mock response timing in both no-price and error tests, replacing the fragile queryClient.isFetching() polling pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show structured skeleton placeholders (title, price, button) while program details load, then render real content or unmount if no priced programs. Tests now verify the full loading→removed lifecycle using waitForElementToBeRemoved and use distinct prices for multi-program assertions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace sidebarSummary, enrollButton, programUpsell, summaryTitle, and productNoun props with a single infoBox prop. CoursePage uses CourseInfoBox, ProgramPage uses ProgramInfoBox. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rather than silently returning null when all program detail fetches fail, log a console.warn to aid debugging. Add allowConsoleErrors() to the error-state test to accommodate jest-fail-on-console. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename CourseInfoBox → InfoBoxCourse, ProgramInfoBox → InfoBoxProgram for alphabetical grouping with InfoBoxParts. Extract shared styled components (now InfoBoxCard, InfoBoxContent, InfoBoxEnrollArea, AskTimButton) into InfoBoxParts so InfoBoxProgram no longer depends on InfoBoxCourse for shared primitives. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move ProgramBundleUpsell component and all its styled components out of ProductSummary.tsx into ProgramBundleUpsell.tsx, with tests moved to ProgramBundleUpsell.test.tsx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove dedicated InfoBoxCourse and InfoBoxProgram test files — their coverage is better placed at the page level. Rename section headings to "Course Information" / "Program Information". Add a CoursePage test asserting the program bundle upsell renders when the course belongs to a program. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove redundant fragment wrappers in InfoBoxCourse and InfoBoxProgram
- Use V2ProgramDetail instead of V2Program for RequirementsSection prop type
- Remove no-op `& {}` intersection on CourseCertificateBox
- Clarify bundle upsell smoke test name in CoursePage.test
- Fix misleading `courses` variable name in ProgramPage 404 test
- Batch program detail fetches into single programsList query
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On tablet, use flex + justify-end instead of centering the enrollment button. Also remove unnecessary width: 100% and WideButtonLink wrapper from enrollment button components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove "Best value" header, use h5 bold for full title line, and regroup price + button into BundleUpsellActions so price sits above the button on tablet (single HTML structure, CSS-only responsive). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gumaerc
left a comment
There was a problem hiding this comment.
This looks good and functioned properly for me once I got the test data set up, but I have a few questions:
| ...others | ||
| }) => { | ||
| if (!course.programs || course.programs.length === 0) return null | ||
| const label = `Part of the following ${pluralize("program", course.programs.length)}` |
There was a problem hiding this comment.
Question:
This would previously display even for programs that do not have a price. The new ProgramBundleUpsell component does not display if a program doesn't have a price. This text is still shown to the side of the upsell examples here in Figma:
https://www.figma.com/design/Eux3guSenAFVvNHGi1Y9Wm/MIT-Design-System?node-id=34395-88815&m=dev
I'm not sure if it's intended to be kept though, so maybe worth asking Steve / Bilal?
There was a problem hiding this comment.
Ah, good point:
- we now have
enrollment_modes - Previously, if a program had no price
enrollment_modes=["audit"], we still showed the parent program "Part of the following programs". - Now if a program is free-only, we do not show parent program.
☝️ also applies to programs with "verified" enrollment mode but no corresponding product.
I'd call this an expected behavior change and in practice it probably doesn't matter. I think all programs will have verified/product options.
We could add a "Part of " version of the upsell for free-only programs, but... I don't know if we need it. I would say ... we do NOT need it for now.
| width: "100%", | ||
| }, | ||
| !href && { | ||
| pointerEvents: "none", |
There was a problem hiding this comment.
Previously when there was no href available, the pointer on this button would be intentionally set to the normal pointer to indicate that it wasn't clickable. Now, the button looks clickable but doesn't do anything. Is this intentional?
There was a problem hiding this comment.
It was somewhat intentional... I didn't think a style override was worth handling bad-data.
But I think I'll restore it for now. We can build this behavior into smoot-design later.
| [theme.breakpoints.between("sm", "md")]: { | ||
| display: "flex", | ||
| justifyContent: "flex-end", | ||
| "> *": { flex: "0 1 50%" }, |
There was a problem hiding this comment.
I don't think we need to change this here but I'm a bit confused as to the scope of the changes here. This PR is supposed to be about the program upsell but seems to be doing a lot of change to the layout, but doesn't completely align it with the current Figma.
The tablet view is rendering the enrollment button inside the info box here, but it seems it's supposed to be in the image area. I imagine that work is being saved for a future PR?
There was a problem hiding this comment.
The scope of this PR is "Program Bundle Upsell" / "Unified HTML structure" / " InfoBox refactor".
The banner updates are handled in
Let me confirm the enrollment button removal with Steve/Bilal tomorrow. I suspect that's finalized now, and since the banner PR is pretty much ready, we can remove it now. (Figma still shows it for Mobile, but that may be a mistake; we will keep it in the infobox on desktop.)
What are the relevant tickets?
Closes https://github.com/mitodl/hq/issues/10434
Description (What does it do?)
Program Bundle Upsell: When a course belongs to one or more MITx programs, shows a "Best value" upsell below the course metadata with the program price, required course count, and a "View Program" link.
Unified HTML structure: The info box sidebar uses a single HTML structure with CSS-only responsive behavior instead of separate DOM trees per breakpoint.
InfoBox refactor: Introduced
InfoBoxCourseandInfoBoxProgramwrapper components that own the full sidebar card composition (summary + enrollment button + upsell), with shared styled primitives inInfoBoxParts.ProductPageTemplatenow takes a singleinfoBoxprop instead of separate sidebar-related props.Screenshots (if appropriate):
Note
Enrollment likely to be removed from mobile/tablet infobox, but will wait until it has been merged into banner.
How can this be tested?
Prerequisites:
NEXT_PUBLIC_MITX_ONLINE_BASE_URL=https://api.rc.learn.mit.edu/mitxonline./manage.py create_courseware,./manage.py create_courseware_page, use--helpfor more info). In Django Admin, add courses as a requirement for a program; create products for the program. Ensure all of the data is live and wagtail pages published. Wagtail pages also needinclude_in_learn_catalog.Now:
Additional Context