feat(wip): initial ui and functionality for certifications

This commit is contained in:
swve 2025-07-14 21:45:58 +02:00
parent aabb4d190c
commit 86f7a80eb7
5 changed files with 1155 additions and 1 deletions

View file

@ -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<CertificatePreviewProps> = ({
certificationName,
certificationDescription,
certificationType,
certificatePattern,
certificateInstructor
}) => {
const [qrCodeUrl, setQrCodeUrl] = useState<string>('');
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 */}
<div className="absolute inset-3 border-4 border-amber-200 rounded-lg opacity-60"></div>
<div className="absolute inset-4 border-2 border-amber-300 rounded-md opacity-40"></div>
{/* Crown-like decorations in corners */}
<div className="absolute top-1 left-1/2 transform -translate-x-1/2">
<div className="w-8 h-4 bg-amber-200 opacity-50" style={{
clipPath: 'polygon(0% 100%, 20% 0%, 40% 100%, 60% 0%, 80% 100%, 100% 0%, 100% 100%)'
}}></div>
</div>
<div className="absolute bottom-1 left-1/2 transform -translate-x-1/2 rotate-180">
<div className="w-8 h-4 bg-amber-200 opacity-50" style={{
clipPath: 'polygon(0% 100%, 20% 0%, 40% 100%, 60% 0%, 80% 100%, 100% 0%, 100% 100%)'
}}></div>
</div>
{/* Royal background pattern */}
<div className="absolute inset-0 opacity-3">
<div className="w-full h-full" style={{
backgroundImage: `radial-gradient(circle at 25% 25%, #f59e0b 2px, transparent 2px), radial-gradient(circle at 75% 75%, #f59e0b 1px, transparent 1px)`,
backgroundSize: '16px 16px'
}}></div>
</div>
</>
);
case 'tech':
return (
<>
{/* Tech circuit board borders */}
<div className="absolute inset-3 border-2 border-cyan-200 opacity-50"></div>
{/* Circuit-like corner elements */}
<div className="absolute top-3 left-3 w-6 h-6 border-l-2 border-t-2 border-cyan-300 opacity-60"></div>
<div className="absolute top-3 left-5 w-2 h-2 bg-cyan-300 opacity-60"></div>
<div className="absolute top-5 left-3 w-2 h-2 bg-cyan-300 opacity-60"></div>
<div className="absolute top-3 right-3 w-6 h-6 border-r-2 border-t-2 border-cyan-300 opacity-60"></div>
<div className="absolute top-3 right-5 w-2 h-2 bg-cyan-300 opacity-60"></div>
<div className="absolute top-5 right-3 w-2 h-2 bg-cyan-300 opacity-60"></div>
<div className="absolute bottom-3 left-3 w-6 h-6 border-l-2 border-b-2 border-cyan-300 opacity-60"></div>
<div className="absolute bottom-3 left-5 w-2 h-2 bg-cyan-300 opacity-60"></div>
<div className="absolute bottom-5 left-3 w-2 h-2 bg-cyan-300 opacity-60"></div>
<div className="absolute bottom-3 right-3 w-6 h-6 border-r-2 border-b-2 border-cyan-300 opacity-60"></div>
<div className="absolute bottom-3 right-5 w-2 h-2 bg-cyan-300 opacity-60"></div>
<div className="absolute bottom-5 right-3 w-2 h-2 bg-cyan-300 opacity-60"></div>
{/* Tech grid background */}
<div className="absolute inset-0 opacity-4">
<div className="w-full h-full" style={{
backgroundImage: `linear-gradient(90deg, #06b6d4 1px, transparent 1px), linear-gradient(0deg, #06b6d4 1px, transparent 1px)`,
backgroundSize: '8px 8px'
}}></div>
</div>
</>
);
case 'nature':
return (
<>
{/* Nature organic border */}
<div className="absolute inset-3 border-2 border-green-200 rounded-2xl opacity-50"></div>
{/* Leaf-like decorations */}
<div className="absolute top-2 left-2 w-4 h-6 bg-green-200 opacity-50 rounded-full transform rotate-45"></div>
<div className="absolute top-2 left-4 w-3 h-4 bg-green-300 opacity-40 rounded-full transform rotate-12"></div>
<div className="absolute top-2 right-2 w-4 h-6 bg-green-200 opacity-50 rounded-full transform -rotate-45"></div>
<div className="absolute top-2 right-4 w-3 h-4 bg-green-300 opacity-40 rounded-full transform -rotate-12"></div>
<div className="absolute bottom-2 left-2 w-4 h-6 bg-green-200 opacity-50 rounded-full transform -rotate-45"></div>
<div className="absolute bottom-2 left-4 w-3 h-4 bg-green-300 opacity-40 rounded-full transform -rotate-12"></div>
<div className="absolute bottom-2 right-2 w-4 h-6 bg-green-200 opacity-50 rounded-full transform rotate-45"></div>
<div className="absolute bottom-2 right-4 w-3 h-4 bg-green-300 opacity-40 rounded-full transform rotate-12"></div>
{/* Organic background pattern */}
<div className="absolute inset-0 opacity-3">
<div className="w-full h-full" style={{
backgroundImage: `radial-gradient(ellipse at 30% 30%, #10b981 1px, transparent 1px), radial-gradient(ellipse at 70% 70%, #10b981 0.5px, transparent 0.5px)`,
backgroundSize: '12px 8px'
}}></div>
</div>
</>
);
case 'geometric':
return (
<>
{/* Geometric angular borders */}
<div className="absolute inset-2 border-2 border-purple-200 opacity-50" style={{
clipPath: 'polygon(0 10px, 10px 0, calc(100% - 10px) 0, 100% 10px, 100% calc(100% - 10px), calc(100% - 10px) 100%, 10px 100%, 0 calc(100% - 10px))'
}}></div>
{/* Geometric corner elements */}
<div className="absolute top-1 left-1 w-6 h-6 border-2 border-purple-300 opacity-60 transform rotate-45"></div>
<div className="absolute top-1 right-1 w-6 h-6 border-2 border-purple-300 opacity-60 transform rotate-45"></div>
<div className="absolute bottom-1 left-1 w-6 h-6 border-2 border-purple-300 opacity-60 transform rotate-45"></div>
<div className="absolute bottom-1 right-1 w-6 h-6 border-2 border-purple-300 opacity-60 transform rotate-45"></div>
{/* Abstract geometric shapes */}
<div className="absolute top-1/4 left-1 w-2 h-8 bg-purple-200 opacity-30 transform rotate-12"></div>
<div className="absolute top-1/4 right-1 w-2 h-8 bg-purple-200 opacity-30 transform -rotate-12"></div>
<div className="absolute bottom-1/4 left-1 w-2 h-8 bg-purple-200 opacity-30 transform -rotate-12"></div>
<div className="absolute bottom-1/4 right-1 w-2 h-8 bg-purple-200 opacity-30 transform rotate-12"></div>
{/* Geometric background */}
<div className="absolute inset-0 opacity-4">
<div className="w-full h-full" style={{
backgroundImage: `linear-gradient(45deg, #8b5cf6 25%, transparent 25%), linear-gradient(-45deg, #8b5cf6 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #8b5cf6 75%), linear-gradient(-45deg, transparent 75%, #8b5cf6 75%)`,
backgroundSize: '6px 6px'
}}></div>
</div>
</>
);
case 'vintage':
return (
<>
{/* Art deco style borders */}
<div className="absolute inset-2 border-2 border-orange-200 opacity-50"></div>
<div className="absolute inset-3 border border-orange-300 opacity-40"></div>
{/* Art deco corner decorations */}
<div className="absolute top-2 left-2 w-8 h-8 border-2 border-orange-300 opacity-50" style={{
clipPath: 'polygon(0 0, 100% 0, 100% 50%, 50% 100%, 0 100%)'
}}></div>
<div className="absolute top-2 right-2 w-8 h-8 border-2 border-orange-300 opacity-50" style={{
clipPath: 'polygon(0 0, 100% 0, 100% 100%, 50% 100%, 0 50%)'
}}></div>
<div className="absolute bottom-2 left-2 w-8 h-8 border-2 border-orange-300 opacity-50" style={{
clipPath: 'polygon(0 0, 50% 0, 100% 50%, 100% 100%, 0 100%)'
}}></div>
<div className="absolute bottom-2 right-2 w-8 h-8 border-2 border-orange-300 opacity-50" style={{
clipPath: 'polygon(0 50%, 50% 0, 100% 0, 100% 100%, 0 100%)'
}}></div>
{/* Art deco sunburst pattern */}
<div className="absolute inset-0 opacity-3">
<div className="w-full h-full" style={{
backgroundImage: `repeating-conic-gradient(from 0deg at 50% 50%, #f97316 0deg, #f97316 2deg, transparent 2deg, transparent 8deg)`,
backgroundSize: '100% 100%'
}}></div>
</div>
</>
);
case 'waves':
return (
<>
{/* Flowing wave borders */}
<div className="absolute inset-2 border-2 border-blue-200 rounded-3xl opacity-50"></div>
{/* Wave decorations */}
<div className="absolute top-2 left-0 right-0 h-4 opacity-30" style={{
background: `radial-gradient(ellipse at center, #3b82f6 30%, transparent 30%)`,
backgroundSize: '20px 8px'
}}></div>
<div className="absolute bottom-2 left-0 right-0 h-4 opacity-30" style={{
background: `radial-gradient(ellipse at center, #3b82f6 30%, transparent 30%)`,
backgroundSize: '20px 8px'
}}></div>
{/* Side wave patterns */}
<div className="absolute left-2 top-0 bottom-0 w-4 opacity-30" style={{
background: `radial-gradient(ellipse at center, #3b82f6 30%, transparent 30%)`,
backgroundSize: '8px 20px'
}}></div>
<div className="absolute right-2 top-0 bottom-0 w-4 opacity-30" style={{
background: `radial-gradient(ellipse at center, #3b82f6 30%, transparent 30%)`,
backgroundSize: '8px 20px'
}}></div>
{/* Wave background */}
<div className="absolute inset-0 opacity-4">
<div className="w-full h-full" style={{
backgroundImage: `repeating-linear-gradient(45deg, #3b82f6 0px, #3b82f6 1px, transparent 1px, transparent 8px), repeating-linear-gradient(-45deg, #3b82f6 0px, #3b82f6 1px, transparent 1px, transparent 8px)`,
backgroundSize: '12px 12px'
}}></div>
</div>
</>
);
case 'minimal':
return (
<>
{/* Minimal clean border */}
<div className="absolute inset-6 border border-gray-300 opacity-60"></div>
{/* Subtle corner accents */}
<div className="absolute top-5 left-5 w-3 h-3 border-l border-t border-gray-400 opacity-40"></div>
<div className="absolute top-5 right-5 w-3 h-3 border-r border-t border-gray-400 opacity-40"></div>
<div className="absolute bottom-5 left-5 w-3 h-3 border-l border-b border-gray-400 opacity-40"></div>
<div className="absolute bottom-5 right-5 w-3 h-3 border-r border-b border-gray-400 opacity-40"></div>
</>
);
case 'professional':
return (
<>
{/* Professional double border */}
<div className="absolute inset-2 border-2 border-slate-300 opacity-50"></div>
<div className="absolute inset-3 border border-slate-400 opacity-40"></div>
{/* Professional corner brackets */}
<div className="absolute top-2 left-2 w-6 h-6 border-l-2 border-t-2 border-slate-400 opacity-60"></div>
<div className="absolute top-2 right-2 w-6 h-6 border-r-2 border-t-2 border-slate-400 opacity-60"></div>
<div className="absolute bottom-2 left-2 w-6 h-6 border-l-2 border-b-2 border-slate-400 opacity-60"></div>
<div className="absolute bottom-2 right-2 w-6 h-6 border-r-2 border-b-2 border-slate-400 opacity-60"></div>
{/* Subtle professional background */}
<div className="absolute inset-0 opacity-2">
<div className="w-full h-full" style={{
backgroundImage: `linear-gradient(90deg, #64748b 1px, transparent 1px), linear-gradient(0deg, #64748b 1px, transparent 1px)`,
backgroundSize: '20px 20px'
}}></div>
</div>
</>
);
case 'academic':
return (
<>
{/* Academic traditional border */}
<div className="absolute inset-2 border-3 border-indigo-300 opacity-50"></div>
<div className="absolute inset-3 border border-indigo-400 opacity-40"></div>
{/* Academic shield-like corners */}
<div className="absolute top-2 left-2 w-8 h-8 border-2 border-indigo-400 opacity-50 rounded-tl-lg"></div>
<div className="absolute top-2 right-2 w-8 h-8 border-2 border-indigo-400 opacity-50 rounded-tr-lg"></div>
<div className="absolute bottom-2 left-2 w-8 h-8 border-2 border-indigo-400 opacity-50 rounded-bl-lg"></div>
<div className="absolute bottom-2 right-2 w-8 h-8 border-2 border-indigo-400 opacity-50 rounded-br-lg"></div>
{/* Academic laurel-like decorations */}
<div className="absolute top-1/2 left-1 transform -translate-y-1/2">
<div className="w-1 h-6 bg-indigo-300 opacity-40 rounded-full"></div>
</div>
<div className="absolute top-1/2 right-1 transform -translate-y-1/2">
<div className="w-1 h-6 bg-indigo-300 opacity-40 rounded-full"></div>
</div>
{/* Academic background pattern */}
<div className="absolute inset-0 opacity-3">
<div className="w-full h-full" style={{
backgroundImage: `radial-gradient(circle at 50% 50%, #6366f1 1px, transparent 1px)`,
backgroundSize: '15px 15px'
}}></div>
</div>
</>
);
case 'modern':
return (
<>
{/* Modern clean asymmetric border */}
<div className="absolute inset-2 border-2 border-gray-300 opacity-50" style={{
clipPath: 'polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px))'
}}></div>
{/* Modern accent lines */}
<div className="absolute top-2 left-2 w-8 h-0.5 bg-blue-400 opacity-60"></div>
<div className="absolute top-2 left-2 w-0.5 h-8 bg-blue-400 opacity-60"></div>
<div className="absolute bottom-2 right-2 w-8 h-0.5 bg-blue-400 opacity-60"></div>
<div className="absolute bottom-2 right-2 w-0.5 h-8 bg-blue-400 opacity-60"></div>
{/* Modern dot accents */}
<div className="absolute top-4 right-4 w-2 h-2 bg-blue-400 opacity-50 rounded-full"></div>
<div className="absolute bottom-4 left-4 w-2 h-2 bg-blue-400 opacity-50 rounded-full"></div>
{/* Modern subtle background */}
<div className="absolute inset-0 opacity-2">
<div className="w-full h-full" style={{
backgroundImage: `linear-gradient(135deg, #3b82f6 0%, transparent 1%), linear-gradient(225deg, #3b82f6 0%, transparent 1%)`,
backgroundSize: '12px 12px'
}}></div>
</div>
</>
);
default:
return null;
}
};
const theme = getPatternTheme(certificatePattern);
return (
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 border border-blue-200 rounded-xl p-4 w-full h-full">
<div className="bg-white rounded-lg shadow-sm p-6 relative overflow-hidden w-full h-full flex flex-col">
{/* Dynamic Certificate Pattern */}
{renderCertificatePattern(certificatePattern)}
{/* Certificate ID - Top Left */}
<div className="absolute top-4 left-4 sm:top-6 sm:left-6 z-20">
<div className="flex items-center space-x-1">
<Hash className={`w-3 h-3 sm:w-4 sm:h-4 ${theme.icon}`} />
<span className={`text-xs sm:text-sm ${theme.secondary} font-medium`}>ID: LH-2024-001</span>
</div>
</div>
{/* QR Code Box - Top Right */}
<div className="absolute top-4 right-4 sm:top-6 sm:right-6 z-20">
<div className={`w-16 h-16 sm:w-24 sm:h-24 border-2 ${theme.secondary.replace('text-', 'border-')} rounded-md bg-white/90 backdrop-blur-sm p-1`}>
{qrCodeUrl ? (
<img
src={qrCodeUrl}
alt="Certificate QR Code"
className="w-full h-full object-contain"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<QrCode className={`w-8 h-8 sm:w-12 sm:h-12 ${theme.icon}`} />
</div>
)}
</div>
</div>
{/* Main Content */}
<div className="relative z-10 flex-1 flex flex-col items-center justify-center text-center space-y-3 px-6 py-6">
{/* Header with decorative line */}
<div className="flex items-center justify-center space-x-2 mb-2">
<div className={`w-6 sm:w-8 h-px bg-gradient-to-r from-transparent ${theme.secondary.replace('text-', 'to-')}`}></div>
<div className={`text-xs sm:text-sm ${theme.secondary} font-medium uppercase tracking-wider`}>Certificate</div>
<div className={`w-6 sm:w-8 h-px bg-gradient-to-l from-transparent ${theme.secondary.replace('text-', 'to-')}`}></div>
</div>
{/* Award Icon with decorative elements */}
<div className="flex justify-center relative">
<div className={`w-12 h-12 sm:w-16 sm:h-16 bg-gradient-to-br ${theme.icon.replace('text-', 'from-')}-100 ${theme.icon.replace('text-', 'to-')}-200 rounded-full flex items-center justify-center relative`}>
<Award className={`w-6 h-6 sm:w-8 sm:h-8 ${theme.icon}`} />
{/* Decorative rays */}
<div className="absolute inset-0 rounded-full">
<div className={`absolute top-0 left-1/2 w-px h-2 sm:h-3 ${theme.secondary.replace('text-', 'bg-')} transform -translate-x-1/2 -translate-y-1 opacity-60`}></div>
<div className={`absolute bottom-0 left-1/2 w-px h-2 sm:h-3 ${theme.secondary.replace('text-', 'bg-')} transform -translate-x-1/2 translate-y-1 opacity-60`}></div>
<div className={`absolute left-0 top-1/2 w-2 sm:w-3 h-px ${theme.secondary.replace('text-', 'bg-')} transform -translate-y-1/2 -translate-x-1 opacity-60`}></div>
<div className={`absolute right-0 top-1/2 w-2 sm:w-3 h-px ${theme.secondary.replace('text-', 'bg-')} transform -translate-y-1/2 translate-x-1 opacity-60`}></div>
</div>
</div>
</div>
{/* Certificate Content */}
<div className="flex flex-col justify-center items-center flex-1 max-w-full">
<h4 className={`font-bold text-sm sm:text-base ${theme.primary} mb-2 text-center`}>
{certificationName || 'Certification Name'}
</h4>
<p className={`text-xs sm:text-sm ${theme.secondary} text-center leading-relaxed max-w-xs sm:max-w-sm`}>
{certificationDescription || 'Certification description will appear here...'}
</p>
</div>
{/* Decorative divider */}
<div className="flex items-center justify-center space-x-1 py-1">
<div className={`w-2 h-px ${theme.secondary.replace('text-', 'bg-')} opacity-50`}></div>
<div className={`w-1 h-1 ${theme.primary.replace('text-', 'bg-')} rounded-full opacity-60`}></div>
<div className={`w-2 h-px ${theme.secondary.replace('text-', 'bg-')} opacity-50`}></div>
</div>
{/* Certification Type Badge */}
<div className={`inline-flex items-center space-x-1 text-xs sm:text-sm ${theme.badge} px-3 py-1 rounded-full border`}>
<CheckCircle size={12} />
<span className="font-medium">
{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'}
</span>
</div>
</div>
{/* Bottom Section */}
<div className="relative z-10 mt-auto p-6 pt-8">
<div className="flex items-end justify-between w-full">
{/* Left: Teacher/Organization Signature */}
<div className="flex flex-col items-start space-y-1 flex-1">
<div className="flex items-center space-x-1">
<User className={`w-2.5 h-2.5 sm:w-3 sm:h-3 ${theme.icon}`} />
<span className={`text-xs ${theme.secondary} font-medium`}>Instructor</span>
</div>
<div className={`text-xs ${theme.primary} font-semibold`}>
{certificateInstructor || 'Dr. Jane Smith'}
</div>
<div className={`h-px w-10 sm:w-12 ${theme.secondary.replace('text-', 'bg-')} opacity-50`}></div>
</div>
{/* Center: Logo */}
<div className="flex flex-col items-center space-y-1 flex-1">
<div className={`w-8 h-8 sm:w-10 sm:h-10 flex items-center justify-center`}>
{org?.logo_image ? (
<img
src={`${getOrgLogoMediaDirectory(org.org_uuid, org?.logo_image)}`}
alt="Organization Logo"
className="w-full h-full object-contain"
/>
) : (
<div className={`w-full h-full ${theme.icon.replace('text-', 'bg-')}-100 rounded-full flex items-center justify-center`}>
<Building className={`w-4 h-4 sm:w-5 sm:h-5 ${theme.icon}`} />
</div>
)}
</div>
<div className={`text-xs ${theme.secondary} font-medium`}>
{org?.name || 'LearnHouse'}
</div>
</div>
{/* Right: Award Date */}
<div className="flex flex-col items-end space-y-1 flex-1">
<div className="flex items-center space-x-1">
<Calendar className={`w-2.5 h-2.5 sm:w-3 sm:h-3 ${theme.icon}`} />
<span className={`text-xs ${theme.secondary} font-medium`}>Awarded</span>
</div>
<div className={`text-xs ${theme.primary} font-semibold`}>
Dec 15, 2024
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default CertificatePreview;

View file

@ -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 <div>Loading...</div>;
}
return (
<div>
{courseStructure && (
<div>
<div className="h-6"></div>
<div className="mx-4 sm:mx-10 bg-white rounded-xl shadow-xs px-4 py-4">
{/* Header Section */}
<div className="flex items-center justify-between bg-gray-50 px-3 sm:px-5 py-3 rounded-md mb-3">
<div className="flex flex-col -space-y-1">
<h1 className="font-bold text-lg sm:text-xl text-gray-800">Course Certification</h1>
<h2 className="text-gray-500 text-xs sm:text-sm">
Enable and configure certificates for students who complete this course
</h2>
</div>
<div className="flex items-center space-x-3">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={formik.values.enable_certification}
onChange={(e) => formik.setFieldValue('enable_certification', e.target.checked)}
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label>
</div>
</div>
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 mb-6 transition-all shadow-xs">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
{/* Certification Configuration */}
{formik.values.enable_certification && (
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
{/* Form Section */}
<div className="lg:col-span-3">
<FormLayout onSubmit={formik.handleSubmit}>
<div className="space-y-6">
{/* Basic Information Section */}
<div className="flex flex-col bg-gray-50 -space-y-1 px-3 sm:px-5 py-3 rounded-md mb-3">
<h3 className="font-bold text-md text-gray-800 flex items-center gap-2">
<FileText size={16} />
Basic Information
</h3>
<p className="text-gray-500 text-xs sm:text-sm">
Configure the basic details of your certification
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Certification Name */}
<FormField name="certification_name">
<FormLabelAndMessage
label="Certification Name"
message={formik.errors.certification_name}
/>
<Form.Control asChild>
<Input
style={{ backgroundColor: 'white' }}
onChange={formik.handleChange}
value={formik.values.certification_name}
type="text"
placeholder="e.g., Advanced JavaScript Certification"
required
/>
</Form.Control>
</FormField>
{/* Certification Type */}
<FormField name="certification_type">
<FormLabelAndMessage label="Certification Type" />
<Form.Control asChild>
<CustomSelect
value={formik.values.certification_type}
onValueChange={(value) => {
if (!value) return;
formik.setFieldValue('certification_type', value);
}}
>
<CustomSelectTrigger className="w-full bg-white">
<CustomSelectValue>
{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'}
</CustomSelectValue>
</CustomSelectTrigger>
<CustomSelectContent>
<CustomSelectItem value="completion">Course Completion</CustomSelectItem>
<CustomSelectItem value="achievement">Achievement Based</CustomSelectItem>
<CustomSelectItem value="assessment">Assessment Based</CustomSelectItem>
<CustomSelectItem value="participation">Participation</CustomSelectItem>
<CustomSelectItem value="mastery">Skill Mastery</CustomSelectItem>
<CustomSelectItem value="professional">Professional Development</CustomSelectItem>
<CustomSelectItem value="continuing">Continuing Education</CustomSelectItem>
<CustomSelectItem value="workshop">Workshop Attendance</CustomSelectItem>
<CustomSelectItem value="specialization">Specialization</CustomSelectItem>
</CustomSelectContent>
</CustomSelect>
</Form.Control>
</FormField>
</div>
{/* Certification Description */}
<FormField name="certification_description">
<FormLabelAndMessage
label="Certification Description"
message={formik.errors.certification_description}
/>
<Form.Control asChild>
<Textarea
style={{ backgroundColor: 'white', height: '120px', minHeight: '120px' }}
onChange={formik.handleChange}
value={formik.values.certification_description}
placeholder="Describe what this certification represents and its value..."
required
/>
</Form.Control>
</FormField>
{/* Certificate Design Section */}
<div className="flex flex-col bg-gray-50 -space-y-1 px-3 sm:px-5 py-3 rounded-md mb-3">
<h3 className="font-bold text-md text-gray-800 flex items-center gap-2">
<Award size={16} />
Certificate Design
</h3>
<p className="text-gray-500 text-xs sm:text-sm">
Choose a decorative pattern for your certificate
</p>
</div>
{/* Pattern Selection */}
<FormField name="certificate_pattern">
<FormLabelAndMessage label="Certificate Pattern" />
<Form.Control asChild>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3">
{[
{ value: 'royal', name: 'Royal', description: 'Ornate with crown motifs' },
{ value: 'tech', name: 'Tech', description: 'Circuit-inspired patterns' },
{ value: 'nature', name: 'Nature', description: 'Organic leaf patterns' },
{ value: 'geometric', name: 'Geometric', description: 'Abstract shapes & lines' },
{ value: 'vintage', name: 'Vintage', description: 'Art deco styling' },
{ value: 'waves', name: 'Waves', description: 'Flowing water patterns' },
{ value: 'minimal', name: 'Minimal', description: 'Clean and simple' },
{ value: 'professional', name: 'Professional', description: 'Business-ready design' },
{ value: 'academic', name: 'Academic', description: 'Traditional university style' },
{ value: 'modern', name: 'Modern', description: 'Contemporary clean lines' }
].map((pattern) => (
<div
key={pattern.value}
className={`p-3 border-2 rounded-lg cursor-pointer transition-all ${
formik.values.certificate_pattern === pattern.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => formik.setFieldValue('certificate_pattern', pattern.value)}
>
<div className="text-center">
<div className="text-sm font-medium text-gray-900">{pattern.name}</div>
<div className="text-xs text-gray-500 mt-1">{pattern.description}</div>
</div>
</div>
))}
</div>
</Form.Control>
</FormField>
{/* Custom Instructor */}
<FormField name="certificate_instructor">
<FormLabelAndMessage label="Instructor Name (Optional)" />
<Form.Control asChild>
<Input
style={{ backgroundColor: 'white' }}
onChange={formik.handleChange}
value={formik.values.certificate_instructor}
type="text"
placeholder="e.g., Dr. Jane Smith"
/>
</Form.Control>
</FormField>
</div>
</FormLayout>
</div>
{/* Preview Section */}
<div className="lg:col-span-2">
<div className="bg-white rounded-xl shadow-xs border border-gray-200 sticky top-6 min-h-[320px]">
<div className="flex flex-col bg-gray-50 -space-y-1 px-3 sm:px-5 py-3 rounded-t-xl mb-3">
<h3 className="font-bold text-md text-gray-800 flex items-center gap-2">
<Award size={16} />
Certificate Preview
</h3>
<p className="text-gray-500 text-xs sm:text-sm">
Live preview of your certificate
</p>
</div>
<div className="p-4">
<CertificatePreview
certificationName={formik.values.certification_name}
certificationDescription={formik.values.certification_description}
certificationType={formik.values.certification_type}
certificatePattern={formik.values.certificate_pattern}
certificateInstructor={formik.values.certificate_instructor}
/>
</div>
</div>
</div>
</div>
)}
{/* Disabled State */}
{!formik.values.enable_certification && (
<div className="bg-gray-50 border border-gray-200 rounded-lg p-8 text-center">
<Award className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="font-medium text-gray-700 mb-2">No Certification Configured</h3>
<p className="text-sm text-gray-500 mb-4">
Enable certification to provide students with certificates upon course completion.
</p>
<button
type="button"
onClick={() => formik.setFieldValue('enable_certification', true)}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<Award size={16} />
Enable Certification
</button>
</div>
)}
</div>
</div>
)}
</div>
);
}
export default EditCourseCertification;