diff --git a/.cursor/rules/basics.mdc b/.cursor/rules/basics.mdc new file mode 100644 index 000000000..698e44116 --- /dev/null +++ b/.cursor/rules/basics.mdc @@ -0,0 +1,10 @@ +--- +alwaysApply: true +--- + +- Use `pnpm` as package manager. +- The project is structured as a monorepo i.e. a pnpm workspace. The apps are in `apps` folder and re-usable packages are in `packages`. +- Command for running script in a workspace: `pnpm --filter `. +- Command for running tests: `pnpm test`. +- The project uses shadcn for building UI so stick to its conventions and design. +- In `apps/web` workspace, create a string first in `apps/web/config/strings.ts` and then import it in the `.tsx` files, instead of using inline strings. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 13974cbeb..21f9023a2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ coverage # Text editors configurations .vscode .rgignore -.cursor # Env file .env*.local @@ -39,4 +38,7 @@ report*.json # Dev tools files .eslintcache -.npmrc \ No newline at end of file +.npmrc + +# Jest files +globalConfig.json \ No newline at end of file diff --git a/apps/docs/public/assets/products/accomplishment-page.png b/apps/docs/public/assets/products/accomplishment-page.png new file mode 100644 index 000000000..def0fbf78 Binary files /dev/null and b/apps/docs/public/assets/products/accomplishment-page.png differ diff --git a/apps/docs/public/assets/products/certificate-badge-product-card.png b/apps/docs/public/assets/products/certificate-badge-product-card.png new file mode 100644 index 000000000..1a9911d47 Binary files /dev/null and b/apps/docs/public/assets/products/certificate-badge-product-card.png differ diff --git a/apps/docs/public/assets/products/certificate-template-customization-expand.png b/apps/docs/public/assets/products/certificate-template-customization-expand.png new file mode 100644 index 000000000..fdc989ee9 Binary files /dev/null and b/apps/docs/public/assets/products/certificate-template-customization-expand.png differ diff --git a/apps/docs/public/assets/products/courselit-certificate-example.jpg b/apps/docs/public/assets/products/courselit-certificate-example.jpg new file mode 100644 index 000000000..c2b365304 Binary files /dev/null and b/apps/docs/public/assets/products/courselit-certificate-example.jpg differ diff --git a/apps/docs/public/assets/products/customize-certificate-texts.png b/apps/docs/public/assets/products/customize-certificate-texts.png new file mode 100644 index 000000000..592b4f775 Binary files /dev/null and b/apps/docs/public/assets/products/customize-certificate-texts.png differ diff --git a/apps/docs/public/assets/products/issue-certificate-toggle.png b/apps/docs/public/assets/products/issue-certificate-toggle.png new file mode 100644 index 000000000..19c1b15df Binary files /dev/null and b/apps/docs/public/assets/products/issue-certificate-toggle.png differ diff --git a/apps/docs/public/assets/products/preview-certificate.png b/apps/docs/public/assets/products/preview-certificate.png new file mode 100644 index 000000000..c03487eb4 Binary files /dev/null and b/apps/docs/public/assets/products/preview-certificate.png differ diff --git a/apps/docs/public/assets/products/product-manage-menu.png b/apps/docs/public/assets/products/product-manage-menu.png new file mode 100644 index 000000000..c088909da Binary files /dev/null and b/apps/docs/public/assets/products/product-manage-menu.png differ diff --git a/apps/docs/public/assets/products/view-certificate-button.png b/apps/docs/public/assets/products/view-certificate-button.png new file mode 100644 index 000000000..2926f5815 Binary files /dev/null and b/apps/docs/public/assets/products/view-certificate-button.png differ diff --git a/apps/docs/src/config.ts b/apps/docs/src/config.ts index 93a61caa3..2c8153a75 100644 --- a/apps/docs/src/config.ts +++ b/apps/docs/src/config.ts @@ -58,6 +58,7 @@ export const SIDEBAR: Sidebar = { { text: "Add content", link: "en/courses/add-content" }, { text: "Manage sections", link: "en/products/section" }, { text: "Invite customers", link: "en/products/invite-customers" }, + { text: "Certificates", link: "en/products/certificates" }, ], "Digital downloads": [ { text: "Introduction", link: "en/downloads/introduction" }, diff --git a/apps/docs/src/pages/en/products/certificates.md b/apps/docs/src/pages/en/products/certificates.md new file mode 100644 index 000000000..277f5bd40 --- /dev/null +++ b/apps/docs/src/pages/en/products/certificates.md @@ -0,0 +1,88 @@ +--- +title: Certificates +description: Issue certificate on course completion +layout: ../../../layouts/MainLayout.astro +--- + +Certificates add significant value to your courses by providing your customers with verifiable credentials they can showcase. CourseLit makes it effortless to issue certificates automatically upon course completion. + +Here are the key features of CourseLit certificates: + +- A dedicated page for each certificate issued, allowing customers to share a verifiable link on their profiles +- Customizable text and images to make certificates reflect your brand + +**Note**: Certificates are only available for `Course` products. + +## Overview + +Customers will receive a certificate after completing all lessons in a course. + +The certificate will be hosted on a dedicated page that can be used to verify the customer's learning achievement. + +Here is an example certificate: + +![CourseLit Example Certificate](/assets/products/courselit-certificate-example.jpg) + +## Enable certificates for a course + +1. Go to the manage section of your `Course` product. + + ![Product manage menu](/assets/products/product-manage-menu.png) + +2. Scroll down to the `Certificates` section and toggle on the `Issue certificate` switch. + + ![Issue certificate switch](/assets/products/issue-certificate-toggle.png) + + That's it! Your customers will now receive a certificate upon course completion. + + > Previous customers will not receive certificates retroactively, as the certificate generation process is automated. + +## Customize the certificate + +1. Click on the `Chevron` icon on the far right of the `Certificate Template` subsection under the `Certificates` section to expand it. + + ![Certificate template customization expand button](/assets/products/certificate-template-customization-expand.png) + +2. Here are the elements you can customize in a certificate: + + - Certificate Title + - Certificate Subtitle + - Certificate Description + - Signature Name + - Signature Designation + - Signature Image + - Logo (defaults to school's logo if not provided) + +3. Modify the text configuration to your preference and click `Save`. + + ![Customize certificate texts](/assets/products/customize-certificate-texts.png) + +4. `Signature Image` and `Logo` properties are automatically saved. + +## Preview the certificate + +Click on the `Preview` link located to the left of the `Issue certificates` switch. + +You will be taken to a new tab where you can see an actual certificate in PDF format with your changes applied. + +![Preview certificate](/assets/products/preview-certificate.png) + +## Customer's experience + +Upon completing a course, customers will be taken back to the `My content` lobby. On the product card, they will see a badge indicating that a certificate is available. + +![Certificate badge on product card](/assets/products/certificate-badge-product-card.png) + +On the course's introduction page, customers will see a `View certificate` button. + +![View certificate button on product's intro in course viewer](/assets/products/view-certificate-button.png) + +When they click the `View certificate` button, customers are taken to the `Accomplishment` page, where they can view all certification details. + +![Accomplishment Page](/assets/products/accomplishment-page.png) + +Customers can also print their certificate by clicking the `Print` button. + +## Stuck somewhere? + +We are always here for you. Come chat with us in our Discord channel or send a tweet at @CourseLit. diff --git a/apps/web/app/(with-contexts)/(with-layout)/accomplishment/[certId]/page.tsx b/apps/web/app/(with-contexts)/(with-layout)/accomplishment/[certId]/page.tsx new file mode 100644 index 000000000..f2ba93add --- /dev/null +++ b/apps/web/app/(with-contexts)/(with-layout)/accomplishment/[certId]/page.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { + Badge, + Button, + Header1, + Header2, + Section, + Text1, +} from "@courselit/page-primitives"; +import { redirect, useParams } from "next/navigation"; +import { ThemeContext } from "@components/contexts"; +import { useContext, useRef, useState } from "react"; +import { BadgeCheck, ExternalLinkIcon, Printer } from "lucide-react"; +import Link from "next/link"; +import { Image } from "@courselit/components-library"; +import { formattedLocaleDate } from "@ui-lib/utils"; +import { useCertificate } from "@/hooks/use-certificate"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function AccomplishmentPage() { + const params = useParams(); + const certId = params.certId; + const { theme } = useContext(ThemeContext); + const [isIframeLoaded, setIsIframeLoaded] = useState(false); + const iframeRef = useRef(null); + const { certificate, loaded } = useCertificate(certId as string); + + const handlePrint = () => { + const iframeElement = iframeRef.current; + if (!iframeElement) return; + const iframeWindow = iframeElement.contentWindow; + if (!iframeWindow) return; + iframeWindow.focus(); + iframeWindow.print(); + }; + + if (loaded && !certificate) { + redirect("/"); + } + + if (!certificate) { + return ( +
+
+
+ + +
+ + {/* User completion info skeleton */} +
+ +
+ + +
+
+ + {/* Certificate section header + viewer skeleton */} +
+ + +
+
+
+ ); + } + + return ( +
+
+
+ + {certificate.productTitle} + +
+ + + Visit + + +
+
+ + {/* User completion info - responsive layout */} +
+ {certificate.userImage && ( +
+ {certificate.userName} +
+ )} +
+ + + Completed by{" "} + + {certificate.userName} + + + + {formattedLocaleDate(+certificate.createdAt)} + +
+
+ + {/* Certificate section header - responsive */} +
+
+ Certificate + +
+ +
+ {!isIframeLoaded && ( + + )} +
+
+