diff --git a/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/CertificatePreview.tsx b/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/CertificatePreview.tsx index 4148f4e1..6af4fb2e 100644 --- a/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/CertificatePreview.tsx +++ b/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/CertificatePreview.tsx @@ -10,6 +10,9 @@ interface CertificatePreviewProps { certificationType: string; certificatePattern: string; certificateInstructor?: string; + certificateId?: string; + awardedDate?: string; + qrCodeLink?: string; } const CertificatePreview: React.FC = ({ @@ -17,7 +20,10 @@ const CertificatePreview: React.FC = ({ certificationDescription, certificationType, certificatePattern, - certificateInstructor + certificateInstructor, + certificateId, + awardedDate, + qrCodeLink }) => { const [qrCodeUrl, setQrCodeUrl] = useState(''); const org = useOrg() as any; @@ -26,7 +32,7 @@ const CertificatePreview: React.FC = ({ useEffect(() => { const generateQRCode = async () => { try { - const certificateData = `https://learnhouse.app/verify/LH-2024-001`; + const certificateData = qrCodeLink || `${certificateId}`; const qrUrl = await QRCode.toDataURL(certificateData, { width: 185, margin: 1, @@ -44,7 +50,7 @@ const CertificatePreview: React.FC = ({ }; generateQRCode(); - }, []); + }, [certificateId, qrCodeLink]); // Function to get theme colors for each pattern const getPatternTheme = (pattern: string) => { switch (pattern) { @@ -433,7 +439,7 @@ const CertificatePreview: React.FC = ({
- ID: LH-2024-001 + ID: {certificateId || 'LH-2024-001'}
@@ -553,7 +559,7 @@ const CertificatePreview: React.FC = ({ Awarded
- Dec 15, 2024 + {awardedDate || 'Dec 15, 2024'}
diff --git a/apps/web/components/Pages/Activity/CourseEndView.tsx b/apps/web/components/Pages/Activity/CourseEndView.tsx index a493ca78..95876eb3 100644 --- a/apps/web/components/Pages/Activity/CourseEndView.tsx +++ b/apps/web/components/Pages/Activity/CourseEndView.tsx @@ -1,11 +1,17 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useEffect, useState } from 'react'; import ReactConfetti from 'react-confetti'; -import { Trophy, ArrowLeft, BookOpen, Target } from 'lucide-react'; +import { Trophy, ArrowLeft, BookOpen, Target, Download } from 'lucide-react'; import Link from 'next/link'; import { getUriWithOrg } from '@services/config/config'; import { getCourseThumbnailMediaDirectory } from '@services/media/media'; import { useWindowSize } from 'usehooks-ts'; import { useOrg } from '@components/Contexts/OrgContext'; +import { useLHSession } from '@components/Contexts/LHSessionContext'; +import { getUserCertificates } from '@services/courses/certifications'; +import CertificatePreview from '@components/Dashboard/Pages/Course/EditCourseCertification/CertificatePreview'; +import html2canvas from 'html2canvas'; +import jsPDF from 'jspdf'; +import QRCode from 'qrcode'; interface CourseEndViewProps { courseName: string; @@ -26,6 +32,13 @@ const CourseEndView: React.FC = ({ }) => { const { width, height } = useWindowSize(); const org = useOrg() as any; + const session = useLHSession() as any; + const [userCertificate, setUserCertificate] = useState(null); + const [isLoadingCertificate, setIsLoadingCertificate] = useState(false); + const [certificateError, setCertificateError] = useState(null); + const qrCodeLink = getUriWithOrg(orgslug, `/certificate/${userCertificate.user_certification_uuid}/verify`); + + // Check if course is actually completed const isCourseCompleted = useMemo(() => { @@ -62,6 +75,296 @@ const CourseEndView: React.FC = ({ return totalActivities > 0 && completedActivities === totalActivities; }, [trailData, course]); + // Fetch user certificate when course is completed + useEffect(() => { + const fetchUserCertificate = async () => { + if (!isCourseCompleted) return; + + if (!session?.data?.tokens?.access_token) { + setCertificateError('Authentication required to view certificate'); + return; + } + + setIsLoadingCertificate(true); + setCertificateError(null); + try { + const cleanCourseUuid = courseUuid.replace('course_', ''); + const result = await getUserCertificates( + `course_${cleanCourseUuid}`, + session.data.tokens.access_token + ); + + if (result.success && result.data && result.data.length > 0) { + setUserCertificate(result.data[0]); + } else { + setCertificateError('No certificate found for this course'); + } + } catch (error) { + console.error('Error fetching user certificate:', error); + setCertificateError('Failed to load certificate. Please try again later.'); + } finally { + setIsLoadingCertificate(false); + } + }; + + fetchUserCertificate(); + }, [isCourseCompleted, courseUuid, session?.data?.tokens?.access_token]); + + // Generate PDF using canvas + const downloadCertificate = async () => { + if (!userCertificate) return; + + try { + // Create a temporary div for the certificate + const certificateDiv = document.createElement('div'); + certificateDiv.style.position = 'absolute'; + certificateDiv.style.left = '-9999px'; + certificateDiv.style.top = '0'; + certificateDiv.style.width = '800px'; + certificateDiv.style.height = '600px'; + certificateDiv.style.background = 'white'; + certificateDiv.style.padding = '40px'; + certificateDiv.style.fontFamily = 'Arial, sans-serif'; + certificateDiv.style.textAlign = 'center'; + certificateDiv.style.display = 'flex'; + certificateDiv.style.flexDirection = 'column'; + certificateDiv.style.justifyContent = 'center'; + certificateDiv.style.alignItems = 'center'; + certificateDiv.style.position = 'relative'; + certificateDiv.style.overflow = 'hidden'; + + // Get theme colors based on pattern + const getPatternTheme = (pattern: string) => { + switch (pattern) { + case 'royal': + return { primary: '#b45309', secondary: '#d97706', icon: '#d97706' }; + case 'tech': + return { primary: '#0e7490', secondary: '#0891b2', icon: '#0891b2' }; + case 'nature': + return { primary: '#15803d', secondary: '#16a34a', icon: '#16a34a' }; + case 'geometric': + return { primary: '#7c3aed', secondary: '#9333ea', icon: '#9333ea' }; + case 'vintage': + return { primary: '#c2410c', secondary: '#ea580c', icon: '#ea580c' }; + case 'waves': + return { primary: '#1d4ed8', secondary: '#2563eb', icon: '#2563eb' }; + case 'minimal': + return { primary: '#374151', secondary: '#4b5563', icon: '#4b5563' }; + case 'professional': + return { primary: '#334155', secondary: '#475569', icon: '#475569' }; + case 'academic': + return { primary: '#3730a3', secondary: '#4338ca', icon: '#4338ca' }; + case 'modern': + return { primary: '#1d4ed8', secondary: '#2563eb', icon: '#2563eb' }; + default: + return { primary: '#374151', secondary: '#4b5563', icon: '#4b5563' }; + } + }; + + const theme = getPatternTheme(userCertificate.certification.config.certificate_pattern); + const certificateId = userCertificate.certificate_user.user_certification_uuid; + const qrCodeData = qrCodeLink; + + // Generate QR code + const qrCodeDataUrl = await QRCode.toDataURL(qrCodeData, { + width: 120, + margin: 2, + color: { + dark: '#000000', + light: '#FFFFFF' + }, + errorCorrectionLevel: 'M', + type: 'image/png' + }); + + // Create certificate content + certificateDiv.innerHTML = ` +
ID: ${certificateId}
+ +
+ QR Code +
+ +
+
+ Certificate +
+
+ +
🏆
+ +
${userCertificate.certification.config.certification_name}
+ +
${userCertificate.certification.config.certification_description || 'This is to certify that the course has been successfully completed.'}
+ +
+
+
+
+
+ +
+ + ${userCertificate.certification.config.certification_type === 'completion' ? 'Course Completion' : + userCertificate.certification.config.certification_type === 'achievement' ? 'Achievement Based' : + userCertificate.certification.config.certification_type === 'assessment' ? 'Assessment Based' : + userCertificate.certification.config.certification_type === 'participation' ? 'Participation' : + userCertificate.certification.config.certification_type === 'mastery' ? 'Skill Mastery' : + userCertificate.certification.config.certification_type === 'professional' ? 'Professional Development' : + userCertificate.certification.config.certification_type === 'continuing' ? 'Continuing Education' : + userCertificate.certification.config.certification_type === 'workshop' ? 'Workshop Attendance' : + userCertificate.certification.config.certification_type === 'specialization' ? 'Specialization' : 'Course Completion'} +
+ +
+
+ Certificate ID: ${certificateId} +
+
+ Awarded: ${new Date(userCertificate.certificate_user.created_at).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + })} +
+ ${userCertificate.certification.config.certificate_instructor ? + `
+ Instructor: ${userCertificate.certification.config.certificate_instructor} +
` : '' + } +
+ +
+ This certificate can be verified at ${qrCodeLink} +
+ `; + + // Add to document temporarily + document.body.appendChild(certificateDiv); + + // Convert to canvas + const canvas = await html2canvas(certificateDiv, { + width: 800, + height: 600, + scale: 2, // Higher resolution + useCORS: true, + allowTaint: true, + backgroundColor: '#ffffff' + }); + + // Remove temporary div + document.body.removeChild(certificateDiv); + + // Create PDF + const imgData = canvas.toDataURL('image/png'); + const pdf = new jsPDF('landscape', 'mm', 'a4'); + + // Calculate dimensions to center the certificate + const pdfWidth = pdf.internal.pageSize.getWidth(); + const pdfHeight = pdf.internal.pageSize.getHeight(); + const imgWidth = 280; // mm + const imgHeight = 210; // mm + + // Center the image + const x = (pdfWidth - imgWidth) / 2; + const y = (pdfHeight - imgHeight) / 2; + + pdf.addImage(imgData, 'PNG', x, y, imgWidth, imgHeight); + + // Save the PDF + const fileName = `${userCertificate.certification.config.certification_name.replace(/[^a-zA-Z0-9]/g, '_')}_Certificate.pdf`; + pdf.save(fileName); + + } catch (error) { + console.error('Error generating PDF:', error); + alert('Failed to generate PDF. Please try again.'); + } + }; + // Calculate progress for incomplete courses const progressInfo = useMemo(() => { if (!trailData || !course || isCourseCompleted) return null; @@ -115,7 +418,7 @@ const CourseEndView: React.FC = ({ /> -
+
{thumbnailImage && ( = ({ Your dedication and hard work have paid off. You've mastered all the content in this course.

+ {/* Certificate Display */} + {isLoadingCertificate ? ( +
+
+ Loading your certificate... +
+ ) : certificateError ? ( +
+

+ {certificateError} +

+
+ ) : userCertificate ? ( +
+

Your Certificate

+
+
+ +
+
+
+ +
+
+ ) : ( +
+

+ No certificate is available for this course. Contact your instructor for more information. +

+
+ )} +
@@ -224,7 +578,7 @@ const CourseEndView: React.FC = ({
diff --git a/apps/web/components/Pages/Certificate/CertificatePage.tsx b/apps/web/components/Pages/Certificate/CertificatePage.tsx new file mode 100644 index 00000000..62871cb5 --- /dev/null +++ b/apps/web/components/Pages/Certificate/CertificatePage.tsx @@ -0,0 +1,426 @@ +'use client'; + +import React, { useEffect, useState, useRef } from 'react'; +import { useLHSession } from '@components/Contexts/LHSessionContext'; +import { getUserCertificates } from '@services/courses/certifications'; +import CertificatePreview from '@components/Dashboard/Pages/Course/EditCourseCertification/CertificatePreview'; +import { ArrowLeft, Download } from 'lucide-react'; +import Link from 'next/link'; +import { getUriWithOrg } from '@services/config/config'; +import html2canvas from 'html2canvas'; +import jsPDF from 'jspdf'; +import QRCode from 'qrcode'; + +interface CertificatePageProps { + orgslug: string; + courseid: string; + qrCodeLink: string; +} + +const CertificatePage: React.FC = ({ orgslug, courseid, qrCodeLink }) => { + const session = useLHSession() as any; + const [userCertificate, setUserCertificate] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch user certificate + useEffect(() => { + const fetchCertificate = async () => { + if (!session?.data?.tokens?.access_token) { + setError('Authentication required to view certificate'); + setIsLoading(false); + return; + } + + try { + const cleanCourseId = courseid.replace('course_', ''); + const result = await getUserCertificates( + `course_${cleanCourseId}`, + session.data.tokens.access_token + ); + + if (result.success && result.data && result.data.length > 0) { + setUserCertificate(result.data[0]); + } else { + setError('No certificate found for this course'); + } + } catch (error) { + console.error('Error fetching certificate:', error); + setError('Failed to load certificate. Please try again later.'); + } finally { + setIsLoading(false); + } + }; + + fetchCertificate(); + }, [courseid, session?.data?.tokens?.access_token]); + + + + // Generate PDF using canvas + const downloadCertificate = async () => { + if (!userCertificate) return; + + try { + // Create a temporary div for the certificate + const certificateDiv = document.createElement('div'); + certificateDiv.style.position = 'absolute'; + certificateDiv.style.left = '-9999px'; + certificateDiv.style.top = '0'; + certificateDiv.style.width = '800px'; + certificateDiv.style.height = '600px'; + certificateDiv.style.background = 'white'; + certificateDiv.style.padding = '40px'; + certificateDiv.style.fontFamily = 'Arial, sans-serif'; + certificateDiv.style.textAlign = 'center'; + certificateDiv.style.display = 'flex'; + certificateDiv.style.flexDirection = 'column'; + certificateDiv.style.justifyContent = 'center'; + certificateDiv.style.alignItems = 'center'; + certificateDiv.style.position = 'relative'; + certificateDiv.style.overflow = 'hidden'; + + // Get theme colors based on pattern + const getPatternTheme = (pattern: string) => { + switch (pattern) { + case 'royal': + return { primary: '#b45309', secondary: '#d97706', icon: '#d97706' }; + case 'tech': + return { primary: '#0e7490', secondary: '#0891b2', icon: '#0891b2' }; + case 'nature': + return { primary: '#15803d', secondary: '#16a34a', icon: '#16a34a' }; + case 'geometric': + return { primary: '#7c3aed', secondary: '#9333ea', icon: '#9333ea' }; + case 'vintage': + return { primary: '#c2410c', secondary: '#ea580c', icon: '#ea580c' }; + case 'waves': + return { primary: '#1d4ed8', secondary: '#2563eb', icon: '#2563eb' }; + case 'minimal': + return { primary: '#374151', secondary: '#4b5563', icon: '#4b5563' }; + case 'professional': + return { primary: '#334155', secondary: '#475569', icon: '#475569' }; + case 'academic': + return { primary: '#3730a3', secondary: '#4338ca', icon: '#4338ca' }; + case 'modern': + return { primary: '#1d4ed8', secondary: '#2563eb', icon: '#2563eb' }; + default: + return { primary: '#374151', secondary: '#4b5563', icon: '#4b5563' }; + } + }; + + const theme = getPatternTheme(userCertificate.certification.config.certificate_pattern); + const certificateId = userCertificate.certificate_user.user_certification_uuid; + const qrCodeData = qrCodeLink ; + + // Generate QR code + const qrCodeDataUrl = await QRCode.toDataURL(qrCodeData, { + width: 120, + margin: 2, + color: { + dark: '#000000', + light: '#FFFFFF' + }, + errorCorrectionLevel: 'M', + type: 'image/png' + }); + + // Create certificate content + certificateDiv.innerHTML = ` +
ID: ${certificateId}
+ +
+ QR Code +
+ +
+
+ Certificate +
+
+ +
🏆
+ +
${userCertificate.certification.config.certification_name}
+ +
${userCertificate.certification.config.certification_description || 'This is to certify that the course has been successfully completed.'}
+ +
+
+
+
+
+ +
+ + ${userCertificate.certification.config.certification_type === 'completion' ? 'Course Completion' : + userCertificate.certification.config.certification_type === 'achievement' ? 'Achievement Based' : + userCertificate.certification.config.certification_type === 'assessment' ? 'Assessment Based' : + userCertificate.certification.config.certification_type === 'participation' ? 'Participation' : + userCertificate.certification.config.certification_type === 'mastery' ? 'Skill Mastery' : + userCertificate.certification.config.certification_type === 'professional' ? 'Professional Development' : + userCertificate.certification.config.certification_type === 'continuing' ? 'Continuing Education' : + userCertificate.certification.config.certification_type === 'workshop' ? 'Workshop Attendance' : + userCertificate.certification.config.certification_type === 'specialization' ? 'Specialization' : 'Course Completion'} +
+ +
+
+ Certificate ID: ${certificateId} +
+
+ Awarded: ${new Date(userCertificate.certificate_user.created_at).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + })} +
+ ${userCertificate.certification.config.certificate_instructor ? + `
+ Instructor: ${userCertificate.certification.config.certificate_instructor} +
` : '' + } +
+ +
+ This certificate can be verified at ${qrCodeData.replace('https://', '').replace('http://', '')} +
+ `; + + // Add to document temporarily + document.body.appendChild(certificateDiv); + + // Convert to canvas + const canvas = await html2canvas(certificateDiv, { + width: 800, + height: 600, + scale: 2, // Higher resolution + useCORS: true, + allowTaint: true, + backgroundColor: '#ffffff' + }); + + // Remove temporary div + document.body.removeChild(certificateDiv); + + // Create PDF + const imgData = canvas.toDataURL('image/png'); + const pdf = new jsPDF('landscape', 'mm', 'a4'); + + // Calculate dimensions to center the certificate + const pdfWidth = pdf.internal.pageSize.getWidth(); + const pdfHeight = pdf.internal.pageSize.getHeight(); + const imgWidth = 280; // mm + const imgHeight = 210; // mm + + // Center the image + const x = (pdfWidth - imgWidth) / 2; + const y = (pdfHeight - imgHeight) / 2; + + pdf.addImage(imgData, 'PNG', x, y, imgWidth, imgHeight); + + // Save the PDF + const fileName = `${userCertificate.certification.config.certification_name.replace(/[^a-zA-Z0-9]/g, '_')}_Certificate.pdf`; + pdf.save(fileName); + + } catch (error) { + console.error('Error generating PDF:', error); + alert('Failed to generate PDF. Please try again.'); + } + }; + + if (isLoading) { + return ( +
+
+
+

Loading certificate...

+
+
+ ); + } + + if (error) { + return ( +
+
+
+

Certificate Not Available

+

{error}

+ + + Back to Course + +
+
+
+ ); + } + + if (!userCertificate) { + return ( +
+
+
+

No Certificate Found

+

+ No certificate is available for this course. Please contact your instructor for more information. +

+ + + Back to Course + +
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+ + + Back to Course + + +
+ +
+
+ + {/* Certificate Display */} +
+
+ +
+
+ + {/* Instructions */} +
+

+ Click "Download PDF" to generate and download a high-quality certificate PDF. +

+

+ The PDF includes a scannable QR code for certificate verification. +

+
+
+
+ ); +}; + +export default CertificatePage; \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index afb9c2c0..1a4ddecb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -50,6 +50,7 @@ "@tiptap/react": "^2.11.7", "@tiptap/starter-kit": "^2.11.7", "@types/dompurify": "^3.2.0", + "@types/html2canvas": "^1.0.0", "@types/randomcolor": "^0.5.9", "avvvatars-react": "^0.4.2", "class-variance-authority": "^0.7.1", @@ -62,6 +63,9 @@ "framer-motion": "^12.6.3", "get-youtube-id": "^1.0.1", "highlight.js": "^11.11.1", + "html2canvas": "^1.4.1", + "jspdf": "^3.0.1", + "jspdf-html2canvas": "^1.5.2", "katex": "^0.16.21", "lowlight": "^3.3.0", "lucide-react": "^0.453.0", diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 2fe163ba..62594a84 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -129,6 +129,9 @@ importers: '@types/dompurify': specifier: ^3.2.0 version: 3.2.0 + '@types/html2canvas': + specifier: ^1.0.0 + version: 1.0.0 '@types/randomcolor': specifier: ^0.5.9 version: 0.5.9 @@ -165,6 +168,15 @@ importers: highlight.js: specifier: ^11.11.1 version: 11.11.1 + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 + jspdf: + specifier: ^3.0.1 + version: 3.0.1 + jspdf-html2canvas: + specifier: ^1.5.2 + version: 1.5.2 katex: specifier: ^0.16.21 version: 0.16.21 @@ -1842,6 +1854,10 @@ packages: '@types/hoist-non-react-statics@3.3.6': resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} + '@types/html2canvas@1.0.0': + resolution: {integrity: sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==} + deprecated: This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed. + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1863,6 +1879,9 @@ packages: '@types/qrcode@1.5.5': resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + '@types/randomcolor@0.5.9': resolution: {integrity: sha512-k58cfpkK15AKn1m+oRd9nh5BnuiowhbyvBBdAzcddtARMr3xRzP0VlFaAKovSG6N6Knx08EicjPlOMzDejerrQ==} @@ -2097,6 +2116,11 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -2118,6 +2142,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -2128,6 +2156,11 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + btoa@1.2.1: + resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} + engines: {node: '>= 0.4.0'} + hasBin: true + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -2158,6 +2191,10 @@ packages: caniuse-lite@1.0.30001712: resolution: {integrity: sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==} + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2223,6 +2260,9 @@ packages: resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} engines: {node: '>=4'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-to-react-native@3.2.0: resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} @@ -2322,6 +2362,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dompurify@2.5.8: + resolution: {integrity: sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==} + dompurify@3.2.5: resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==} @@ -2550,6 +2593,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2700,6 +2746,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2859,6 +2909,15 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + jspdf-html2canvas@1.5.2: + resolution: {integrity: sha512-jA/QvZWPiBJFR7Ut4XCwKsOcml1L75lvO7+mmw5qgTfmi8qbfHNr2DPXeQ8J6mNKJfB4Fn8QBBiYcr2NR3ssdQ==} + + jspdf@2.5.2: + resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==} + + jspdf@3.0.1: + resolution: {integrity: sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -3185,6 +3244,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3320,6 +3382,9 @@ packages: raf-schd@4.0.3: resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + randomcolor@0.6.2: resolution: {integrity: sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==} @@ -3459,6 +3524,9 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -3497,6 +3565,10 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} @@ -3590,6 +3662,10 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -3664,6 +3740,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + swr@2.3.3: resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==} peerDependencies: @@ -3687,6 +3767,9 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} @@ -3814,6 +3897,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -5354,6 +5440,10 @@ snapshots: '@types/react': 19.0.10 hoist-non-react-statics: 3.3.2 + '@types/html2canvas@1.0.0': + dependencies: + html2canvas: 1.4.1 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -5375,6 +5465,9 @@ snapshots: dependencies: '@types/node': 20.12.2 + '@types/raf@3.4.3': + optional: true + '@types/randomcolor@0.5.9': {} '@types/react-dom@19.0.4(@types/react@19.0.10)': @@ -5632,6 +5725,8 @@ snapshots: async-function@1.0.0: {} + atob@2.1.2: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -5650,6 +5745,8 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -5663,6 +5760,8 @@ snapshots: dependencies: fill-range: 7.1.1 + btoa@1.2.1: {} + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -5692,6 +5791,18 @@ snapshots: caniuse-lite@1.0.30001712: {} + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.27.0 + '@types/raf': 3.4.3 + core-js: 3.42.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -5757,6 +5868,10 @@ snapshots: css-color-keywords@1.0.0: {} + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + css-to-react-native@3.2.0: dependencies: camelize: 1.0.1 @@ -5842,6 +5957,9 @@ snapshots: dependencies: esutils: 2.0.3 + dompurify@2.5.8: + optional: true + dompurify@3.2.5: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -6217,6 +6335,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -6370,6 +6490,11 @@ snapshots: dependencies: react-is: 16.13.1 + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + ignore@5.3.2: {} import-fresh@3.3.1: @@ -6531,6 +6656,35 @@ snapshots: dependencies: minimist: 1.2.8 + jspdf-html2canvas@1.5.2: + dependencies: + html2canvas: 1.4.1 + jspdf: 2.5.2 + + jspdf@2.5.2: + dependencies: + '@babel/runtime': 7.27.0 + atob: 2.1.2 + btoa: 1.2.1 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.42.0 + dompurify: 2.5.8 + html2canvas: 1.4.1 + + jspdf@3.0.1: + dependencies: + '@babel/runtime': 7.27.0 + atob: 2.1.2 + btoa: 1.2.1 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.42.0 + dompurify: 3.2.5 + html2canvas: 1.4.1 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 @@ -6845,6 +6999,9 @@ snapshots: path-parse@1.0.7: {} + performance-now@2.1.0: + optional: true + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -7019,6 +7176,11 @@ snapshots: raf-schd@4.0.3: {} + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + randomcolor@0.6.2: {} rangetouch@2.0.1: {} @@ -7155,6 +7317,9 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regenerator-runtime@0.13.11: + optional: true + regenerator-runtime@0.14.1: {} regexp.prototype.flags@1.5.4: @@ -7196,6 +7361,9 @@ snapshots: reusify@1.1.0: {} + rgbcolor@1.0.1: + optional: true + rope-sequence@1.3.4: {} run-parallel@1.2.0: @@ -7351,6 +7519,9 @@ snapshots: stable-hash@0.0.5: {} + stackblur-canvas@2.7.0: + optional: true + streamsearch@1.1.0: {} string-width@4.2.3: @@ -7444,6 +7615,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-pathdata@6.0.3: + optional: true + swr@2.3.3(react@19.0.0): dependencies: dequal: 2.0.3 @@ -7462,6 +7636,10 @@ snapshots: tapable@2.2.1: {} + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + tiny-case@1.0.3: {} tiny-invariant@1.3.3: {} @@ -7604,6 +7782,10 @@ snapshots: lodash.debounce: 4.0.8 react: 19.0.0 + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + uuid@8.3.2: {} uuid@9.0.1: {} diff --git a/apps/web/services/courses/certifications.ts b/apps/web/services/courses/certifications.ts index 822db702..ff308a7d 100644 --- a/apps/web/services/courses/certifications.ts +++ b/apps/web/services/courses/certifications.ts @@ -59,4 +59,16 @@ export async function deleteCertification( ) const res = await errorHandling(result) return res +} + +export async function getUserCertificates( + course_uuid: string, + access_token: string +) { + const result = await fetch( + `${getAPIUrl()}certifications/user/course/${course_uuid}`, + RequestBodyWithAuthHeader('GET', null, null, access_token) + ) + const res = await getResponseMetadata(result) + return res } \ No newline at end of file