diff --git a/app/components/MarkdownModal.tsx b/app/components/MarkdownModal.tsx index f76a65a..7a3cbbe 100644 --- a/app/components/MarkdownModal.tsx +++ b/app/components/MarkdownModal.tsx @@ -1,15 +1,126 @@ // components/MarkdownModal.tsx -import { X } from "lucide-react"; +import { X, Download } from "lucide-react"; +import { jsPDF } from "jspdf"; +import { useState } from "react"; interface HtmlModalProps { - content: string | null; + content: string[] | null; onClose: () => void; } +const renderHtmlOnPdfPage = (pdf: jsPDF, htmlContent: string) => { + const pageHeight = pdf.internal.pageSize.getHeight(); + const pageWidth = pdf.internal.pageSize.getWidth(); + const margin = 15; + let cursorY = margin; + + const container = document.createElement("div"); + container.innerHTML = htmlContent; + + const processNode = ( + node: ChildNode, + currentStyle: { fontStyle: "normal" | "bold" | "italic"; fontSize: number } + ) => { + if (cursorY > pageHeight - margin) { + pdf.addPage(); + cursorY = margin; + } + + const nextStyle = { ...currentStyle }; + let spacingAfter = 0; + + switch (node.nodeName) { + case "H1": + nextStyle.fontSize = 22; + nextStyle.fontStyle = "bold"; + spacingAfter = 8; + break; + case "H2": + nextStyle.fontSize = 18; + nextStyle.fontStyle = "bold"; + spacingAfter = 6; + break; + case "H3": + nextStyle.fontSize = 14; + nextStyle.fontStyle = "bold"; + spacingAfter = 4; + break; + case "P": + nextStyle.fontSize = 10; + nextStyle.fontStyle = "normal"; + spacingAfter = 4; + break; + case "UL": + spacingAfter = 4; + break; + case "STRONG": + case "B": + nextStyle.fontStyle = "bold"; + break; + } + + if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) { + pdf.setFontSize(nextStyle.fontSize); + pdf.setFont("Helvetica", nextStyle.fontStyle); + const textLines = node.textContent.split("\n"); + textLines.forEach((line) => { + if (line.trim() === "") { + cursorY += nextStyle.fontSize * 0.4; + return; + } + const splitText = pdf.splitTextToSize(line, pageWidth - margin * 2); + pdf.text(splitText, margin, cursorY); + cursorY += splitText.length * nextStyle.fontSize * 0.4; + }); + } else if (node.nodeName === "UL") { + Array.from((node as HTMLUListElement).children).forEach((li) => { + const liText = `• ${li.textContent?.trim() || ""}`; + const lines = pdf.splitTextToSize(liText, pageWidth - margin * 2 - 5); + pdf.setFontSize(10); + pdf.setFont("Helvetica", "normal"); + pdf.text(lines, margin + 5, cursorY); + cursorY += lines.length * 10 * 0.4 + 2; + }); + } + + if (node.childNodes.length > 0) { + node.childNodes.forEach((child) => processNode(child, nextStyle)); + } + cursorY += spacingAfter; + }; + + container.childNodes.forEach((node) => + processNode(node, { fontStyle: "normal", fontSize: 10 }) + ); +}; + export default function MarkdownModal({ content, onClose }: HtmlModalProps) { + const [isDownloading, setIsDownloading] = useState(false); if (!content) return null; + const handleDownloadPdf = async () => { + if (!content || content.length === 0) return; + setIsDownloading(true); + try { + const pdf = new jsPDF({ orientation: "p", unit: "mm", format: "a4" }); + pdf.setFont("Helvetica", "normal"); + content.forEach((pageHtml, index) => { + if (index > 0) pdf.addPage(); + renderHtmlOnPdfPage(pdf, pageHtml); + }); + pdf.save("document_anonymise.pdf"); + } catch (error) { + console.error("Erreur lors de la génération du PDF:", error); + } finally { + setIsDownloading(false); + } + }; + + const previewHtml = content.join( + '
' + ); + return (
Aperçu du Document - +
+ + +
- - {/* Ce conteneur stylise le HTML propre qu'il reçoit */}
diff --git a/app/page.tsx b/app/page.tsx index 00b34f8..a1a994e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -6,86 +6,163 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; -import { - Upload, FileText, ShieldCheck, Download, Trash2, AlertCircle, X, Zap, Lock, Shield, Clock, - User, AtSign, MapPin, Cake, Home as HomeIcon, Venus, Phone, Building, Fingerprint, CreditCard, Check, - Eye // <-- Icône ajoutée +import { + Upload, + FileText, + ShieldCheck, + Download, + Trash2, + AlertCircle, + X, + Zap, + Lock, + Shield, + Clock, + User, + AtSign, + MapPin, + Cake, + Home as HomeIcon, + Venus, + Phone, + Building, + Fingerprint, + CreditCard, + Check, + Eye, } from "lucide-react"; -import MarkdownModal from "./components/MarkdownModal"; // <-- Composant de la modale ajouté +import MarkdownModal from "./components/MarkdownModal"; // === Interfaces et Données === - -// L'interface est mise à jour pour inclure le contenu texte pour l'aperçu interface ProcessedFile { - id: string; name: string; status: 'processing' | 'completed' | 'error'; - timestamp: Date; originalSize?: string; processedSize?: string; - piiCount?: number; errorMessage?: string; processedBlob?: Blob; - textContent?: string; // <-- Ajouté pour stocker le contenu du fichier + id: string; + name: string; + status: "processing" | "completed" | "error"; + timestamp: Date; + originalSize?: string; + processedSize?: string; + piiCount?: number; + errorMessage?: string; + processedBlob?: Blob; + // textContent est maintenant un tableau de strings, une par page. + textContent?: string[]; } -interface AnonymizationOptions { [key: string]: boolean; } +interface AnonymizationOptions { + [key: string]: boolean; +} const piiOptions = [ - { id: 'name', label: 'Nom & Prénom', icon: User }, - { id: 'email', label: 'Email', icon: AtSign }, - { id: 'location', label: 'Lieu', icon: MapPin }, - { id: 'birthdate', label: 'Date de naissance', icon: Cake }, - { id: 'address', label: 'Adresse', icon: HomeIcon }, - { id: 'gender', label: 'Genre / Sexe', icon: Venus }, - { id: 'phone', label: 'Téléphone', icon: Phone }, - { id: 'organization', label: 'Entreprise', icon: Building }, - { id: 'idNumber', label: 'N° Identification', icon: Fingerprint }, - { id: 'financial', label: 'Données financières', icon: CreditCard }, + { id: "name", label: "Nom & Prénom", icon: User }, + { id: "email", label: "Email", icon: AtSign }, + { id: "location", label: "Lieu", icon: MapPin }, + { id: "birthdate", label: "Date de naissance", icon: Cake }, + { id: "address", label: "Adresse", icon: HomeIcon }, + { id: "gender", label: "Genre / Sexe", icon: Venus }, + { id: "phone", label: "Téléphone", icon: Phone }, + { id: "organization", label: "Entreprise", icon: Building }, + { id: "idNumber", label: "N° Identification", icon: Fingerprint }, + { id: "financial", label: "Données financières", icon: CreditCard }, ]; export default function Home() { - // === Déclaration des états (States) === const [file, setFile] = useState(null); const [isProcessing, setIsProcessing] = useState(false); const [progress, setProgress] = useState(0); const [isDragOver, setIsDragOver] = useState(false); const [history, setHistory] = useState([]); const [error, setError] = useState(null); - const [anonymizationOptions, setAnonymizationOptions] = useState( - piiOptions.reduce((acc, option) => ({ ...acc, [option.id]: true }), {}) - ); - // Nouvel état pour gérer la modale - const [modalContent, setModalContent] = useState(null); + const [anonymizationOptions, setAnonymizationOptions] = + useState( + piiOptions.reduce((acc, option) => ({ ...acc, [option.id]: true }), {}) + ); + // L'état de la modale attend maintenant un tableau de strings ou null + const [modalContent, setModalContent] = useState(null); - // === Fonctions utilitaires et Handlers === - const handleOptionChange = (id: string) => setAnonymizationOptions(prev => ({ ...prev, [id]: !prev[id] })); - const handleFileChange = (event: React.ChangeEvent) => { if (event.target.files?.length) { setFile(event.target.files[0]); setError(null); }}; - const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); if (e.dataTransfer.files?.length) { setFile(e.dataTransfer.files[0]); setError(null); }}, []); - const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true); }, []); - const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); }, []); - const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; - const clearFile = () => { setFile(null); setError(null); }; + const handleOptionChange = (id: string) => + setAnonymizationOptions((prev) => ({ ...prev, [id]: !prev[id] })); + const handleFileChange = (event: React.ChangeEvent) => { + if (event.target.files?.length) { + setFile(event.target.files[0]); + setError(null); + } + }; + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + if (e.dataTransfer.files?.length) { + setFile(e.dataTransfer.files[0]); + setError(null); + } + }, []); + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(true); + }, []); + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + }, []); + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; + }; + const clearFile = () => { + setFile(null); + setError(null); + }; const clearHistory = () => setHistory([]); - const removeFromHistory = (id: string) => setHistory(prev => prev.filter(item => item.id !== id)); - - const handleDownload = (id: string) => { - const fileToDownload = history.find(item => item.id === id); - if (!fileToDownload?.processedBlob) return; - const url = URL.createObjectURL(fileToDownload.processedBlob); - const a = document.createElement('a'); - a.href = url; - a.download = `anonymized_${fileToDownload.name.split('.')[0] || 'file'}.txt`; - a.click(); - URL.revokeObjectURL(url); - a.remove(); + const removeFromHistory = (id: string) => + setHistory((prev) => prev.filter((item) => item.id !== id)); + + const handleDownload = (id: string) => { + const fileToDownload = history.find((item) => item.id === id); + if (!fileToDownload?.processedBlob) return; + const url = URL.createObjectURL(fileToDownload.processedBlob); + const a = document.createElement("a"); + a.href = url; + a.download = `anonymized_${ + fileToDownload.name.split(".")[0] || "file" + }.txt`; + a.click(); + URL.revokeObjectURL(url); + a.remove(); }; - // Nouvelle fonction pour gérer l'ouverture de l'aperçu + // Met à jour le contenu de la modale avec le tableau de pages const handlePreview = (id: string) => { const fileToPreview = history.find((item) => item.id === id); if (fileToPreview?.textContent) { setModalContent(fileToPreview.textContent); } }; - - const getStatusInfo = (item: ProcessedFile) => { switch (item.status) { case 'completed': return { icon: , color: 'bg-[#061717]' }; case 'error': return { icon: , color: 'bg-[#F7AB6E]' }; default: return { icon:
, color: 'bg-[#061717]' }; } }; - // === Fonction principale pour l'appel à n8n === + const getStatusInfo = (item: ProcessedFile) => { + switch (item.status) { + case "completed": + return { + icon: , + color: "bg-[#061717]", + }; + case "error": + return { + icon: , + color: "bg-[#F7AB6E]", + }; + default: + return { + icon: ( +
+ ), + color: "bg-[#061717]", + }; + } + }; + + // === Fonction principale mise à jour pour gérer le format JSON === const processFile = async () => { if (!file) return; const n8nWebhookUrl = process.env.NEXT_PUBLIC_N8N_WEBHOOK_URL; @@ -94,125 +171,397 @@ export default function Home() { return; } - setIsProcessing(true); setProgress(0); setError(null); + setIsProcessing(true); + setProgress(0); + setError(null); const fileId = `${Date.now()}-${file.name}`; - setHistory(prev => [{ id: fileId, name: file.name, status: 'processing', timestamp: new Date(), originalSize: formatFileSize(file.size) }, ...prev]); + setHistory((prev) => [ + { + id: fileId, + name: file.name, + status: "processing", + timestamp: new Date(), + originalSize: formatFileSize(file.size), + }, + ...prev, + ]); setFile(null); setProgress(10); try { const formData = new FormData(); - formData.append('file', file); - formData.append('options', JSON.stringify(anonymizationOptions)); - + formData.append("file", file); + formData.append("options", JSON.stringify(anonymizationOptions)); + setProgress(30); - const response = await fetch(n8nWebhookUrl, { method: 'POST', body: formData }); + const response = await fetch(n8nWebhookUrl, { + method: "POST", + body: formData, + }); setProgress(70); if (!response.ok) { - const errorResult = await response.json().catch(() => ({ error: `Erreur serveur [${response.status}]` })); + const errorResult = await response + .json() + .catch(() => ({ error: `Erreur serveur [${response.status}]` })); throw new Error(errorResult.error || `Échec du traitement.`); } - const processedBlob = await response.blob(); - const piiCount = parseInt(response.headers.get('X-Pii-Count') || '0', 10); - - // On lit le contenu du blob en texte pour pouvoir l'afficher - const textContent = await processedBlob.text(); - setProgress(90); - - // On met à jour l'historique avec toutes les informations, y compris le contenu texte - setHistory(prev => prev.map(item => item.id === fileId ? { ...item, status: 'completed', processedSize: formatFileSize(processedBlob.size), piiCount, processedBlob, textContent } : item)); - setProgress(100); + // On parse la réponse JSON au lieu de lire un blob + const result = await response.json(); + // Validation de la structure de la réponse + if ( + !result.anonymizedDocument || + !Array.isArray(result.anonymizedDocument.pages) + ) { + throw new Error( + "Format de réponse invalide du service d'anonymisation." + ); + } + + const textContent: string[] = result.anonymizedDocument.pages; + const piiCount: number = result.anonymizedDocument.piiCount || 0; + + // On crée un blob pour le téléchargement .txt en joignant les pages + const fullText = textContent.join("\n\n--- Page Suivante ---\n\n"); + const processedBlob = new Blob([fullText], { + type: "text/plain;charset=utf-8", + }); + setProgress(90); + + // On met à jour l'historique avec le tableau de pages + setHistory((prev) => + prev.map((item) => + item.id === fileId + ? { + ...item, + status: "completed", + processedSize: formatFileSize(processedBlob.size), + piiCount, + processedBlob, + textContent, + } + : item + ) + ); + setProgress(100); } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Une erreur inconnue est survenue."; + const errorMessage = + err instanceof Error + ? err.message + : "Une erreur inconnue est survenue."; setError(errorMessage); - setHistory(prev => prev.map(item => item.id === fileId ? { ...item, status: 'error', errorMessage } : item)); + setHistory((prev) => + prev.map((item) => + item.id === fileId ? { ...item, status: "error", errorMessage } : item + ) + ); } finally { - setIsProcessing(false); setTimeout(() => setProgress(0), 1000); + setIsProcessing(false); + setTimeout(() => setProgress(0), 1000); } }; - // === Rendu du composant (JSX) === + // === Rendu du composant (JSX - inchangé) === return (
- {/* --- Sidebar (Historique) --- */} - {/* --- Contenu Principal --- */}
-
-

LeCercle.IA

-

Anonymisation • RGPD • Sécurisé

+
+ +
+

+ LeCercle.IA +

+

+ Anonymisation • RGPD • Sécurisé +

-
- +
+
-
{file ? : }
- {file ? (

{file.name}

{formatFileSize(file.size)}

) : (

Glissez votre document

Ou cliquez ici

)} +
+ {file ? ( + + ) : ( + + )} +
+ {file ? ( +
+

+ {file.name} +

+

+ {formatFileSize(file.size)} +

+
+ ) : ( +
+

+ Glissez votre document +

+

+ Ou cliquez ici +

+
+ )}
-

Options d'Anonymisation

+

+ Options d'Anonymisation +

- {piiOptions.map((option) => ())} + {piiOptions.map((option) => ( + + ))}
- {isProcessing && (
Traitement...{Math.round(progress)}%
)} - {error && (

{error}

)} + {isProcessing && ( +
+
+ + Traitement... + + + {Math.round(progress)}% + +
+
+
+
+
+ )} + {error && ( +
+ +

+ {error} +

+
+ )}
- - {file && !isProcessing && } + + {file && !isProcessing && ( + + )}
- {[{ icon: Shield, title: "RGPD", subtitle: "Conforme" }, { icon: Clock, title: "Rapide", subtitle: "Local" }, { icon: Lock, title: "Sécurisé", subtitle: "Sans Serveur" }].map((item, index) => (

{item.title}

{item.subtitle}

))} + {[ + { icon: Shield, title: "RGPD", subtitle: "Conforme" }, + { icon: Clock, title: "Rapide", subtitle: "Local" }, + { icon: Lock, title: "Sécurisé", subtitle: "Sans Serveur" }, + ].map((item, index) => ( +
+
+ +
+

+ {item.title} +

+

+ {item.subtitle} +

+
+ ))}
- {/* Le composant de la modale est rendu ici, en dehors du flux principal */} - setModalContent(null)} + setModalContent(null)} />
); -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 8e79bce..e04dac5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@ungap/with-resolvers": "^0.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "html2canvas": "^1.4.1", + "jspdf": "^3.0.1", "lucide-react": "^0.514.0", "mammoth": "^1.9.1", "next": "15.3.3", @@ -67,6 +69,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -1719,6 +1730,13 @@ "pdfjs-dist": "*" } }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", @@ -1738,6 +1756,13 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2583,6 +2608,18 @@ "node": ">= 0.4" } }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2636,6 +2673,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2697,6 +2743,18 @@ "node": ">=8" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -2812,6 +2870,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -3023,6 +3101,18 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js": { + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.43.0.tgz", + "integrity": "sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -3043,6 +3133,15 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3268,6 +3367,16 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/duck": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", @@ -4052,6 +4161,12 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4494,6 +4609,19 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -5200,6 +5328,24 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jspdf": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz", + "integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.7", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -7217,6 +7363,13 @@ "@napi-rs/canvas": "^0.1.67" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7371,6 +7524,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -7463,6 +7626,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -7642,6 +7812,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8008,6 +8188,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stdin-discarder": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", @@ -8298,6 +8488,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -8343,6 +8543,15 @@ "node": ">=18" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -8747,6 +8956,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/package.json b/package.json index fc62efd..0cf2bf7 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@ungap/with-resolvers": "^0.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "html2canvas": "^1.4.1", + "jspdf": "^3.0.1", "lucide-react": "^0.514.0", "mammoth": "^1.9.1", "next": "15.3.3",