rgpd + mise en page
This commit is contained in:
@@ -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(
|
||||
'<hr style="margin: 2.5rem 0; border-style: dashed; border-color: rgba(255,255,255,0.2);">'
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-70 z-50 flex justify-center items-center p-4"
|
||||
@@ -23,18 +134,29 @@ export default function MarkdownModal({ content, onClose }: HtmlModalProps) {
|
||||
<h3 className="text-lg font-black text-white uppercase">
|
||||
Aperçu du Document
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleDownloadPdf}
|
||||
disabled={isDownloading}
|
||||
className="p-2 bg-[#F7AB6E] text-white border-2 border-white shadow-[3px_3px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5 disabled:opacity-50"
|
||||
>
|
||||
{isDownloading ? (
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent" />
|
||||
) : (
|
||||
<Download className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 bg-[#F7AB6E] text-white border-2 border-white shadow-[3px_3px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Ce conteneur stylise le HTML propre qu'il reçoit */}
|
||||
<div
|
||||
className="preview-content p-6 overflow-y-auto w-full h-full"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
dangerouslySetInnerHTML={{ __html: previewHtml }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
523
app/page.tsx
523
app/page.tsx
@@ -7,75 +7,132 @@ 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
|
||||
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<File | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [history, setHistory] = useState<ProcessedFile[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [anonymizationOptions, setAnonymizationOptions] = useState<AnonymizationOptions>(
|
||||
const [anonymizationOptions, setAnonymizationOptions] =
|
||||
useState<AnonymizationOptions>(
|
||||
piiOptions.reduce((acc, option) => ({ ...acc, [option.id]: true }), {})
|
||||
);
|
||||
// Nouvel état pour gérer la modale
|
||||
const [modalContent, setModalContent] = useState<string | null>(null);
|
||||
// L'état de la modale attend maintenant un tableau de strings ou null
|
||||
const [modalContent, setModalContent] = useState<string[] | null>(null);
|
||||
|
||||
// === Fonctions utilitaires et Handlers ===
|
||||
const handleOptionChange = (id: string) => setAnonymizationOptions(prev => ({ ...prev, [id]: !prev[id] }));
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { 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<HTMLInputElement>) => {
|
||||
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 removeFromHistory = (id: string) =>
|
||||
setHistory((prev) => prev.filter((item) => item.id !== id));
|
||||
|
||||
const handleDownload = (id: string) => {
|
||||
const fileToDownload = history.find(item => item.id === id);
|
||||
const fileToDownload = history.find((item) => item.id === id);
|
||||
if (!fileToDownload?.processedBlob) return;
|
||||
const url = URL.createObjectURL(fileToDownload.processedBlob);
|
||||
const a = document.createElement('a');
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `anonymized_${fileToDownload.name.split('.')[0] || 'file'}.txt`;
|
||||
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) {
|
||||
@@ -83,9 +140,29 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusInfo = (item: ProcessedFile) => { switch (item.status) { case 'completed': return { icon: <ShieldCheck className="h-4 w-4 text-white" />, color: 'bg-[#061717]' }; case 'error': return { icon: <AlertCircle className="h-4 w-4 text-white" />, color: 'bg-[#F7AB6E]' }; default: return { icon: <div className="h-2 w-2 rounded-full bg-[#F7AB6E] animate-pulse" />, color: 'bg-[#061717]' }; } };
|
||||
const getStatusInfo = (item: ProcessedFile) => {
|
||||
switch (item.status) {
|
||||
case "completed":
|
||||
return {
|
||||
icon: <ShieldCheck className="h-4 w-4 text-white" />,
|
||||
color: "bg-[#061717]",
|
||||
};
|
||||
case "error":
|
||||
return {
|
||||
icon: <AlertCircle className="h-4 w-4 text-white" />,
|
||||
color: "bg-[#F7AB6E]",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: (
|
||||
<div className="h-2 w-2 rounded-full bg-[#F7AB6E] animate-pulse" />
|
||||
),
|
||||
color: "bg-[#061717]",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// === Fonction principale pour l'appel à n8n ===
|
||||
// === 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,121 +171,393 @@ 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 parse la réponse JSON au lieu de lire un blob
|
||||
const result = await response.json();
|
||||
|
||||
// On lit le contenu du blob en texte pour pouvoir l'afficher
|
||||
const textContent = await processedBlob.text();
|
||||
// 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 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));
|
||||
// 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 (
|
||||
<div className="h-screen w-screen bg-[#061717] flex flex-col md:flex-row overflow-hidden">
|
||||
{/* --- Sidebar (Historique) --- */}
|
||||
<aside className="w-full md:w-80 md:flex-shrink-0 bg-[#061717] border-b-4 md:border-b-0 md:border-r-4 border-white flex flex-col shadow-[4px_0_0_0_black]">
|
||||
<div className="p-4 border-b-4 border-white bg-[#F7AB6E] shadow-[0_4px_0_0_black] flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Image src="/logo.svg" alt="LeCercle.IA Logo" width={32} height={32} />
|
||||
<h2 className="text-lg font-black text-white uppercase tracking-wide">Historique</h2>
|
||||
<Image
|
||||
src="/logo.svg"
|
||||
alt="LeCercle.IA Logo"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
<h2 className="text-lg font-black text-white uppercase tracking-wide">
|
||||
Historique
|
||||
</h2>
|
||||
</div>
|
||||
{history.length > 0 && <Button onClick={clearHistory} variant="ghost" size="icon" className="bg-[#061717] text-white border-2 border-white shadow-[3px_3px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"><Trash2 className="h-4 w-4" /></Button>}
|
||||
{history.length > 0 && (
|
||||
<Button
|
||||
onClick={clearHistory}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="bg-[#061717] text-white border-2 border-white shadow-[3px_3px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
||||
{history.length === 0 ? (
|
||||
<div className="text-center py-10 px-4 flex flex-col justify-center h-full">
|
||||
<div className="w-14 h-14 bg-[#F7AB6E] border-4 border-white shadow-[6px_6px_0_0_black] mx-auto mb-4 flex items-center justify-center"><FileText className="h-7 w-7 text-white" /></div>
|
||||
<p className="text-white font-black text-base uppercase">Aucun Document</p>
|
||||
<p className="text-white/70 font-bold mt-1 text-xs">Vos fichiers apparaîtront ici.</p>
|
||||
<div className="w-14 h-14 bg-[#F7AB6E] border-4 border-white shadow-[6px_6px_0_0_black] mx-auto mb-4 flex items-center justify-center">
|
||||
<FileText className="h-7 w-7 text-white" />
|
||||
</div>
|
||||
<p className="text-white font-black text-base uppercase">
|
||||
Aucun Document
|
||||
</p>
|
||||
<p className="text-white/70 font-bold mt-1 text-xs">
|
||||
Vos fichiers apparaîtront ici.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
history.map((item) => {
|
||||
const status = getStatusInfo(item);
|
||||
return (<div key={item.id} className="bg-[#061717] border-2 border-white shadow-[4px_4px_0_0_black] p-3 transition-all"><div className="flex items-start justify-between"><div className="flex items-center gap-3 min-w-0"><div className={`flex-shrink-0 w-7 h-7 border-2 border-white shadow-[2px_2px_0_0_black] flex items-center justify-center ${status.color}`}>{status.icon}</div><div className="min-w-0"><p className="text-sm font-black text-white uppercase truncate" title={item.name}>{item.name}</p>{item.status === 'completed' && (<div className="flex items-center gap-2 mt-1"><span className="text-xs font-bold text-white/70">{item.originalSize} → {item.processedSize}</span><Badge className="bg-[#F7AB6E] text-white border-2 border-white shadow-[2px_2px_0_0_black] font-black uppercase text-[10px] px-1.5 py-0">{item.piiCount} PII</Badge></div>)}{item.status === 'error' && <p className="text-xs font-bold text-[#F7AB6E] mt-1 uppercase">Erreur</p>}{item.status === 'processing' && <p className="text-xs font-bold text-[#F7AB6E] mt-1 uppercase">Traitement...</p>}</div></div><div className="flex items-center gap-1.5 ml-2">
|
||||
{/* Le nouveau bouton "Aperçu" est ajouté ici */}
|
||||
{item.status === 'completed' && item.textContent && (
|
||||
<Button onClick={() => handlePreview(item.id)} variant="ghost" size="icon" className="h-7 w-7 bg-[#061717] text-white border-2 border-white shadow-[2px_2px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5">
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-[#061717] border-2 border-white shadow-[4px_4px_0_0_black] p-3 transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div
|
||||
className={`flex-shrink-0 w-7 h-7 border-2 border-white shadow-[2px_2px_0_0_black] flex items-center justify-center ${status.color}`}
|
||||
>
|
||||
{status.icon}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p
|
||||
className="text-sm font-black text-white uppercase truncate"
|
||||
title={item.name}
|
||||
>
|
||||
{item.name}
|
||||
</p>
|
||||
{item.status === "completed" && (
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs font-bold text-white/70">
|
||||
{item.originalSize} → {item.processedSize}
|
||||
</span>
|
||||
<Badge className="bg-[#F7AB6E] text-white border-2 border-white shadow-[2px_2px_0_0_black] font-black uppercase text-[10px] px-1.5 py-0">
|
||||
{item.piiCount} PII
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{item.status === "error" && (
|
||||
<p className="text-xs font-bold text-[#F7AB6E] mt-1 uppercase">
|
||||
Erreur
|
||||
</p>
|
||||
)}
|
||||
{item.status === "processing" && (
|
||||
<p className="text-xs font-bold text-[#F7AB6E] mt-1 uppercase">
|
||||
Traitement...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 ml-2">
|
||||
{item.status === "completed" && item.textContent && (
|
||||
<Button
|
||||
onClick={() => handlePreview(item.id)}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 bg-[#061717] text-white border-2 border-white shadow-[2px_2px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
|
||||
>
|
||||
<Eye className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{item.status === 'completed' && (<Button onClick={() => handleDownload(item.id)} variant="ghost" size="icon" className="h-7 w-7 bg-[#061717] text-white border-2 border-white shadow-[2px_2px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"><Download className="h-3 w-3" /></Button>)}
|
||||
<Button onClick={() => removeFromHistory(item.id)} variant="ghost" size="icon" className="h-7 w-7 bg-[#F7AB6E] text-white border-2 border-white shadow-[2px_2px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"><X className="h-3 w-3" /></Button>
|
||||
</div></div></div>);
|
||||
{item.status === "completed" && (
|
||||
<Button
|
||||
onClick={() => handleDownload(item.id)}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 bg-[#061717] text-white border-2 border-white shadow-[2px_2px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
|
||||
>
|
||||
<Download className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => removeFromHistory(item.id)}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 bg-[#F7AB6E] text-white border-2 border-white shadow-[2px_2px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* --- Contenu Principal --- */}
|
||||
<main className="flex-1 flex flex-col items-center justify-center p-4 md:p-6 bg-[#061717] overflow-y-auto">
|
||||
<div className="w-full max-w-xl flex flex-col h-full">
|
||||
<header className="text-center pt-4">
|
||||
<div className="inline-block p-2 bg-[#F7AB6E] border-2 border-white shadow-[6px_6px_0_0_black] mb-3"><Lock className="h-8 w-8 text-white" /></div>
|
||||
<h1 className="text-3xl md:text-4xl font-black text-white uppercase tracking-tighter mb-1">LeCercle.IA</h1>
|
||||
<p className="text-base md:text-lg font-bold text-white/90 uppercase tracking-wider">Anonymisation • RGPD • Sécurisé</p>
|
||||
<div className="inline-block p-2 bg-[#F7AB6E] border-2 border-white shadow-[6px_6px_0_0_black] mb-3">
|
||||
<Lock className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl font-black text-white uppercase tracking-tighter mb-1">
|
||||
LeCercle.IA
|
||||
</h1>
|
||||
<p className="text-base md:text-lg font-bold text-white/90 uppercase tracking-wider">
|
||||
Anonymisation • RGPD • Sécurisé
|
||||
</p>
|
||||
</header>
|
||||
<Card className="flex-grow my-4 bg-[#061717] border-4 border-white shadow-[10px_10px_0_0_black] flex flex-col">
|
||||
<CardContent className="p-4 md:p-6 space-y-4 flex-grow flex flex-col">
|
||||
<div onDrop={handleDrop} onDragOver={handleDragOver} onDragLeave={handleDragLeave} className={`relative border-4 border-dashed p-6 text-center transition-all duration-200 ${isDragOver ? 'border-[#F7AB6E] bg-white/5' : file ? 'border-[#F7AB6E] bg-white/5 border-solid' : 'border-white hover:border-[#F7AB6E] hover:bg-white/5'}`}>
|
||||
<Input type="file" onChange={handleFileChange} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" accept=".txt,.json,.csv,.pdf,.docx" />
|
||||
<div
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
className={`relative border-4 border-dashed p-6 text-center transition-all duration-200 ${
|
||||
isDragOver
|
||||
? "border-[#F7AB6E] bg-white/5"
|
||||
: file
|
||||
? "border-[#F7AB6E] bg-white/5 border-solid"
|
||||
: "border-white hover:border-[#F7AB6E] hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
<Input
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
accept=".txt,.json,.csv,.pdf,.docx"
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<div className={`w-14 h-14 border-4 border-white shadow-[5px_5px_0_0_black] flex items-center justify-center transition-all duration-200 ${file ? 'bg-[#F7AB6E]' : 'bg-[#061717]'}`}>{file ? <FileText className="h-7 w-7 text-white" /> : <Upload className="h-7 w-7 text-white" />}</div>
|
||||
{file ? (<div><p className="text-base font-black text-white uppercase">{file.name}</p><p className="text-sm font-bold text-white/70">{formatFileSize(file.size)}</p></div>) : (<div><p className="text-lg font-black text-white uppercase tracking-wide">Glissez votre document</p><p className="text-base font-bold text-white/70 uppercase mt-1">Ou cliquez ici</p></div>)}
|
||||
<div
|
||||
className={`w-14 h-14 border-4 border-white shadow-[5px_5px_0_0_black] flex items-center justify-center transition-all duration-200 ${
|
||||
file ? "bg-[#F7AB6E]" : "bg-[#061717]"
|
||||
}`}
|
||||
>
|
||||
{file ? (
|
||||
<FileText className="h-7 w-7 text-white" />
|
||||
) : (
|
||||
<Upload className="h-7 w-7 text-white" />
|
||||
)}
|
||||
</div>
|
||||
{file ? (
|
||||
<div>
|
||||
<p className="text-base font-black text-white uppercase">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-sm font-bold text-white/70">
|
||||
{formatFileSize(file.size)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-lg font-black text-white uppercase tracking-wide">
|
||||
Glissez votre document
|
||||
</p>
|
||||
<p className="text-base font-bold text-white/70 uppercase mt-1">
|
||||
Ou cliquez ici
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<h3 className="text-base font-black text-white uppercase tracking-wide mb-3">Options d'Anonymisation</h3>
|
||||
<h3 className="text-base font-black text-white uppercase tracking-wide mb-3">
|
||||
Options d'Anonymisation
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-2 gap-x-4 gap-y-2">
|
||||
{piiOptions.map((option) => (<label key={option.id} htmlFor={option.id} className="flex items-center gap-2 cursor-pointer group"><input type="checkbox" id={option.id} className="sr-only peer" checked={anonymizationOptions[option.id]} onChange={() => handleOptionChange(option.id)} /><div className="w-5 h-5 border-2 border-white shadow-[2px_2px_0_0_black] flex items-center justify-center transition-all peer-checked:bg-[#F7AB6E] peer-checked:border-[#F7AB6E]">{anonymizationOptions[option.id] && <Check className="h-4 w-4 text-white" />}</div><span className="font-bold text-xs text-white uppercase">{option.label}</span></label>))}
|
||||
{piiOptions.map((option) => (
|
||||
<label
|
||||
key={option.id}
|
||||
htmlFor={option.id}
|
||||
className="flex items-center gap-2 cursor-pointer group"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={option.id}
|
||||
className="sr-only peer"
|
||||
checked={anonymizationOptions[option.id]}
|
||||
onChange={() => handleOptionChange(option.id)}
|
||||
/>
|
||||
<div className="w-5 h-5 border-2 border-white shadow-[2px_2px_0_0_black] flex items-center justify-center transition-all peer-checked:bg-[#F7AB6E] peer-checked:border-[#F7AB6E]">
|
||||
{anonymizationOptions[option.id] && (
|
||||
<Check className="h-4 w-4 text-white" />
|
||||
)}
|
||||
</div>
|
||||
<span className="font-bold text-xs text-white uppercase">
|
||||
{option.label}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{isProcessing && (<div className="space-y-2"><div className="flex justify-between items-center"><span className="text-base font-black text-white uppercase">Traitement...</span><span className="text-lg font-black text-[#F7AB6E]">{Math.round(progress)}%</span></div><div className="h-4 bg-[#061717] border-2 border-white shadow-[3px_3px_0_0_black]"><div className="h-full bg-[#F7AB6E] transition-all duration-300" style={{ width: `${progress}%` }} /></div></div>)}
|
||||
{error && (<div className="flex items-start gap-3 p-3 bg-[#F7AB6E] border-2 border-white shadow-[4px_4px_0_0_black]"><AlertCircle className="h-5 w-5 text-white flex-shrink-0 mt-0.5" /><p className="text-sm font-bold text-white uppercase">{error}</p></div>)}
|
||||
{isProcessing && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-base font-black text-white uppercase">
|
||||
Traitement...
|
||||
</span>
|
||||
<span className="text-lg font-black text-[#F7AB6E]">
|
||||
{Math.round(progress)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-4 bg-[#061717] border-2 border-white shadow-[3px_3px_0_0_black]">
|
||||
<div
|
||||
className="h-full bg-[#F7AB6E] transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex items-start gap-3 p-3 bg-[#F7AB6E] border-2 border-white shadow-[4px_4px_0_0_black]">
|
||||
<AlertCircle className="h-5 w-5 text-white flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm font-bold text-white uppercase">
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-stretch gap-3 pt-2">
|
||||
<Button onClick={processFile} disabled={!file || isProcessing} className="flex-1 bg-[#F7AB6E] text-white border-4 border-white shadow-[6px_6px_0_0_black] hover:shadow-[3px_3px_0_0_black] active:shadow-none active:translate-x-1 active:translate-y-1 h-12 text-base font-black uppercase tracking-wide disabled:opacity-50">{isProcessing ? <div className="animate-spin rounded-full h-6 w-6 border-4 border-white border-t-transparent" /> : <><Zap className="h-5 w-5 mr-2" />Anonymiser</>}</Button>
|
||||
{file && !isProcessing && <Button onClick={clearFile} className="h-12 w-12 p-0 bg-[#061717] text-white border-4 border-white shadow-[6px_6px_0_0_black] hover:shadow-[3px_3px_0_0_black] active:shadow-none active:translate-x-1 active:translate-y-1"><X className="h-6 w-6" /></Button>}
|
||||
<Button
|
||||
onClick={processFile}
|
||||
disabled={!file || isProcessing}
|
||||
className="flex-1 bg-[#F7AB6E] text-white border-4 border-white shadow-[6px_6px_0_0_black] hover:shadow-[3px_3px_0_0_black] active:shadow-none active:translate-x-1 active:translate-y-1 h-12 text-base font-black uppercase tracking-wide disabled:opacity-50"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-4 border-white border-t-transparent" />
|
||||
) : (
|
||||
<>
|
||||
<Zap className="h-5 w-5 mr-2" />
|
||||
Anonymiser
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{file && !isProcessing && (
|
||||
<Button
|
||||
onClick={clearFile}
|
||||
className="h-12 w-12 p-0 bg-[#061717] text-white border-4 border-white shadow-[6px_6px_0_0_black] hover:shadow-[3px_3px_0_0_black] active:shadow-none active:translate-x-1 active:translate-y-1"
|
||||
>
|
||||
<X className="h-6 w-6" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid grid-cols-3 gap-3 pb-4">
|
||||
{[{ icon: Shield, title: "RGPD", subtitle: "Conforme" }, { icon: Clock, title: "Rapide", subtitle: "Local" }, { icon: Lock, title: "Sécurisé", subtitle: "Sans Serveur" }].map((item, index) => (<div key={index} className="bg-[#061717] border-2 border-white shadow-[4px_4px_0_0_black] p-3 text-center"><div className="w-8 h-8 bg-[#F7AB6E] border-2 border-white shadow-[2px_2px_0_0_black] mx-auto mb-2 flex items-center justify-center"><item.icon className="h-4 w-4 text-white" /></div><p className="text-sm font-black text-white uppercase">{item.title}</p><p className="text-xs font-bold text-white/70 uppercase">{item.subtitle}</p></div>))}
|
||||
{[
|
||||
{ icon: Shield, title: "RGPD", subtitle: "Conforme" },
|
||||
{ icon: Clock, title: "Rapide", subtitle: "Local" },
|
||||
{ icon: Lock, title: "Sécurisé", subtitle: "Sans Serveur" },
|
||||
].map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-[#061717] border-2 border-white shadow-[4px_4px_0_0_black] p-3 text-center"
|
||||
>
|
||||
<div className="w-8 h-8 bg-[#F7AB6E] border-2 border-white shadow-[2px_2px_0_0_black] mx-auto mb-2 flex items-center justify-center">
|
||||
<item.icon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<p className="text-sm font-black text-white uppercase">
|
||||
{item.title}
|
||||
</p>
|
||||
<p className="text-xs font-bold text-white/70 uppercase">
|
||||
{item.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Le composant de la modale est rendu ici, en dehors du flux principal */}
|
||||
<MarkdownModal
|
||||
content={modalContent}
|
||||
onClose={() => setModalContent(null)}
|
||||
|
||||
218
package-lock.json
generated
218
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user