import React, { useState, useRef, useEffect } from "react";
// PoliceDossierEditor - Single-file React component (finished)
// Usage: paste into a Create React App / Vite project (src/App.jsx)
// Optional styling: Tailwind CSS. If you don't use Tailwind, the component still works — classes are conservative.
// Required libs for PDF export: html2canvas, jspdf
// Install: npm i html2canvas jspdf
export default function App() {
// Simple auth: demo credentials (you can replace with real backend)
const DEMO_USER = { username: "admin", password: "demo123" };
const [user, setUser] = useState(() => {
const saved = localStorage.getItem("pd_user");
return saved ? JSON.parse(saved) : null;
});
const [cred, setCred] = useState({ username: "", password: "" });
// Document state - everything editable
const defaultDoc = {
title: "Dossier de Recrue",
entryDate: "20/07/2025",
rpName: "MAYET",
rpFirst: "TOMMY",
discord: "tommy.mayet2025",
rpAge: "35 ans",
birthDate: "10/02/1990",
nationality: "Francais",
gender: "Homme",
parcours: [
"Fiche d'inscription reçue",
"Acceptation officielle",
"Présentation faite",
"A attribué rôle Communication",
"Sécurité Armés / Tir",
"Ethique & Déontologie",
],
progress: {
Droit: "20/20",
Procedure: "20/20",
Technique: "20/20",
Psychologie: "20/20",
Securite: "20/20",
},
comments: "",
signature: "Mayet Tommy",
logoDataUrl: null,
};
const [doc, setDoc] = useState(() => {
const saved = localStorage.getItem("pd_doc");
return saved ? JSON.parse(saved) : defaultDoc;
});
// Edit mode toggles which UI is edit vs preview
const [editMode, setEditMode] = useState(true);
const previewRef = useRef();
useEffect(() => {
localStorage.setItem("pd_doc", JSON.stringify(doc));
}, [doc]);
useEffect(() => {
localStorage.setItem("pd_user", JSON.stringify(user));
}, [user]);
function login(e) {
e.preventDefault();
if (cred.username === DEMO_USER.username && cred.password === DEMO_USER.password) {
setUser({ username: cred.username });
setCred({ username: "", password: "" });
} else {
alert("Identifiants incorrects — utilisez admin / demo123 pour la démo");
}
}
function logout() {
setUser(null);
}
// Generic field updater (path supports nested keys with dot notation)
function updateField(path, value) {
setDoc((d) => {
const copy = JSON.parse(JSON.stringify(d));
const keys = path.split(".");
let cur = copy;
for (let i = 0; i < keys.length - 1; i++) cur = cur[keys[i]];
cur[keys[keys.length - 1]] = value;
return copy;
});
}
// Logo upload
function handleLogo(e) {
const f = e.target.files[0];
if (!f) return;
const reader = new FileReader();
reader.onload = () => updateField("logoDataUrl", reader.result);
reader.readAsDataURL(f);
}
// Download as PDF using html2canvas + jsPDF
async function downloadPdf() {
try {
const html2canvas = (await import("html2canvas")).default;
const { jsPDF } = await import("jspdf");
const node = previewRef.current;
if (!node) return;
// html2canvas renders the node to a canvas
const canvas = await html2canvas(node, { scale: 2, useCORS: true });
const imgData = canvas.toDataURL("image/png");
const pdf = new jsPDF({ orientation: "portrait", unit: "pt", format: "a4" });
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight);
const filename = (doc.rpName || 'dossier').split(' ').join('_') + '.pdf';
pdf.save(filename);
} catch (err) {
console.error(err);
alert("Erreur lors de la génération PDF — ouvrez la console pour plus de détails.");
}
}
// Export / Import JSON for full editability
function exportJson() {
const blob = new Blob([JSON.stringify(doc, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "dossier.json";
a.click();
URL.revokeObjectURL(url);
}
function importJson(e) {
const f = e.target.files[0];
if (!f) return;
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result);
setDoc(data);
alert("Fichier importé avec succès");
} catch (err) {
alert("Fichier JSON invalide");
}
};
reader.readAsText(f);
}
// Reset template
function resetTemplate() {
if (confirm("Remplacer le document par le modèle par défaut ?")) {
setDoc(defaultDoc);
}
}
// Helper: pretty button classes (works whether or not you have Tailwind)
const btnClass = "inline-block px-3 py-1 rounded bg-sky-600 text-white hover:opacity-95 text-sm";
return (
Police Dossier Editor
{user ? (
<>
Connecté: {user.username}
>
) : (
)}
Sauvegarde & utilisateurs
Démo: login admin / mot de passe demo123
Les données sont stockées localement (localStorage). Remplacez par un backend si besoin.
{/* Document preview — fully built from `doc` state */}
{doc.logoDataUrl ? (

) : (
Logo
)}
{doc.title}
Date d'entrée : {doc.entryDate}
Signature :
{doc.signature}
Informations générales
updateField("rpName", v)} editable={editMode && user} />
updateField("rpFirst", v)} editable={editMode && user} />
updateField("discord", v)} editable={editMode && user} />
updateField("rpAge", v)} editable={editMode && user} />
updateField("birthDate", v)} editable={editMode && user} />
updateField("nationality", v)} editable={editMode && user} />
updateField("gender", v)} editable={editMode && user} />
Progression pédagogique
{Object.entries(doc.progress).map(([k, v]) => (
))}
Parcours d'inscription
{editMode && user && (
)}
Signature
{doc.signature}
ÉCOLE DE POLICE NATIONALE — FRANCE
);
}
// Small presentational Field component
function Field({ label, value, onChange, editable }) {
return (
{label}
{editable ? (
onChange(e.target.value)} className="w-full border px-2 py-1 rounded" />
) : (
{value}
)}
);
}