From 86f7a80eb7447b858743e00c4cb6b345272f1807 Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 14 Jul 2025 21:45:58 +0200 Subject: [PATCH] feat(wip): initial ui and functionality for certifications --- .../course/[courseuuid]/[subpage]/page.tsx | 23 +- .../CertificatePreview.tsx | 566 ++++++++++++++++++ .../EditCourseCertification.tsx | 366 +++++++++++ apps/web/package.json | 2 + apps/web/pnpm-lock.yaml | 199 ++++++ 5 files changed, 1155 insertions(+), 1 deletion(-) create mode 100644 apps/web/components/Dashboard/Pages/Course/EditCourseCertification/CertificatePreview.tsx create mode 100644 apps/web/components/Dashboard/Pages/Course/EditCourseCertification/EditCourseCertification.tsx diff --git a/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx b/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx index 0124cd5f..bb59b5b6 100644 --- a/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx @@ -5,11 +5,12 @@ import { CourseProvider } from '../../../../../../../../components/Contexts/Cour import Link from 'next/link' import { CourseOverviewTop } from '@components/Dashboard/Misc/CourseOverviewTop' import { motion } from 'framer-motion' -import { GalleryVerticalEnd, Globe, Info, UserPen, UserRoundCog, Users } from 'lucide-react' +import { GalleryVerticalEnd, Globe, Info, UserPen, UserRoundCog, Users, Award } from 'lucide-react' import EditCourseStructure from '@components/Dashboard/Pages/Course/EditCourseStructure/EditCourseStructure' import EditCourseGeneral from '@components/Dashboard/Pages/Course/EditCourseGeneral/EditCourseGeneral' import EditCourseAccess from '@components/Dashboard/Pages/Course/EditCourseAccess/EditCourseAccess' import EditCourseContributors from '@components/Dashboard/Pages/Course/EditCourseContributors/EditCourseContributors' +import EditCourseCertification from '@components/Dashboard/Pages/Course/EditCourseCertification/EditCourseCertification' export type CourseOverviewParams = { orgslug: string courseuuid: string @@ -102,6 +103,24 @@ function CourseOverviewPage(props: { params: Promise }) { + +
+
+ +
Certification
+
+
+ @@ -117,6 +136,8 @@ function CourseOverviewPage(props: { params: Promise }) { {params.subpage == 'general' ? () : ('')} {params.subpage == 'access' ? () : ('')} {params.subpage == 'contributors' ? () : ('')} + {params.subpage == 'certification' ? () : ('')} + diff --git a/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/CertificatePreview.tsx b/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/CertificatePreview.tsx new file mode 100644 index 00000000..4148f4e1 --- /dev/null +++ b/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/CertificatePreview.tsx @@ -0,0 +1,566 @@ +import React, { useEffect, useState } from 'react'; +import { Award, CheckCircle, QrCode, Building, User, Calendar, Hash } from 'lucide-react'; +import QRCode from 'qrcode'; +import { useOrg } from '@components/Contexts/OrgContext'; +import { getOrgLogoMediaDirectory } from '@services/media/media'; + +interface CertificatePreviewProps { + certificationName: string; + certificationDescription: string; + certificationType: string; + certificatePattern: string; + certificateInstructor?: string; +} + +const CertificatePreview: React.FC = ({ + certificationName, + certificationDescription, + certificationType, + certificatePattern, + certificateInstructor +}) => { + const [qrCodeUrl, setQrCodeUrl] = useState(''); + const org = useOrg() as any; + + // Generate QR code + useEffect(() => { + const generateQRCode = async () => { + try { + const certificateData = `https://learnhouse.app/verify/LH-2024-001`; + const qrUrl = await QRCode.toDataURL(certificateData, { + width: 185, + margin: 1, + color: { + dark: '#000000', + light: '#FFFFFF' + }, + errorCorrectionLevel: 'M', + type: 'image/png' + }); + setQrCodeUrl(qrUrl); + } catch (error) { + console.error('Error generating QR code:', error); + } + }; + + generateQRCode(); + }, []); + // Function to get theme colors for each pattern + const getPatternTheme = (pattern: string) => { + switch (pattern) { + case 'royal': + return { + primary: 'text-amber-700', + secondary: 'text-amber-600', + icon: 'text-amber-600', + badge: 'bg-amber-50 text-amber-700 border-amber-200' + }; + case 'tech': + return { + primary: 'text-cyan-700', + secondary: 'text-cyan-600', + icon: 'text-cyan-600', + badge: 'bg-cyan-50 text-cyan-700 border-cyan-200' + }; + case 'nature': + return { + primary: 'text-green-700', + secondary: 'text-green-600', + icon: 'text-green-600', + badge: 'bg-green-50 text-green-700 border-green-200' + }; + case 'geometric': + return { + primary: 'text-purple-700', + secondary: 'text-purple-600', + icon: 'text-purple-600', + badge: 'bg-purple-50 text-purple-700 border-purple-200' + }; + case 'vintage': + return { + primary: 'text-orange-700', + secondary: 'text-orange-600', + icon: 'text-orange-600', + badge: 'bg-orange-50 text-orange-700 border-orange-200' + }; + case 'waves': + return { + primary: 'text-blue-700', + secondary: 'text-blue-600', + icon: 'text-blue-600', + badge: 'bg-blue-50 text-blue-700 border-blue-200' + }; + case 'minimal': + return { + primary: 'text-gray-700', + secondary: 'text-gray-600', + icon: 'text-gray-600', + badge: 'bg-gray-50 text-gray-700 border-gray-200' + }; + case 'professional': + return { + primary: 'text-slate-700', + secondary: 'text-slate-600', + icon: 'text-slate-600', + badge: 'bg-slate-50 text-slate-700 border-slate-200' + }; + case 'academic': + return { + primary: 'text-indigo-700', + secondary: 'text-indigo-600', + icon: 'text-indigo-600', + badge: 'bg-indigo-50 text-indigo-700 border-indigo-200' + }; + case 'modern': + return { + primary: 'text-blue-700', + secondary: 'text-blue-600', + icon: 'text-blue-600', + badge: 'bg-blue-50 text-blue-700 border-blue-200' + }; + default: + return { + primary: 'text-gray-700', + secondary: 'text-gray-600', + icon: 'text-gray-600', + badge: 'bg-gray-50 text-gray-700 border-gray-200' + }; + } + }; + + // Function to render different certificate patterns + const renderCertificatePattern = (pattern: string) => { + switch (pattern) { + case 'royal': + return ( + <> + {/* Royal ornate border with crown elements */} +
+
+ + {/* Crown-like decorations in corners */} +
+
+
+
+
+
+ + {/* Royal background pattern */} +
+
+
+ + ); + + case 'tech': + return ( + <> + {/* Tech circuit board borders */} +
+ + {/* Circuit-like corner elements */} +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + {/* Tech grid background */} +
+
+
+ + ); + + case 'nature': + return ( + <> + {/* Nature organic border */} +
+ + {/* Leaf-like decorations */} +
+
+ +
+
+ +
+
+ +
+
+ + {/* Organic background pattern */} +
+
+
+ + ); + + case 'geometric': + return ( + <> + {/* Geometric angular borders */} +
+ + {/* Geometric corner elements */} +
+
+
+
+ + {/* Abstract geometric shapes */} +
+
+
+
+ + {/* Geometric background */} +
+
+
+ + ); + + case 'vintage': + return ( + <> + {/* Art deco style borders */} +
+
+ + {/* Art deco corner decorations */} +
+
+
+
+ + {/* Art deco sunburst pattern */} +
+
+
+ + ); + + case 'waves': + return ( + <> + {/* Flowing wave borders */} +
+ + {/* Wave decorations */} +
+
+ + {/* Side wave patterns */} +
+
+ + {/* Wave background */} +
+
+
+ + ); + + case 'minimal': + return ( + <> + {/* Minimal clean border */} +
+ + {/* Subtle corner accents */} +
+
+
+
+ + ); + + case 'professional': + return ( + <> + {/* Professional double border */} +
+
+ + {/* Professional corner brackets */} +
+
+
+
+ + {/* Subtle professional background */} +
+
+
+ + ); + + case 'academic': + return ( + <> + {/* Academic traditional border */} +
+
+ + {/* Academic shield-like corners */} +
+
+
+
+ + {/* Academic laurel-like decorations */} +
+
+
+
+
+
+ + {/* Academic background pattern */} +
+
+
+ + ); + + case 'modern': + return ( + <> + {/* Modern clean asymmetric border */} +
+ + {/* Modern accent lines */} +
+
+ +
+
+ + {/* Modern dot accents */} +
+
+ + {/* Modern subtle background */} +
+
+
+ + ); + + default: + return null; + } + }; + + const theme = getPatternTheme(certificatePattern); + + return ( +
+
+ {/* Dynamic Certificate Pattern */} + {renderCertificatePattern(certificatePattern)} + + {/* Certificate ID - Top Left */} +
+
+ + ID: LH-2024-001 +
+
+ + {/* QR Code Box - Top Right */} +
+
+ {qrCodeUrl ? ( + Certificate QR Code + ) : ( +
+ +
+ )} +
+
+ + {/* Main Content */} +
+ {/* Header with decorative line */} +
+
+
Certificate
+
+
+ + {/* Award Icon with decorative elements */} +
+
+ + {/* Decorative rays */} +
+
+
+
+
+
+
+
+ + {/* Certificate Content */} +
+

+ {certificationName || 'Certification Name'} +

+

+ {certificationDescription || 'Certification description will appear here...'} +

+
+ + {/* Decorative divider */} +
+
+
+
+
+ + {/* Certification Type Badge */} +
+ + + {certificationType === 'completion' ? 'Course Completion' : + certificationType === 'achievement' ? 'Achievement Based' : + certificationType === 'assessment' ? 'Assessment Based' : + certificationType === 'participation' ? 'Participation' : + certificationType === 'mastery' ? 'Skill Mastery' : + certificationType === 'professional' ? 'Professional Development' : + certificationType === 'continuing' ? 'Continuing Education' : + certificationType === 'workshop' ? 'Workshop Attendance' : + certificationType === 'specialization' ? 'Specialization' : 'Course Completion'} + +
+
+ + {/* Bottom Section */} +
+
+ {/* Left: Teacher/Organization Signature */} +
+
+ + Instructor +
+
+ {certificateInstructor || 'Dr. Jane Smith'} +
+
+
+ + {/* Center: Logo */} +
+
+ {org?.logo_image ? ( + Organization Logo + ) : ( +
+ +
+ )} +
+
+ {org?.name || 'LearnHouse'} +
+
+ + {/* Right: Award Date */} +
+
+ + Awarded +
+
+ Dec 15, 2024 +
+
+
+
+
+
+ ); +}; + +export default CertificatePreview; \ No newline at end of file diff --git a/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/EditCourseCertification.tsx b/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/EditCourseCertification.tsx new file mode 100644 index 00000000..4e1b4722 --- /dev/null +++ b/apps/web/components/Dashboard/Pages/Course/EditCourseCertification/EditCourseCertification.tsx @@ -0,0 +1,366 @@ +import FormLayout, { + FormField, + FormLabelAndMessage, + Input, + Textarea, +} from '@components/Objects/StyledElements/Form/Form'; +import { useFormik } from 'formik'; +import { AlertTriangle, Award, CheckCircle, FileText, Settings } from 'lucide-react'; +import CertificatePreview from './CertificatePreview'; +import * as Form from '@radix-ui/react-form'; +import React, { useEffect, useState } from 'react'; +import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext'; +import { + CustomSelect, + CustomSelectContent, + CustomSelectItem, + CustomSelectTrigger, + CustomSelectValue, +} from "../EditCourseGeneral/CustomSelect"; + +type EditCourseCertificationProps = { + orgslug: string + course_uuid?: string +} + +const validate = (values: any) => { + const errors = {} as any; + + if (values.enable_certification && !values.certification_name) { + errors.certification_name = 'Required when certification is enabled'; + } else if (values.certification_name && values.certification_name.length > 100) { + errors.certification_name = 'Must be 100 characters or less'; + } + + if (values.enable_certification && !values.certification_description) { + errors.certification_description = 'Required when certification is enabled'; + } else if (values.certification_description && values.certification_description.length > 500) { + errors.certification_description = 'Must be 500 characters or less'; + } + + return errors; +}; + +function EditCourseCertification(props: EditCourseCertificationProps) { + const [error, setError] = useState(''); + const course = useCourse(); + const dispatchCourse = useCourseDispatch() as any; + const { isLoading, courseStructure } = course as any; + + // Create initial values object + const getInitialValues = () => { + // Helper function to get instructor name from authors + const getInstructorName = () => { + if (courseStructure?.authors && courseStructure.authors.length > 0) { + const author = courseStructure.authors[0]; + const firstName = author.first_name || ''; + const lastName = author.last_name || ''; + + // Only return if at least one name exists + if (firstName || lastName) { + return `${firstName} ${lastName}`.trim(); + } + } + return ''; + }; + + return { + enable_certification: courseStructure?.enable_certification || false, + certification_name: courseStructure?.certification_name || courseStructure?.name || '', + certification_description: courseStructure?.certification_description || courseStructure?.description || '', + certification_type: courseStructure?.certification_type || 'completion', + certificate_pattern: courseStructure?.certificate_pattern || 'professional', + certificate_instructor: courseStructure?.certificate_instructor || getInstructorName(), + }; + }; + + const formik = useFormik({ + initialValues: getInitialValues(), + validate, + onSubmit: async values => { + try { + // Add your submission logic here + dispatchCourse({ type: 'setIsSaved' }); + } catch (e) { + setError('Failed to save certification settings.'); + } + }, + enableReinitialize: true, + }) as any; + + // Reset form when courseStructure changes + useEffect(() => { + if (courseStructure && !isLoading) { + const newValues = getInitialValues(); + formik.resetForm({ values: newValues }); + } + }, [courseStructure, isLoading]); + + useEffect(() => { + if (!isLoading) { + const formikValues = formik.values as any; + const initialValues = formik.initialValues as any; + const valuesChanged = Object.keys(formikValues).some( + key => formikValues[key] !== initialValues[key] + ); + + if (valuesChanged) { + dispatchCourse({ type: 'setIsNotSaved' }); + const updatedCourse = { + ...courseStructure, + ...formikValues, + }; + dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse }); + } + } + }, [formik.values, isLoading]); + + if (isLoading || !courseStructure) { + return
Loading...
; + } + + return ( +
+ {courseStructure && ( +
+
+
+ {/* Header Section */} +
+
+

Course Certification

+

+ Enable and configure certificates for students who complete this course +

+
+
+ +
+
+ + {error && ( +
+ +
{error}
+
+ )} + + {/* Certification Configuration */} + {formik.values.enable_certification && ( +
+ {/* Form Section */} +
+ +
+ {/* Basic Information Section */} +
+

+ + Basic Information +

+

+ Configure the basic details of your certification +

+
+ +
+ {/* Certification Name */} + + + + + + + + {/* Certification Type */} + + + + { + if (!value) return; + formik.setFieldValue('certification_type', value); + }} + > + + + {formik.values.certification_type === 'completion' ? 'Course Completion' : + formik.values.certification_type === 'achievement' ? 'Achievement Based' : + formik.values.certification_type === 'assessment' ? 'Assessment Based' : + formik.values.certification_type === 'participation' ? 'Participation' : + formik.values.certification_type === 'mastery' ? 'Skill Mastery' : + formik.values.certification_type === 'professional' ? 'Professional Development' : + formik.values.certification_type === 'continuing' ? 'Continuing Education' : + formik.values.certification_type === 'workshop' ? 'Workshop Attendance' : + formik.values.certification_type === 'specialization' ? 'Specialization' : 'Course Completion'} + + + + Course Completion + Achievement Based + Assessment Based + Participation + Skill Mastery + Professional Development + Continuing Education + Workshop Attendance + Specialization + + + + +
+ + {/* Certification Description */} + + + +