Skip to content
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -18,6 +24,7 @@ describe("CertificatePage", () => {
<CertificatePage
certificateType={CertificateType.Course}
uuid={certificate.uuid}
pageUrl={`https://${process.env.NEXT_PUBLIC_ORIGIN}/certificate/course/${certificate.uuid}`}
/>,
)

Expand Down Expand Up @@ -80,6 +87,7 @@ describe("CertificatePage", () => {
<CertificatePage
certificateType={CertificateType.Program}
uuid={certificate.uuid}
pageUrl={`https://${process.env.NEXT_PUBLIC_ORIGIN}/certificate/program/${certificate.uuid}`}
/>,
)

Expand All @@ -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(<SharePopover {...mockProps} />)

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(<SharePopover {...mockProps} />)

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(<SharePopover {...mockProps} />)

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(<SharePopover {...mockProps} open={false} />)

expect(screen.queryByText("Share on social")).not.toBeInTheDocument()
expect(screen.queryByText("Share a link")).not.toBeInTheDocument()
})
})
39 changes: 31 additions & 8 deletions frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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})`,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -694,6 +700,9 @@ const CertificatePage: React.FC<{
}
}, [print])

const [shareOpen, setShareOpen] = useState(false)
const shareButtonRef = useRef<HTMLDivElement>(null)

if (isCourseLoading || isProgramLoading) {
return <Page />
}
Expand All @@ -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)
Expand All @@ -728,19 +737,33 @@ const CertificatePage: React.FC<{

return (
<Page>
<SharePopover
open={shareOpen}
title={`${title} Certificate issued by MIT Open Learning`}
anchorEl={shareButtonRef.current}
onClose={() => setShareOpen(false)}
pageUrl={pageUrl}
/>
<Title>
<Typography variant="h3">
<strong>{title}</strong> {displayType}
</Typography>
</Title>
<Buttons>
<Buttons ref={shareButtonRef}>
<Button
variant="primary"
startIcon={<RiDownloadLine />}
onClick={download}
>
Download PDF
</Button>
<Button
variant="bordered"
startIcon={<RiShareLine />}
onClick={() => setShareOpen(true)}
>
Share
</Button>
<Button
variant="bordered"
startIcon={<RiPrinterLine />}
Expand Down
164 changes: 164 additions & 0 deletions frontends/main/src/app-pages/CertificatePage/SharePopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import React, { useState } from "react"
import { Popover, Typography, styled, theme } from "ol-components"
import Link from "next/link"
import {
FACEBOOK_SHARE_BASE_URL,
TWITTER_SHARE_BASE_URL,
LINKEDIN_SHARE_BASE_URL,
} from "@/common/urls"
import {
RiFacebookFill,
RiTwitterXLine,
RiLinkedinFill,
RiLink,
} from "@remixicon/react"
import { Button, Input } from "@mitodl/smoot-design"

const StyledPopover = styled(Popover)({
width: "648px",
maxWidth: "calc(100vw - 48px)",
".MuiPopper-arrow": {
display: "none",
},
})

const Contents = styled.div(({ theme }) => ({
padding: "8px",
display: "flex",
gap: "40px",
[theme.breakpoints.down("sm")]: {
flexDirection: "column",
gap: "32px",
},
}))

const SocialContainer = styled.div({
display: "flex",
flexDirection: "column",
gap: "16px",
})

const LinkContainer = styled.div({
display: "flex",
flexDirection: "column",
gap: "12px",
flex: 1,
})

const Heading = styled(Typography)(({ theme }) => ({
...theme.typography.body2,
color: theme.custom.colors.darkGray2,
fontWeight: theme.typography.fontWeightBold,
}))

const ButtonContainer = styled.div({
display: "flex",
alignSelf: "stretch",
gap: "16px",
a: {
height: "18px",
},
})

const ShareLink = styled(Link)({
color: theme.custom.colors.silverGrayDark,
"&:hover": {
color: theme.custom.colors.lightRed,
},
})

const LinkControls = styled.div(({ theme }) => ({
display: "flex",
gap: "16px",
input: {
...theme.typography.body3,
color: theme.custom.colors.darkGray2,
padding: "0 3px",
},
}))

const RedLinkIcon = styled(RiLink)({
color: theme.custom.colors.red,
})

const CopyLinkButton = styled(Button)({
minWidth: "104px",
})

const SharePopover = ({
open,
title,
anchorEl,
onClose,
pageUrl,
}: {
open: boolean
title: string
anchorEl: HTMLDivElement | null
onClose: () => void
pageUrl: string
}) => {
const [copyText, setCopyText] = useState("Copy Link")

return (
<StyledPopover open={open} onClose={onClose} anchorEl={anchorEl}>
<Contents>
<SocialContainer>
<Heading variant="body2">Share on social</Heading>
<ButtonContainer>
<ShareLink
href={`${FACEBOOK_SHARE_BASE_URL}?u=${encodeURIComponent(pageUrl)}`}
aria-label="Share on Facebook"
target="_blank"
>
<RiFacebookFill size={18} />
</ShareLink>
<ShareLink
href={`${TWITTER_SHARE_BASE_URL}?text=${encodeURIComponent(title)}&url=${encodeURIComponent(pageUrl)}`}
aria-label="Share on Twitter"
target="_blank"
>
<RiTwitterXLine size={18} />
</ShareLink>
<ShareLink
href={`${LINKEDIN_SHARE_BASE_URL}?url=${encodeURIComponent(pageUrl)}`}
aria-label="Share on LinkedIn"
target="_blank"
>
<RiLinkedinFill size={18} />
</ShareLink>
</ButtonContainer>
</SocialContainer>
<LinkContainer>
<Heading variant="body2">Share a link</Heading>
<LinkControls>
<Input
fullWidth
value={pageUrl}
size="small"
onClick={(event) => {
const input = event.currentTarget.querySelector("input")
if (!input) return
input.select()
Copy link
Contributor

Choose a reason for hiding this comment

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

nice touch :)

}}
/>
<CopyLinkButton
size="small"
edge="circular"
variant="bordered"
startIcon={<RedLinkIcon />}
onClick={() => {
navigator.clipboard?.writeText(pageUrl)
setCopyText("Copied!")
}}
>
{copyText}
</CopyLinkButton>
</LinkControls>
</LinkContainer>
</Contents>
</StyledPopover>
)
}

export default SharePopover
Loading
Loading