n8n anonyme
41
.gitignore
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
37
README.md
@@ -1 +1,36 @@
|
||||
# Anonyme
|
||||
# Anonyme
|
||||
|
||||
This is a Next.js project bootstrapped with `create-next-app`.
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
|
||||
195
app/api/anonymize/route.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
// Fichier : src/app/api/anonymize/route.ts
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import mammoth from "mammoth";
|
||||
import * as pdfjs from "pdfjs-dist";
|
||||
|
||||
// --- Vos URLs Presidio ---
|
||||
const PRESIDIO_ANALYZER_URL =
|
||||
"http://ocs00s000ssow8kssossocco.51.68.233.212.sslip.io/analyze";
|
||||
const PRESIDIO_ANONYMIZER_URL =
|
||||
"http://r8gko4kcwwk4sso40cc0gkg8.51.68.233.212.sslip.io/anonymize";
|
||||
|
||||
// Fonction utilitaire pour extraire le texte d'un PDF
|
||||
const extractTextWithPdfJs = async (pdfData: Uint8Array): Promise<string> => {
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.min.mjs`;
|
||||
|
||||
const doc = await pdfjs.getDocument(pdfData).promise;
|
||||
const numPages = doc.numPages;
|
||||
let fullText = "";
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
const page = await doc.getPage(i);
|
||||
const content = await page.getTextContent();
|
||||
|
||||
// ===================================================================
|
||||
// === CORRECTION CRUCIALE DE L'EXTRACTION DE TEXTE ===
|
||||
// ===================================================================
|
||||
// On joint chaque morceau de texte trouvé avec un espace.
|
||||
// Cela évite de coller des mots comme "beSi".
|
||||
// Le nettoyage ultérieur se chargera des espaces en trop.
|
||||
const pageText = content.items
|
||||
.map((item: unknown) => ("str" in item ? item.str : ""))
|
||||
.join(" ");
|
||||
fullText += pageText + "\n"; // On ajoute un saut de ligne entre les pages
|
||||
}
|
||||
|
||||
return fullText;
|
||||
};
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file") as File | null;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: "Aucun fichier reçu" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// --- Extraction du texte ---
|
||||
let extractedText = "";
|
||||
const fileArrayBuffer = await file.arrayBuffer();
|
||||
if (file.type === "application/pdf") {
|
||||
extractedText = await extractTextWithPdfJs(
|
||||
new Uint8Array(fileArrayBuffer)
|
||||
);
|
||||
} else if (
|
||||
file.type ===
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
) {
|
||||
const { value } = await mammoth.extractRawText({
|
||||
buffer: Buffer.from(fileArrayBuffer),
|
||||
});
|
||||
extractedText = value;
|
||||
} else if (file.type.startsWith("text/")) {
|
||||
extractedText = await file.text();
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: `Type de fichier non supporté: ${file.type}` },
|
||||
{ status: 415 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!extractedText.trim()) {
|
||||
return NextResponse.json(
|
||||
{ error: "Impossible d'extraire du texte de ce fichier." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// --- Nettoyage général du texte après extraction ---
|
||||
const cleanedText = extractedText.replace(/\s+/g, " ").trim();
|
||||
|
||||
// La configuration d'analyse reste la même, complète et agressive
|
||||
const analyzerPayload = {
|
||||
text: cleanedText,
|
||||
language: "fr",
|
||||
ad_hoc_recognizers: [
|
||||
{
|
||||
name: "Email Recognizer",
|
||||
supported_entity: "EMAIL_ADDRESS",
|
||||
deny_list: ["[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+"],
|
||||
},
|
||||
{
|
||||
name: "French Phone Recognizer",
|
||||
supported_entity: "PHONE_NUMBER",
|
||||
deny_list: ["\\b(0|\\+33|0033)[1-9]([-. ]?[0-9]{2}){4}\\b"],
|
||||
},
|
||||
{
|
||||
name: "IBAN Recognizer",
|
||||
supported_entity: "IBAN",
|
||||
deny_list: ["\\b[A-Z]{2}[0-9]{2}(?:[ ]?[0-9]{4}){4,7}\\b"],
|
||||
},
|
||||
{
|
||||
name: "BIC/SWIFT Recognizer",
|
||||
supported_entity: "SWIFT_CODE",
|
||||
deny_list: [
|
||||
"\\b([A-Z]{6}[A-Z2-9][A-NP-Z0-9])(X{3}|[A-NP-Z0-9]{3})?\\b",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Belgian Company Number",
|
||||
supported_entity: "BE_COMPANY_NUMBER",
|
||||
deny_list: ["\\bBE\\s*0[0-9]{3}[.]?[0-9]{3}[.]?[0-9]{3}\\b"],
|
||||
},
|
||||
{
|
||||
name: "Company Name Recognizer",
|
||||
supported_entity: "ORGANIZATION",
|
||||
deny_list: [
|
||||
"\\b[A-Z][a-zA-ZÀ-ÖØ-öø-ÿ-&' ]+\\s+(SPRL|SA|Partners|Solutions|Capital)\\b",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Specific Company Recognizer",
|
||||
supported_entity: "ORGANIZATION",
|
||||
deny_list: [
|
||||
"TechFlow Solutions SPRL",
|
||||
"Innovation Capital Partners SA",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Date Recognizer (DD/MM/YYYY)",
|
||||
supported_entity: "DATE_TIME",
|
||||
deny_list: [
|
||||
"\\b(0[1-9]|[12][0-9]|3[01])[-/.](0[1-9]|1[012])[-/.](19|20)\\d\\d\\b",
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// --- Le reste du processus ne change pas ---
|
||||
const analyzeResponse = await fetch(PRESIDIO_ANALYZER_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(analyzerPayload),
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!analyzeResponse.ok) {
|
||||
const errorText = await analyzeResponse.text();
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Erreur de l'Analyzer [${analyzeResponse.status}]: ${errorText}`,
|
||||
},
|
||||
{ status: analyzeResponse.status }
|
||||
);
|
||||
}
|
||||
const analysisResults = await analyzeResponse.json();
|
||||
|
||||
const anonymizeResponse = await fetch(PRESIDIO_ANONYMIZER_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
text: cleanedText,
|
||||
analyzer_results: analysisResults,
|
||||
anonymizers: { DEFAULT: { type: "replace", new_value: "<ANONYMISÉ>" } },
|
||||
}),
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!anonymizeResponse.ok) {
|
||||
const errorText = await anonymizeResponse.text();
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Erreur de l'Anonymizer [${anonymizeResponse.status}]: ${errorText}`,
|
||||
},
|
||||
{ status: anonymizeResponse.status }
|
||||
);
|
||||
}
|
||||
|
||||
const anonymizedData = await anonymizeResponse.json();
|
||||
const finalResponse = { text: anonymizedData.text, items: analysisResults };
|
||||
return NextResponse.json(finalResponse);
|
||||
} catch (error) {
|
||||
console.error("Erreur critique dans l'API Route:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Erreur inconnue";
|
||||
return NextResponse.json(
|
||||
{ error: `Erreur interne du serveur: ${errorMessage}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
BIN
app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
122
app/globals.css
Normal file
@@ -0,0 +1,122 @@
|
||||
@import "tw-animate-css";
|
||||
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
34
app/layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "LeCercle IA - Anonimyzation",
|
||||
description: "Anonimyzer vos documents de maniére sécurisée.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
174
app/page.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import Image from "next/image";
|
||||
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
|
||||
} from "lucide-react";
|
||||
|
||||
// Interfaces et Données
|
||||
interface ProcessedFile {
|
||||
id: string; name: string; status: 'processing' | 'completed' | 'error';
|
||||
timestamp: Date; originalSize?: string; processedSize?: string;
|
||||
piiCount?: number; errorMessage?: string; processedBlob?: Blob;
|
||||
}
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
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>(
|
||||
piiOptions.reduce((acc, option) => ({ ...acc, [option.id]: true }), {})
|
||||
);
|
||||
|
||||
// === 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 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 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 ===
|
||||
const processFile = async () => {
|
||||
if (!file) return;
|
||||
const n8nWebhookUrl = process.env.NEXT_PUBLIC_N8N_WEBHOOK_URL;
|
||||
if (!n8nWebhookUrl) {
|
||||
setError("L'URL du service est manquante. Contactez l'administrateur.");
|
||||
return;
|
||||
}
|
||||
|
||||
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]);
|
||||
setFile(null);
|
||||
setProgress(10);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
// On attache le fichier binaire
|
||||
formData.append('file', file);
|
||||
// On attache les options cochées sous forme de chaîne JSON
|
||||
formData.append('options', JSON.stringify(anonymizationOptions));
|
||||
|
||||
setProgress(30);
|
||||
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}]` }));
|
||||
throw new Error(errorResult.error || `Échec du traitement.`);
|
||||
}
|
||||
|
||||
const processedBlob = await response.blob();
|
||||
const piiCount = parseInt(response.headers.get('X-Pii-Count') || '0', 10);
|
||||
setProgress(90);
|
||||
|
||||
setHistory(prev => prev.map(item => item.id === fileId ? { ...item, status: 'completed', processedSize: formatFileSize(processedBlob.size), piiCount, processedBlob } : item));
|
||||
setProgress(100);
|
||||
|
||||
} catch (err) {
|
||||
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));
|
||||
} finally {
|
||||
setIsProcessing(false); setTimeout(() => setProgress(0), 1000);
|
||||
}
|
||||
};
|
||||
|
||||
// === Rendu du composant (JSX) ===
|
||||
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>
|
||||
</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>}
|
||||
</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>
|
||||
) : (
|
||||
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">{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>
|
||||
</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 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>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<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>))}
|
||||
</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>)}
|
||||
<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>}
|
||||
</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>))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
46
components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
59
components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
92
components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
21
components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
31
components/ui/progress.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Progress }
|
||||
28
components/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
16
eslint.config.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
6
lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
5
next.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
7440
package-lock.json
generated
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "anonyme",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@shadcn/ui": "^0.0.4",
|
||||
"@ungap/with-resolvers": "^0.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.514.0",
|
||||
"mammoth": "^1.9.1",
|
||||
"next": "15.3.3",
|
||||
"pdfjs-dist": "^5.3.31",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/pdfjs-dist": "^2.10.377",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.3",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
5
postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
public/file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
public/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="189" height="48" viewBox="0 0 189 48" fill="none"><circle cx="23.8" cy="23.8" r="22.9186" stroke="#F8F8F4" stroke-width="1.76297"></circle><path d="M27.3423 17.8564C27.3423 18.8853 27.1989 19.8392 26.9253 20.7051C22.5726 20.7707 19.7061 22.7744 18.6899 26.0146C18.649 26.015 18.608 26.0176 18.5669 26.0176C12.9137 26.0176 9.69678 22.8 9.69678 17.8564C9.69691 12.913 12.9138 9.69629 18.5669 9.69629C24.0778 9.69646 27.3422 12.9132 27.3423 17.8564ZM22.6108 17.8564C22.6108 14.7109 21.7833 13.9542 18.5669 13.9541C15.3501 13.9541 14.4273 14.7108 14.4272 17.8564C14.4272 21.0024 15.35 21.7598 18.5669 21.7598C21.7834 21.7597 22.6108 21.0022 22.6108 17.8564ZM35.9224 28.8623C35.9222 33.8056 32.658 37.0224 27.147 37.0225C21.494 37.0225 18.2771 33.8056 18.2769 28.8623C18.2769 27.8338 18.4185 26.8803 18.6899 26.0146C22.9864 25.9769 25.8941 23.9683 26.9253 20.7051C26.9988 20.704 27.0727 20.7012 27.147 20.7012C32.6582 20.7012 35.9224 23.9187 35.9224 28.8623ZM31.1919 28.8623C31.1919 25.7164 30.3638 24.959 27.147 24.959C23.9301 24.959 23.0073 25.7164 23.0073 28.8623C23.0074 32.0078 23.9303 32.7646 27.147 32.7646C30.3637 32.7646 31.1918 32.0078 31.1919 28.8623Z" fill="#F8F8F4"></path><path d="M69.6001 34.248C66.3788 34.248 63.7868 33.1387 61.8241 30.92C59.8614 28.7014 58.8801 25.8427 58.8801 22.344C58.8801 18.888 59.8508 16.04 61.7921 13.8C63.7548 11.5387 66.4001 10.408 69.7281 10.408C72.3308 10.408 74.4321 11.08 76.0321 12.424C77.6321 13.768 78.5921 15.528 78.9121 17.704H76.0961C75.8188 16.2534 75.1148 15.0907 73.9841 14.216C72.8748 13.3414 71.4348 12.904 69.6641 12.904C67.2108 12.904 65.2908 13.7894 63.9041 15.56C62.5174 17.3094 61.8241 19.5707 61.8241 22.344C61.8241 25.16 62.5388 27.432 63.9681 29.16C65.4188 30.8667 67.3281 31.72 69.6961 31.72C71.7441 31.72 73.3228 31.1227 74.4321 29.928C75.5628 28.7334 76.2028 27.2294 76.3521 25.416H79.2001C79.1788 26.5467 78.9121 27.7094 78.4001 28.904C77.9094 30.0774 77.2588 31.0587 76.4481 31.848C74.8054 33.448 72.5228 34.248 69.6001 34.248ZM90.0796 34.248C87.6263 34.248 85.6743 33.4267 84.2236 31.784C82.7943 30.12 82.0796 28.04 82.0796 25.544C82.0796 23.0907 82.8049 21.032 84.2556 19.368C85.7276 17.6827 87.6156 16.84 89.9196 16.84C92.3516 16.84 94.2289 17.6934 95.5516 19.4C96.8743 21.0854 97.5356 23.336 97.5356 26.152H84.7356C84.8423 27.9014 85.3543 29.3307 86.2716 30.44C87.2103 31.528 88.4796 32.072 90.0796 32.072C92.5543 32.072 94.0796 30.9414 94.6556 28.68H97.1836C96.7996 30.4507 95.9889 31.8267 94.7516 32.808C93.5143 33.768 91.9569 34.248 90.0796 34.248ZM93.3756 20.296C92.5436 19.3574 91.3916 18.888 89.9196 18.888C88.4476 18.888 87.2743 19.3787 86.3996 20.36C85.5463 21.3414 85.0129 22.6214 84.7996 24.2H94.7516C94.6663 22.536 94.2076 21.2347 93.3756 20.296ZM103.622 17.256V20.008H103.686C104.219 19.1334 104.913 18.4294 105.766 17.896C106.619 17.3414 107.569 17.064 108.614 17.064C108.998 17.064 109.35 17.128 109.67 17.256V19.752H109.574C109.254 19.6454 108.87 19.592 108.422 19.592C107.099 19.592 105.969 20.0614 105.03 21C104.091 21.9174 103.622 23.1227 103.622 24.616V33.8H101.03V17.256H103.622ZM119.406 34.248C116.995 34.248 115.054 33.416 113.582 31.752C112.131 30.088 111.406 28.0187 111.406 25.544C111.406 23.0694 112.131 21 113.582 19.336C115.054 17.672 116.995 16.84 119.406 16.84C121.347 16.84 122.937 17.3627 124.174 18.408C125.411 19.432 126.179 20.8294 126.478 22.6H123.854C123.662 21.4694 123.182 20.584 122.414 19.944C121.646 19.304 120.643 18.984 119.406 18.984C117.699 18.984 116.377 19.6027 115.438 20.84C114.521 22.0774 114.062 23.6454 114.062 25.544C114.062 27.4427 114.521 29.0107 115.438 30.248C116.377 31.464 117.699 32.072 119.406 32.072C120.729 32.072 121.785 31.72 122.574 31.016C123.363 30.2907 123.811 29.2774 123.918 27.976H126.478C126.307 29.96 125.603 31.5067 124.366 32.616C123.129 33.704 121.475 34.248 119.406 34.248ZM129.984 33.8V10.92H132.576V33.8H129.984ZM144.232 34.248C141.779 34.248 139.827 33.4267 138.376 31.784C136.947 30.12 136.232 28.04 136.232 25.544C136.232 23.0907 136.957 21.032 138.408 19.368C139.88 17.6827 141.768 16.84 144.072 16.84C146.504 16.84 148.381 17.6934 149.704 19.4C151.027 21.0854 151.688 23.336 151.688 26.152H138.888C138.995 27.9014 139.507 29.3307 140.424 30.44C141.363 31.528 142.632 32.072 144.232 32.072C146.707 32.072 148.232 30.9414 148.808 28.68H151.336C150.952 30.4507 150.141 31.8267 148.904 32.808C147.667 33.768 146.109 34.248 144.232 34.248ZM147.528 20.296C146.696 19.3574 145.544 18.888 144.072 18.888C142.6 18.888 141.427 19.3787 140.552 20.36C139.699 21.3414 139.165 22.6214 138.952 24.2H148.904C148.819 22.536 148.36 21.2347 147.528 20.296ZM161.684 33.8V10.92H164.564V33.8H161.684ZM168.038 33.8L176.454 10.92H179.686L188.134 33.8H184.998L182.566 26.824H173.446L170.982 33.8H168.038ZM176.998 16.808L174.278 24.488H181.766L179.142 16.808L178.118 13.576H178.054C177.648 14.856 177.296 15.9334 176.998 16.808Z" fill="#F8F8F4"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
1
public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
11
tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
27
tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
2
types/pdfjs-dist.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare module "pdfjs-dist/legacy/build/pdf.min.mjs";
|
||||
declare module "pdfjs-dist/legacy/build/pdf.worker.min.mjs";
|
||||