diff --git a/frontends/api/package.json b/frontends/api/package.json
index 9a85853dbc..e7763145fa 100644
--- a/frontends/api/package.json
+++ b/frontends/api/package.json
@@ -30,7 +30,7 @@
"ol-test-utilities": "0.0.0"
},
"dependencies": {
- "@mitodl/mitxonline-api-axios": "2025.8.12",
+ "@mitodl/mitxonline-api-axios": "2025.8.18",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.6.3"
}
diff --git a/frontends/main/next.config.js b/frontends/main/next.config.js
index 9ee6c74782..f2f968dbb7 100644
--- a/frontends/main/next.config.js
+++ b/frontends/main/next.config.js
@@ -92,16 +92,7 @@ const nextConfig = {
images: {
remotePatterns: [
{
- protocol: "http",
hostname: "**",
- port: "",
- pathname: "**",
- },
- {
- protocol: "https",
- hostname: "**",
- port: "",
- pathname: "**",
},
],
},
diff --git a/frontends/main/public/images/certificate-badge-desktop.svg b/frontends/main/public/images/certificate-badge-desktop.svg
new file mode 100644
index 0000000000..d94accad15
--- /dev/null
+++ b/frontends/main/public/images/certificate-badge-desktop.svg
@@ -0,0 +1,8 @@
+
diff --git a/frontends/main/public/images/certificate-badge-mobile.svg b/frontends/main/public/images/certificate-badge-mobile.svg
new file mode 100644
index 0000000000..e2e09beda8
--- /dev/null
+++ b/frontends/main/public/images/certificate-badge-mobile.svg
@@ -0,0 +1,8 @@
+
diff --git a/frontends/main/public/images/mit-learn-logo-black.svg b/frontends/main/public/images/mit-learn-logo-black.svg
new file mode 100644
index 0000000000..7fa921fc6c
--- /dev/null
+++ b/frontends/main/public/images/mit-learn-logo-black.svg
@@ -0,0 +1,5 @@
+
diff --git a/frontends/main/public/images/mit-open-learning-logo.svg b/frontends/main/public/images/mit-open-learning-logo.svg
new file mode 100644
index 0000000000..9f54342c30
--- /dev/null
+++ b/frontends/main/public/images/mit-open-learning-logo.svg
@@ -0,0 +1,22 @@
+
diff --git a/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx b/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx
new file mode 100644
index 0000000000..2cc5b029ba
--- /dev/null
+++ b/frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx
@@ -0,0 +1,502 @@
+"use client"
+
+import React from "react"
+import { useParams } from "next/navigation"
+import Image from "next/image"
+import { Link, Typography, styled } from "ol-components"
+import backgroundImage from "@/public/images/backgrounds/error_page_background.svg"
+import { certificateQueries } from "api/mitxonline-hooks/certificates"
+import { useQuery } from "@tanstack/react-query"
+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"
+
+const Page = styled.div(({ theme }) => ({
+ backgroundImage: `url(${backgroundImage.src})`,
+ backgroundAttachment: "fixed",
+ backgroundRepeat: "no-repeat",
+ backgroundSize: "contain",
+ display: "flow-root",
+ flexGrow: 1,
+ height: "100%",
+ padding: "0 16px 90px",
+ [theme.breakpoints.down("sm")]: {
+ backgroundImage: "none",
+ padding: "0 40px 40px",
+ },
+}))
+
+const Title = styled(Typography)(({ theme }) => ({
+ margin: "60px 160px 40px",
+ textAlign: "center",
+ span: {
+ fontWeight: theme.typography.fontWeightLight,
+ color: theme.custom.colors.darkGray2,
+ },
+ [theme.breakpoints.down("lg")]: {
+ span: {
+ fontSize: theme.typography.pxToRem(26),
+ lineHeight: theme.typography.pxToRem(30),
+ },
+ },
+ [theme.breakpoints.down("md")]: {
+ textAlign: "left",
+ margin: "24px 0",
+ span: {
+ fontSize: theme.typography.pxToRem(24),
+ lineHeight: theme.typography.pxToRem(30),
+ },
+ },
+}))
+
+const Certificate = styled.div(({ theme }) => ({
+ maxWidth: "1306px",
+ minWidth: "1200px",
+ border: `4px solid ${theme.custom.colors.silverGray}`,
+ padding: "24px 23px",
+ backgroundColor: theme.custom.colors.white,
+ marginTop: "50px",
+ margin: "0 auto",
+ [theme.breakpoints.down("lg")]: {
+ padding: 0,
+ border: "none",
+ maxWidth: "unset",
+ minWidth: "unset",
+ },
+}))
+
+const Inner = styled.div(({ theme }) => ({
+ border: `1px solid ${theme.custom.colors.silverGrayLight}`,
+ padding: "67px",
+ display: "flex",
+ flexDirection: "column",
+ gap: "56px",
+ position: "relative",
+ [theme.breakpoints.down("lg")]: {
+ padding: "40px",
+ gap: "40px",
+ },
+ [theme.breakpoints.down("md")]: {
+ border: `2px solid ${theme.custom.colors.lightGray2}`,
+ padding: "24px 16px",
+ gap: "24px",
+ textAlign: "center",
+ },
+}))
+
+const Logo = styled(Image)(({ theme }) => ({
+ width: "260px",
+ height: "auto",
+ [theme.breakpoints.down("lg")]: {
+ width: "230px",
+ height: "59px",
+ },
+ [theme.breakpoints.down("md")]: {
+ width: "129px",
+ margin: "0 auto",
+ },
+}))
+
+const Badge = styled.div(({ theme }) => ({
+ backgroundImage: `url(${CertificateBadgeDesktop.src})`,
+ position: "absolute",
+ top: 0,
+ right: "67px",
+ width: "230px",
+ height: "391px",
+ textAlign: "center",
+ padding: "81px 34px",
+ backgroundRepeat: "no-repeat",
+ [theme.breakpoints.down("lg")]: {
+ backgroundImage: `url(${CertificateBadgeMobile.src})`,
+ top: "24px",
+ right: "40px",
+ width: "156px",
+ height: "191px",
+ },
+ [theme.breakpoints.down("md")]: {
+ position: "relative",
+ height: "191px",
+ top: 0,
+ right: 0,
+ margin: "0 auto",
+ },
+}))
+
+const BadgeText = styled(Typography)(({ theme }) => ({
+ color: theme.custom.colors.white,
+ position: "absolute",
+ top: "169px",
+ right: "26px",
+ width: "175px",
+ textAlign: "center",
+ [theme.breakpoints.down("lg")]: {
+ fontSize: theme.typography.pxToRem(16),
+ lineHeight: "150%",
+ fontWeight: theme.typography.fontWeightMedium,
+ top: "53px",
+ right: "18px",
+ width: "119px",
+ },
+ [theme.breakpoints.down("md")]: {
+ width: "130px",
+ position: "absolute",
+ top: "50px",
+ right: "50%",
+ transform: "translateX(50%)",
+ },
+}))
+
+const Certification = styled.div(({ theme }) => ({
+ display: "flex",
+ flexDirection: "column",
+ gap: "16px",
+ maxWidth: "850px",
+ ".MuiTypography-h4": {
+ fontWeight: theme.typography.fontWeightLight,
+ color: theme.custom.colors.silverGrayDark,
+ },
+ [theme.breakpoints.down("lg")]: {
+ ".MuiTypography-h4": {
+ fontSize: theme.typography.pxToRem(16),
+ },
+ },
+ [theme.breakpoints.down("md")]: {
+ gap: 0,
+ ".MuiTypography-h4": {
+ fontSize: theme.typography.pxToRem(16),
+ fontWeight: theme.typography.fontWeightMedium,
+ lineHeight: "150%",
+ color: theme.custom.colors.silverGrayDark,
+ },
+ },
+}))
+
+const NameText = styled(Typography)(({ theme }) => ({
+ color: theme.custom.colors.red,
+ display: "block",
+ [theme.breakpoints.down("lg")]: {
+ fontSize: theme.typography.pxToRem(34),
+ lineHeight: theme.typography.pxToRem(40),
+ },
+ [theme.breakpoints.down("md")]: {
+ fontSize: theme.typography.pxToRem(24),
+ lineHeight: theme.typography.pxToRem(30),
+ marginTop: "4px",
+ },
+}))
+
+const AchievementText = styled(Typography)(({ theme }) => ({
+ fontWeight: theme.typography.fontWeightLight,
+ fontSize: theme.typography.pxToRem(20),
+ lineHeight: theme.typography.pxToRem(26),
+ color: theme.custom.colors.silverGrayDark,
+ strong: {
+ fontWeight: theme.typography.fontWeightBold,
+ },
+ [theme.breakpoints.down("lg")]: {
+ fontSize: theme.typography.pxToRem(16),
+ lineHeight: theme.typography.pxToRem(24),
+ },
+ [theme.breakpoints.down("md")]: {
+ fontSize: theme.typography.pxToRem(14),
+ lineHeight: theme.typography.pxToRem(18),
+ marginTop: "16px",
+ },
+}))
+
+const CourseInfo = styled.div(({ theme }) => ({
+ display: "flex",
+ flexDirection: "column",
+ gap: "16px",
+ ".MuiTypography-h4": {
+ fontWeight: theme.typography.fontWeightLight,
+ color: theme.custom.colors.silverGrayDark,
+ },
+ [theme.breakpoints.down("lg")]: {
+ ".MuiTypography-h2": {
+ fontSize: theme.typography.pxToRem(28),
+ lineHeight: theme.typography.pxToRem(36),
+ },
+ ".MuiTypography-h4": {
+ fontSize: theme.typography.pxToRem(16),
+ lineHeight: theme.typography.pxToRem(20),
+ },
+ },
+ [theme.breakpoints.down("md")]: {
+ ".MuiTypography-h2": {
+ fontSize: theme.typography.pxToRem(18),
+ lineHeight: theme.typography.pxToRem(26),
+ fontWeight: theme.typography.fontWeightMedium,
+ },
+ ".MuiTypography-h4": {
+ fontSize: theme.typography.pxToRem(14),
+ lineHeight: theme.typography.pxToRem(18),
+ color: theme.custom.colors.darkGray1,
+ },
+ },
+}))
+
+const Spacer = styled.div(({ theme }) => ({
+ height: "30px",
+ [theme.breakpoints.down("lg")]: {
+ display: "none",
+ },
+}))
+
+const Signatories = styled.div(({ theme }) => ({
+ display: "flex",
+ flexDirection: "row",
+ gap: "16px",
+ width: "100%",
+ [theme.breakpoints.down("md")]: {
+ flexDirection: "column",
+ gap: "24px",
+ },
+}))
+
+const Signatory = styled.div(({ theme }) => ({
+ flex: "1 1 0",
+ minWidth: 0,
+ span: {
+ display: "block",
+ },
+ ".MuiTypography-body1": {
+ color: theme.custom.colors.silverGrayDark,
+ },
+ [theme.breakpoints.down("lg")]: {
+ ".MuiTypography-body1": {
+ fontSize: theme.typography.pxToRem(12),
+ lineHeight: theme.typography.pxToRem(16),
+ },
+ ".MuiTypography-body1:last-child": {
+ marginTop: "8px",
+ },
+ },
+ [theme.breakpoints.down("md")]: {
+ ".MuiTypography-body1": {
+ color: theme.custom.colors.darkGray1,
+ fontSize: theme.typography.pxToRem(14),
+ lineHeight: theme.typography.pxToRem(18),
+ },
+ borderBottom: `1px solid ${theme.custom.colors.lightGray2}`,
+ paddingBottom: "24px",
+ p: {
+ marginTop: "8px",
+ },
+ },
+}))
+
+const Signature = styled.img(({ theme }) => ({
+ width: "auto",
+ height: "60px",
+ [theme.breakpoints.down("lg")]: {
+ height: "54px",
+ },
+ [theme.breakpoints.down("md")]: {
+ height: "40px",
+ },
+}))
+
+const SignatoryName = styled(Typography)(({ theme }) => ({
+ marginBottom: "8px",
+ [theme.breakpoints.down("lg")]: {
+ fontSize: theme.typography.pxToRem(18),
+ lineHeight: theme.typography.pxToRem(26),
+ fontWeight: theme.typography.fontWeightMedium,
+ },
+ [theme.breakpoints.down("md")]: {
+ fontSize: theme.typography.pxToRem(16),
+ lineHeight: "150%",
+ marginTop: "16px",
+ },
+}))
+
+const CertificateId = styled(Typography)(({ theme }) => ({
+ span: {
+ color: theme.custom.colors.silverGrayDark,
+ },
+ [theme.breakpoints.down("lg")]: {
+ fontSize: theme.typography.pxToRem(12),
+ lineHeight: theme.typography.pxToRem(16),
+ },
+ [theme.breakpoints.down("md")]: {
+ fontSize: theme.typography.pxToRem(14),
+ lineHeight: theme.typography.pxToRem(20),
+ span: {
+ display: "block",
+ },
+ },
+}))
+
+const Note = styled(Typography)(({ theme }) => ({
+ margin: "48px 0 64px 0",
+ textAlign: "center",
+ fontSize: theme.typography.pxToRem(16),
+ a: {
+ color: theme.custom.colors.red,
+ textDecoration: "underline",
+ fontSize: theme.typography.pxToRem(16),
+ },
+ [theme.breakpoints.down("lg")]: {
+ fontSize: theme.typography.pxToRem(14),
+ lineHeight: theme.typography.pxToRem(18),
+ a: {
+ fontSize: theme.typography.pxToRem(14),
+ },
+ },
+ [theme.breakpoints.down("md")]: {
+ margin: "32px 0 16px",
+ textAlign: "left",
+ },
+}))
+
+enum CertificateType {
+ Course = "course",
+ Program = "program",
+}
+
+const CertificatePage: React.FC = () => {
+ const { certificateType, uuid } = useParams<{
+ certificateType: CertificateType
+ uuid: string
+ }>()
+
+ console.log("certificateType", certificateType)
+ console.log("uuid", uuid)
+
+ const { data: courseCertData, isLoading: isCourseLoading } = useQuery({
+ ...certificateQueries.courseCertificatesRetrieve({
+ cert_uuid: uuid,
+ }),
+ enabled: certificateType === CertificateType.Course,
+ })
+
+ const { data: programCertData, isLoading: isProgramLoading } = useQuery({
+ ...certificateQueries.programCertificatesRetrieve({
+ cert_uuid: uuid,
+ }),
+ enabled: certificateType === CertificateType.Program,
+ })
+
+ const isLoading = isCourseLoading || isProgramLoading
+
+ if (isLoading) {
+ return
+ }
+
+ const title =
+ certificateType === CertificateType.Course
+ ? courseCertData?.course_run?.course?.title
+ : programCertData?.program?.title
+
+ const displayType =
+ certificateType === CertificateType.Course
+ ? "Module Certificate"
+ : `${programCertData?.program?.program_type} Certificate`
+
+ const userName =
+ certificateType === CertificateType.Course
+ ? courseCertData?.user?.name
+ : programCertData?.user?.name
+
+ const shortDisplayType =
+ certificateType === CertificateType.Course
+ ? "module"
+ : programCertData?.program?.program_type === "Series"
+ ? "series"
+ : `${programCertData?.program?.program_type} program`
+
+ const ceus =
+ certificateType === CertificateType.Course
+ ? null
+ : programCertData?.certificate_page?.CEUs
+
+ const signatories =
+ certificateType === CertificateType.Course
+ ? courseCertData?.certificate_page?.signatory_items
+ : programCertData?.certificate_page?.signatory_items
+
+ const startDate =
+ certificateType === CertificateType.Course
+ ? courseCertData?.course_run?.start_date
+ : programCertData?.program?.start_date
+
+ const endDate =
+ certificateType === CertificateType.Course
+ ? courseCertData?.course_run?.end_date
+ : programCertData?.program?.end_date
+
+ return (
+
+
+
+ {title} {displayType}
+
+
+
+
+
+
+ {displayType}
+
+
+ This is to certify that
+ {userName}
+
+ has successfully completed all requirements of the{" "}
+ Universal Artificial Intelligence{" "}
+ {shortDisplayType}:
+
+
+
+ {title}
+ {ceus ? (
+
+ Awarded {ceus} Continuing Education Units (CEUs)
+
+ ) : null}
+ {startDate && endDate && (
+
+
+ {formatDate(startDate)} - {formatDate(endDate)}
+
+
+ )}
+ {ceus ? null : }
+
+
+ {signatories?.map((signatory, index) => (
+
+
+ {signatory.name}
+ {signatory.title_1}
+ {signatory.title_2}
+
+ {signatory.organization}
+
+
+ ))}
+
+
+ Valid Certificate ID: {uuid}
+
+
+
+
+ Note: The name displayed on your certificate is based
+ on your{" "}
+
+ Profile
+ {" "}
+ information.
+
+
+ )
+}
+
+export default CertificatePage
diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx
index e697569c04..0bbf1ec12f 100644
--- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx
+++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx
@@ -338,6 +338,7 @@ const DashboardCard: React.FC = ({
}) => {
const course = dashboardResource as DashboardCourse
const { title, marketingUrl, enrollment, run } = course
+
const titleSection = isLoading ? (
<>
@@ -353,8 +354,9 @@ const DashboardCard: React.FC = ({
>
{title}
- {enrollment?.status === EnrollmentStatus.Completed ? (
-
+ {enrollment?.status === EnrollmentStatus.Completed &&
+ run.certificate?.link ? (
+
{}
View Certificate
diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.ts b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.ts
index 0065d5eda5..b96af4ce58 100644
--- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.ts
+++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/transform.ts
@@ -55,7 +55,11 @@ const mitxonlineEnrollment = (raw: CourseRunEnrollment): DashboardCourse => {
coursewareUrl: raw.run.courseware_url,
certificate: {
uuid: raw.certificate?.uuid ?? "",
- link: raw.certificate?.link ?? "",
+ link:
+ raw.certificate?.link?.replace(
+ /^\/certificate\/([^/]+)\/$/,
+ "/certificate/course/$1/",
+ ) ?? "",
},
},
enrollment: {
diff --git a/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx b/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx
index f77e04b1b7..173e2aac8d 100644
--- a/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx
+++ b/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx
@@ -13,7 +13,7 @@ type ErrorPageTemplateProps = {
loading?: boolean
}
-const Page = styled(Container)(({ theme }) => ({
+const Page = styled.div(({ theme }) => ({
backgroundImage: `url(${backgroundImage.src})`,
backgroundAttachment: "fixed",
backgroundRepeat: "no-repeat",
@@ -25,7 +25,7 @@ const Page = styled(Container)(({ theme }) => ({
},
}))
-const ErrorContainer = styled.div(({ theme }) => ({
+const ErrorContainer = styled(Container)(({ theme }) => ({
padding: "56px 0",
display: "flex",
flexDirection: "column",
diff --git a/frontends/main/src/app/certificate/[certificateType]/[uuid]/page.tsx b/frontends/main/src/app/certificate/[certificateType]/[uuid]/page.tsx
new file mode 100644
index 0000000000..044e755952
--- /dev/null
+++ b/frontends/main/src/app/certificate/[certificateType]/[uuid]/page.tsx
@@ -0,0 +1,31 @@
+import React from "react"
+import { notFound } from "next/navigation"
+import CertificatePage from "@/app-pages/CertificatePage/CertificatePage"
+
+enum CertificateType {
+ Course = "course",
+ Program = "program",
+}
+
+interface PageProps {
+ params: Promise<{
+ certificateType: CertificateType
+ uuid: string
+ }>
+}
+
+const Page: React.FC = async ({ params }) => {
+ const { certificateType } = await params
+
+ if (
+ ![CertificateType.Course, CertificateType.Program].includes(
+ certificateType as CertificateType,
+ )
+ ) {
+ notFound()
+ }
+
+ return
+}
+
+export default Page
diff --git a/yarn.lock b/yarn.lock
index d080767835..9da5db8882 100755
--- a/yarn.lock
+++ b/yarn.lock
@@ -3028,6 +3028,16 @@ __metadata:
languageName: node
linkType: hard
+"@mitodl/mitxonline-api-axios@npm:2025.8.18":
+ version: 2025.8.18
+ resolution: "@mitodl/mitxonline-api-axios@npm:2025.8.18"
+ dependencies:
+ "@types/node": "npm:^20.11.19"
+ axios: "npm:^1.6.5"
+ checksum: 10/91ff2d63fd251ef07dcea8c34497ddbd3b24046ba639b5ac6819d7bcdef560ea4838ecfd2e8db7876d4611072c7d37250d0bd8bc7e643d96d17d61b4db0c192e
+ languageName: node
+ linkType: hard
+
"@mitodl/open-api-axios@npm:2024.9.16":
version: 2024.9.16
resolution: "@mitodl/open-api-axios@npm:2024.9.16"
@@ -7363,7 +7373,7 @@ __metadata:
resolution: "api@workspace:frontends/api"
dependencies:
"@faker-js/faker": "npm:^9.9.0"
- "@mitodl/mitxonline-api-axios": "npm:2025.8.12"
+ "@mitodl/mitxonline-api-axios": "npm:2025.8.18"
"@tanstack/react-query": "npm:^5.66.0"
"@testing-library/react": "npm:^16.1.0"
axios: "npm:^1.6.3"