From 9c02afbe32f558889eb2584350def25ff058b9af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 10:46:30 -0400 Subject: [PATCH 01/10] fix(deps): update django-health-check digest to 592f6a8 (#2527) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- poetry.lock | 17 ++++++++++++----- pyproject.toml | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 67c0936349..e2aa5084b2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1568,7 +1568,7 @@ django = ">=3.2" [[package]] name = "django-health-check" -version = "3.20.1.dev4+gb0500d1" +version = "3.20.1.dev9+g592f6a8fc" description = "Monitor the health of your Django app and its connected services." optional = false python-versions = ">=3.9" @@ -1581,14 +1581,14 @@ Django = ">=4.2" [package.extras] docs = ["sphinx"] -lint = ["ruff (==0.12.3)"] +lint = ["ruff (==0.13.1)"] test = ["boto3", "celery", "django-storages", "pytest", "pytest-cov", "pytest-django", "redis"] [package.source] type = "git" url = "https://github.com/revsys/django-health-check" -reference = "b0500d14c338040984f02ee34ffbe6643b005084" -resolved_reference = "b0500d14c338040984f02ee34ffbe6643b005084" +reference = "592f6a8fc2a8481f4f810678461db52bd1b0e3d6" +resolved_reference = "592f6a8fc2a8481f4f810678461db52bd1b0e3d6" [[package]] name = "django-imagekit" @@ -2432,6 +2432,12 @@ files = [ {file = "geventhttpclient-2.3.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:447fc2d49a41449684154c12c03ab80176a413e9810d974363a061b71bdbf5a0"}, {file = "geventhttpclient-2.3.3-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4598c2aa14c866a10a07a2944e2c212f53d0c337ce211336ad68ae8243646216"}, {file = "geventhttpclient-2.3.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:69d2bd7ab7f94a6c73325f4b88fd07b0d5f4865672ed7a519f2d896949353761"}, + {file = "geventhttpclient-2.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:45a3f7e3531dd2650f5bb840ed11ce77d0eeb45d0f4c9cd6985eb805e17490e6"}, + {file = "geventhttpclient-2.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:73b427e0ea8c2750ee05980196893287bfc9f2a155a282c0f248b472ea7ae3e7"}, + {file = "geventhttpclient-2.3.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2959ef84271e4fa646c3dbaad9e6f2912bf54dcdfefa5999c2ef7c927d92127"}, + {file = "geventhttpclient-2.3.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a800fcb8e53a8f4a7c02b4b403d2325a16cad63a877e57bd603aa50bf0e475b"}, + {file = "geventhttpclient-2.3.3-pp311-pypy311_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:528321e9aab686435ba09cc6ff90f12e577ace79762f74831ec2265eeab624a8"}, + {file = "geventhttpclient-2.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:034be44ff3318359e3c678cb5c4ed13efd69aeb558f2981a32bd3e3fb5355700"}, {file = "geventhttpclient-2.3.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a3182f1457599c2901c48a1def37a5bc4762f696077e186e2050fcc60b2fbdf"}, {file = "geventhttpclient-2.3.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:86b489238dc2cbfa53cdd5621e888786a53031d327e0a8509529c7568292b0ce"}, {file = "geventhttpclient-2.3.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4c8aca6ab5da4211870c1d8410c699a9d543e86304aac47e1558ec94d0da97a"}, @@ -2741,6 +2747,7 @@ files = [ {file = "greenlet-3.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a8fa80665b1a29faf76800173ff5325095f3e66a78e62999929809907aca5659"}, {file = "greenlet-3.2.2-cp39-cp39-win32.whl", hash = "sha256:6629311595e3fe7304039c67f00d145cd1d38cf723bb5b99cc987b23c1433d61"}, {file = "greenlet-3.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:eeb27bece45c0c2a5842ac4c5a1b5c2ceaefe5711078eed4e8043159fa05c834"}, + {file = "greenlet-3.2.2.tar.gz", hash = "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485"}, ] markers = {main = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} @@ -9234,4 +9241,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "~3.12" -content-hash = "d79dd5e782dbf31ad9f38ee63a42d2d624e499d46c081297a15dcddfb86c373e" +content-hash = "65a1d46d17e74c07139fc9f70b1c6c7c3a7a25589b732280b9691718222a2d14" diff --git a/pyproject.toml b/pyproject.toml index 9ea4298904..5b359b8469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ django-cache-memoize = "^0.2.0" django-cors-headers = "^4.0.0" django-filter = "^2.4.0" django-guardian = "^3.0.0" -django-health-check = { git = "https://github.com/revsys/django-health-check", rev="b0500d14c338040984f02ee34ffbe6643b005084" } # pragma: allowlist secret +django-health-check = { git = "https://github.com/revsys/django-health-check", rev="592f6a8fc2a8481f4f810678461db52bd1b0e3d6" } # pragma: allowlist secret django-imagekit = "^5.0.0" From 14e46c32c4e22ae450d9efbe4834583cdf29d918 Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:00:18 +0200 Subject: [PATCH 02/10] Certificate social media / URL sharing (#2524) * Share button and popover * OG metadata. Pass page URL for SSR * Center the popover to the buttons/page * Element type * Pass pageUrl * Update title and filnames * Remove redundant aria-label. Add tests * Update frontends/main/src/app-pages/CertificatePage/CertificatePage.test.tsx Co-authored-by: Chris Chudzicki * Remove unused * aria-labels and improve test selectors * Remove unused --------- Co-authored-by: Chris Chudzicki --- .../CertificatePage/CertificatePage.test.tsx | 77 +++++++- .../CertificatePage/CertificatePage.tsx | 39 ++++- .../CertificatePage/SharePopover.tsx | 164 ++++++++++++++++++ .../[certificateType]/[uuid]/page.tsx | 59 ++++++- .../[certificateType]/[uuid]/pdf/route.tsx | 4 +- frontends/main/src/common/urls.ts | 6 + .../CallToActionSection.tsx | 14 +- 7 files changed, 346 insertions(+), 17 deletions(-) create mode 100644 frontends/main/src/app-pages/CertificatePage/SharePopover.tsx diff --git a/frontends/main/src/app-pages/CertificatePage/CertificatePage.test.tsx b/frontends/main/src/app-pages/CertificatePage/CertificatePage.test.tsx index 4015429088..37b61afb3a 100644 --- a/frontends/main/src/app-pages/CertificatePage/CertificatePage.test.tsx +++ b/frontends/main/src/app-pages/CertificatePage/CertificatePage.test.tsx @@ -1,9 +1,15 @@ import React from "react" import moment from "moment" import { factories, setMockResponse } from "api/test-utils" -import { screen, renderWithProviders } from "@/test-utils" +import { screen, renderWithProviders, user } from "@/test-utils" import CertificatePage, { CertificateType } from "./CertificatePage" +import SharePopover from "./SharePopover" import { urls } from "api/mitxonline-test-utils" +import { + FACEBOOK_SHARE_BASE_URL, + TWITTER_SHARE_BASE_URL, + LINKEDIN_SHARE_BASE_URL, +} from "@/common/urls" describe("CertificatePage", () => { it("renders a course certificate", async () => { @@ -18,6 +24,7 @@ describe("CertificatePage", () => { , ) @@ -80,6 +87,7 @@ describe("CertificatePage", () => { , ) @@ -99,3 +107,70 @@ describe("CertificatePage", () => { await screen.findAllByText(certificate.uuid) }) }) + +describe("CertificatePage - SharePopover", () => { + const mockProps = { + open: true, + title: "Test Certificate", + anchorEl: document.createElement("div"), + onClose: jest.fn(), + pageUrl: "https://example.com/certificate/123", + } + + const mockWriteText = jest.fn() + Object.assign(navigator, { + clipboard: { + writeText: mockWriteText, + }, + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("renders the SharePopover with correct content", () => { + renderWithProviders() + + expect(screen.getByText("Share on social")).toBeInTheDocument() + expect(screen.getByText("Share a link")).toBeInTheDocument() + expect(screen.getByDisplayValue(mockProps.pageUrl)).toBeInTheDocument() + expect(screen.getByText("Copy Link")).toBeInTheDocument() + }) + + it("renders social media share links with correct URLs", () => { + renderWithProviders() + + const facebookHref = `${FACEBOOK_SHARE_BASE_URL}?u=${encodeURIComponent(mockProps.pageUrl)}` + const twitterHref = `${TWITTER_SHARE_BASE_URL}?text=${encodeURIComponent(mockProps.title)}&url=${encodeURIComponent(mockProps.pageUrl)}` + const linkedinHref = `${LINKEDIN_SHARE_BASE_URL}?url=${encodeURIComponent(mockProps.pageUrl)}` + + const facebookLink = screen.getByRole("link", { name: "Share on Facebook" }) + const twitterLink = screen.getByRole("link", { name: "Share on Twitter" }) + const linkedinLink = screen.getByRole("link", { name: "Share on LinkedIn" }) + + expect(facebookLink).toHaveAttribute("href", facebookHref) + expect(twitterLink).toHaveAttribute("href", twitterHref) + expect(linkedinLink).toHaveAttribute("href", linkedinHref) + + expect(facebookLink).toHaveAttribute("target", "_blank") + expect(twitterLink).toHaveAttribute("target", "_blank") + expect(linkedinLink).toHaveAttribute("target", "_blank") + }) + + it("copies link to clipboard when copy button is clicked", async () => { + renderWithProviders() + + const copyButton = screen.getByRole("button", { name: "Copy Link" }) + await user.click(copyButton) + + expect(mockWriteText).toHaveBeenCalledWith(mockProps.pageUrl) + screen.getByRole("button", { name: "Copied!" }) + }) + + it("does not render when open is false", () => { + renderWithProviders() + + expect(screen.queryByText("Share on social")).not.toBeInTheDocument() + expect(screen.queryByText("Share a link")).not.toBeInTheDocument() + }) +}) diff --git a/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx b/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx index a9a2ae60bf..f8317c5b7b 100644 --- a/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx +++ b/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx @@ -1,6 +1,6 @@ "use client" -import React, { useRef, useEffect, useCallback } from "react" +import React, { useRef, useEffect, useCallback, useState } from "react" import { notFound } from "next/navigation" import Image from "next/image" import { Link, Typography, styled } from "ol-components" @@ -12,12 +12,13 @@ import OpenLearningLogo from "@/public/images/mit-open-learning-logo.svg" import CertificateBadgeDesktop from "@/public/images/certificate-badge-desktop.svg" import CertificateBadgeMobile from "@/public/images/certificate-badge-mobile.svg" import { formatDate, NoSSR } from "ol-utilities" -import { RiDownloadLine, RiPrinterLine } from "@remixicon/react" +import { RiDownloadLine, RiPrinterLine, RiShareLine } from "@remixicon/react" import type { V2ProgramCertificate, V2CourseRunCertificate, SignatoryItem, } from "@mitodl/mitxonline-api-axios/v2" +import SharePopover from "./SharePopover" const Page = styled.div(({ theme }) => ({ backgroundImage: `url(${backgroundImage.src})`, @@ -57,12 +58,16 @@ const Title = styled(Typography)(({ theme }) => ({ }, })) -const Buttons = styled.div({ +const Buttons = styled.div(({ theme }) => ({ display: "flex", gap: "12px", justifyContent: "center", - marginBottom: "50px", -}) + width: "fit-content", + margin: "0 auto 50px auto", + [theme.breakpoints.down("md")]: { + margin: "0 auto 32px auto", + }, +})) const Outer = styled.div(({ theme }) => ({ maxWidth: "1306px", @@ -640,7 +645,8 @@ export enum CertificateType { const CertificatePage: React.FC<{ certificateType: CertificateType uuid: string -}> = ({ certificateType, uuid }) => { + pageUrl: string +}> = ({ certificateType, uuid, pageUrl }) => { const { data: courseCertificateData, isLoading: isCourseLoading, @@ -694,6 +700,9 @@ const CertificatePage: React.FC<{ } }, [print]) + const [shareOpen, setShareOpen] = useState(false) + const shareButtonRef = useRef(null) + if (isCourseLoading || isProgramLoading) { return } @@ -709,7 +718,7 @@ const CertificatePage: React.FC<{ const url = window.URL.createObjectURL(blob) const a = document.createElement("a") a.href = url - a.download = `${title} Certificate - MIT Open Learning.pdf` + a.download = `${title} Certificate issued by MIT Open Learning.pdf` document.body.appendChild(a) a.click() document.body.removeChild(a) @@ -728,12 +737,19 @@ const CertificatePage: React.FC<{ return ( + setShareOpen(false)} + pageUrl={pageUrl} + /> <Typography variant="h3"> <strong>{title}</strong> {displayType} </Typography> - + +