lms/resources/js/components/dynamic-certificate.tsx
Ahmed Darrazi 5b4470a323
All checks were successful
Build & Push Docker Image / docker (push) Successful in 2m9s
lang bugfix
2025-12-19 00:11:10 +01:00

407 lines
14 KiB
TypeScript

import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import jsPDF from 'jspdf';
import { Award, Calendar, Download, FileImage, FileText } from 'lucide-react';
import { useRef, useState } from 'react';
import { toast } from 'sonner';
interface DynamicCertificateProps {
template: CertificateTemplate;
courseName: string;
studentName: string;
completionDate: string;
}
const DynamicCertificate = ({ template, courseName, studentName, completionDate }: DynamicCertificateProps) => {
const [downloadFormat, setDownloadFormat] = useState('png');
const certificateRef = useRef<HTMLDivElement>(null);
const dimensions = { width: 900, height: 600 };
const { template_data } = template;
const handleDownloadCertificate = async () => {
if (!certificateRef.current) return;
if (downloadFormat === 'pdf') {
await downloadAsPDF();
} else {
await downloadAsPNG();
}
};
const loadImage = (src: string): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
};
const downloadAsPNG = async () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return;
canvas.width = dimensions.width;
canvas.height = dimensions.height;
// Load logo first if it exists
let logoImage: HTMLImageElement | null = null;
if (template.logo_path) {
try {
logoImage = await loadImage(template.logo_path);
} catch (error) {
console.error('Failed to load logo:', error);
}
}
await drawCertificate(ctx, dimensions, logoImage);
canvas.toBlob((blob) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${studentName}_${courseName}_Zertifikat.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Zertifikat als PNG gespeichert!');
}, 'image/png');
};
const downloadAsPDF = async () => {
const isLandscape = dimensions.width > dimensions.height;
const pdf = new jsPDF({
orientation: isLandscape ? 'landscape' : 'portrait',
unit: 'px',
format: [dimensions.width, dimensions.height],
});
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return;
canvas.width = dimensions.width;
canvas.height = dimensions.height;
// Load logo first if it exists
let logoImage: HTMLImageElement | null = null;
if (template.logo_path) {
try {
logoImage = await loadImage(template.logo_path);
} catch (error) {
console.error('Failed to load logo:', error);
}
}
await drawCertificate(ctx, dimensions, logoImage);
const imgData = canvas.toDataURL('image/png');
pdf.addImage(imgData, 'PNG', 0, 0, dimensions.width, dimensions.height);
pdf.save(`${studentName}_${courseName}_Certificate.pdf`);
toast.success('Zertifikat als PDF gespeichert!');
};
const wrapText = (ctx: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number, lineHeight: number) => {
const words = text.split(' ');
let line = '';
let testLine = '';
const lines = [];
for (let n = 0; n < words.length; n++) {
testLine += `${words[n]} `;
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
lines.push({ text: line.trim(), width: ctx.measureText(line).width });
testLine = `${words[n]} `;
line = `${words[n]} `;
} else {
line = testLine;
}
}
lines.push({ text: line.trim(), width: ctx.measureText(line).width });
let currentY = y;
lines.forEach((lineObj) => {
ctx.fillText(lineObj.text, x, currentY);
currentY += lineHeight;
});
return currentY;
};
const drawCertificate = async (
ctx: CanvasRenderingContext2D,
dimensions: { width: number; height: number },
logoImage: HTMLImageElement | null = null,
) => {
// Background
ctx.fillStyle = template_data.backgroundColor;
ctx.fillRect(0, 0, dimensions.width, dimensions.height);
// Outer border
ctx.strokeStyle = template_data.borderColor;
ctx.lineWidth = 8;
ctx.strokeRect(20, 20, dimensions.width - 40, dimensions.height - 40);
// Inner border
ctx.strokeStyle = template_data.primaryColor;
ctx.lineWidth = 2;
ctx.strokeRect(40, 40, dimensions.width - 80, dimensions.height - 80);
// Set text align
ctx.textAlign = 'center';
let currentY = 100;
// Draw logo if exists
if (logoImage) {
const logoSize = 80;
const logoX = (dimensions.width - logoSize) / 2;
const logoY = 60;
ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize);
currentY = logoY + logoSize + 30; // Position text below logo
}
// Title
ctx.font = `bold 42px ${template_data.fontFamily}`;
ctx.fillStyle = template_data.primaryColor;
ctx.fillText(template_data.titleText, dimensions.width / 2, currentY);
currentY += 20;
// Decorative line under title
ctx.strokeStyle = template_data.borderColor;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(dimensions.width / 2 - 150, currentY);
ctx.lineTo(dimensions.width / 2 + 150, currentY);
ctx.stroke();
currentY += 50;
// Description text
ctx.font = `22px ${template_data.fontFamily}`;
ctx.fillStyle = template_data.secondaryColor;
currentY = wrapText(ctx, template_data.descriptionText, dimensions.width / 2, currentY, dimensions.width - 100, 30);
currentY += 50;
// Student name
ctx.font = `bold 36px ${template_data.fontFamily}`;
ctx.fillStyle = template_data.primaryColor;
ctx.fillText(studentName, dimensions.width / 2, currentY);
// Underline for student name
const nameWidth = ctx.measureText(studentName).width;
ctx.strokeStyle = template_data.borderColor;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo((dimensions.width - nameWidth) / 2 - 20, currentY + 10);
ctx.lineTo((dimensions.width + nameWidth) / 2 + 20, currentY + 10);
ctx.stroke();
currentY += 60;
// Completion text
ctx.font = `22px ${template_data.fontFamily}`;
ctx.fillStyle = template_data.secondaryColor;
ctx.fillText(template_data.completionText, dimensions.width / 2, currentY);
currentY += 50;
// Course name
ctx.font = `bold 28px ${template_data.fontFamily}`;
ctx.fillStyle = template_data.primaryColor;
ctx.fillText(courseName, dimensions.width / 2, currentY);
currentY += 60;
// Completion date
ctx.font = `18px ${template_data.fontFamily}`;
ctx.fillStyle = template_data.secondaryColor;
ctx.fillText(`Abgeschlossen am: ${completionDate}`, dimensions.width / 2, currentY);
currentY += 60;
// Footer
ctx.font = `16px ${template_data.fontFamily}`;
ctx.fillStyle = template_data.secondaryColor;
ctx.fillText(template_data.footerText, dimensions.width / 2, dimensions.height - 60);
};
return (
<Card className="mx-auto max-w-[800px] space-y-7 p-6">
<div
ref={certificateRef}
className="relative flex flex-col justify-center rounded-lg border-4 p-8 text-center"
style={{
backgroundColor: template_data.backgroundColor,
borderColor: template_data.borderColor,
fontFamily: template_data.fontFamily,
}}
>
{/* Inner decorative border */}
<div
className="absolute inset-4 rounded border-2"
style={{
borderColor: template_data.primaryColor,
}}
></div>
<div className="relative z-10">
{/* Logo or Award Icon */}
{template.logo_path ? (
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center">
<img src={template.logo_path} alt="Logo" className="h-full w-full object-contain" />
</div>
) : (
<Award
className="mx-auto mb-3 h-12 w-12"
style={{
color: template_data.borderColor,
}}
/>
)}
{/* Title */}
<div className="mb-6">
<h2
className="mb-2 font-serif text-2xl font-bold"
style={{
color: template_data.primaryColor,
}}
>
{template_data.titleText}
</h2>
<div
className="mx-auto h-0.5 w-32"
style={{
backgroundColor: template_data.borderColor,
}}
></div>
</div>
{/* Description */}
<div className="space-y-4">
<p
className="font-serif text-lg"
style={{
color: template_data.secondaryColor,
}}
>
{template_data.descriptionText}
</p>
{/* Student Name */}
<div className="relative">
<p
className="mx-8 pb-2 font-serif text-2xl font-bold"
style={{
color: template_data.primaryColor,
}}
>
{studentName}
</p>
<div
className="absolute bottom-0 left-1/2 h-0.5 w-48 -translate-x-1/2 transform"
style={{
backgroundColor: template_data.borderColor,
}}
></div>
</div>
{/* Completion Text */}
<p
className="font-serif text-lg"
style={{
color: template_data.secondaryColor,
}}
>
{template_data.completionText}
</p>
{/* Course Name */}
<p
className="font-serif text-xl font-semibold"
style={{
color: template_data.primaryColor,
}}
>
{courseName}
</p>
{/* Completion Date */}
<div className="mt-6 flex items-center justify-center gap-2">
<Calendar
className="h-4 w-4"
style={{
color: template_data.secondaryColor,
}}
/>
<p
className="text-muted-foreground font-serif text-sm"
style={{
color: template_data.secondaryColor,
}}
>
Abgeschlossen am: {completionDate}
</p>
</div>
</div>
{/* Footer */}
<div
className="mt-6 border-t pt-4"
style={{
borderColor: template_data.borderColor,
}}
>
<p
className="font-serif text-sm"
style={{
color: template_data.secondaryColor,
}}
>
{template_data.footerText}
</p>
</div>
</div>
</div>
<div className="space-y-4">
<RadioGroup value={downloadFormat} onValueChange={setDownloadFormat} className="flex justify-center space-x-6">
<div className="flex items-center space-x-2">
<RadioGroupItem className="cursor-pointer" value="png" id="png" />
<Label htmlFor="png" className="flex cursor-pointer items-center gap-2">
<FileImage className="h-4 w-4" />
PNG-Bild
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem className="cursor-pointer" value="pdf" id="pdf" />
<Label htmlFor="pdf" className="flex cursor-pointer items-center gap-2">
<FileText className="h-4 w-4" />
PDF-Dokument
</Label>
</div>
</RadioGroup>
<Button variant="outline" className="w-full" onClick={handleDownloadCertificate}>
<Download className="mr-2 h-4 w-4" />
Herunterladen als {downloadFormat.toUpperCase()}
</Button>
</div>
</Card>
);
};
export default DynamicCertificate;