espace-paie-odentas/components/BugReporter.tsx
2025-10-12 17:05:46 +02:00

264 lines
No EOL
9.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { Bug, X, Send, Camera, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import html2canvas from "html2canvas";
interface BugReportData {
subject: string;
description: string;
screenshot: string; // base64
url: string;
userAgent: string;
timestamp: string;
}
export function BugReporter() {
const [isOpen, setIsOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
subject: "",
description: "",
});
const [screenshot, setScreenshot] = useState<string | null>(null);
const [isCapturing, setIsCapturing] = useState(false);
const captureScreenshot = async () => {
setIsCapturing(true);
try {
// Fermer temporairement le modal pour la capture
setIsOpen(false);
// Attendre un peu pour que le modal se ferme
await new Promise(resolve => setTimeout(resolve, 300));
const canvas = await html2canvas(document.body, {
useCORS: true,
allowTaint: true,
scale: 0.5, // Réduire la qualité pour éviter des fichiers trop lourds
width: window.innerWidth,
height: window.innerHeight,
scrollX: 0,
scrollY: 0,
});
const base64 = canvas.toDataURL("image/jpeg", 0.7);
setScreenshot(base64);
// Rouvrir le modal
setIsOpen(true);
toast.success("Capture d'écran prise !");
} catch (error) {
console.error("Erreur lors de la capture:", error);
toast.error("Erreur lors de la capture d'écran");
setIsOpen(true);
} finally {
setIsCapturing(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.subject.trim() || !formData.description.trim()) {
toast.error("Veuillez remplir tous les champs");
return;
}
setIsSubmitting(true);
try {
const reportData: BugReportData = {
subject: formData.subject,
description: formData.description,
screenshot: screenshot || "",
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
};
const response = await fetch("/api/bug-report", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(reportData),
});
if (!response.ok) {
throw new Error("Erreur lors de l'envoi du rapport");
}
toast.success("Rapport de bug envoyé avec succès !");
setIsOpen(false);
setFormData({ subject: "", description: "" });
setScreenshot(null);
} catch (error) {
console.error("Erreur:", error);
toast.error("Erreur lors de l'envoi du rapport");
} finally {
setIsSubmitting(false);
}
};
const openModal = () => {
setIsOpen(true);
// Prendre automatiquement une capture dès l'ouverture
setTimeout(() => captureScreenshot(), 100);
};
return (
<>
{/* Bouton flottant */}
<button
onClick={openModal}
className="fixed bottom-6 right-6 z-50 w-14 h-14 bg-gradient-to-r from-red-500 to-pink-600 hover:from-red-600 hover:to-pink-700 text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-200 flex items-center justify-center group"
title="Signaler un bug"
disabled={isCapturing}
>
{isCapturing ? (
<Loader2 className="w-6 h-6 animate-spin" />
) : (
<Bug className="w-6 h-6 group-hover:scale-110 transition-transform" />
)}
</button>
{/* Modal */}
{isOpen && (
<div className="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center p-4">
<div className="bg-white rounded-2xl border max-w-2xl w-full max-h-[90vh] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-r from-red-500 to-pink-600 rounded-full flex items-center justify-center">
<Bug className="w-5 h-5 text-white" />
</div>
<div>
<h2 className="text-lg font-semibold">Signaler un problème</h2>
<p className="text-sm text-slate-500">Aidez-nous à améliorer l'application</p>
</div>
</div>
<button
onClick={() => setIsOpen(false)}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-slate-100 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-6 max-h-[calc(90vh-140px)] overflow-y-auto">
<form onSubmit={handleSubmit} className="space-y-4">
{/* Sujet */}
<div>
<label className="block text-sm font-medium mb-2">
Sujet <span className="text-red-500">*</span>
</label>
<input
type="text"
value={formData.subject}
onChange={(e) => setFormData(prev => ({ ...prev, subject: e.target.value }))}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Ex: Erreur lors du chargement des contrats"
maxLength={100}
/>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium mb-2">
Description détaillée <span className="text-red-500">*</span>
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
rows={4}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
placeholder="Décrivez le problème rencontré, les étapes pour le reproduire, et le comportement attendu..."
maxLength={1000}
/>
<div className="text-xs text-slate-500 mt-1">
{formData.description.length}/1000 caractères
</div>
</div>
{/* Capture d'écran */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium">Capture d'écran</label>
<button
type="button"
onClick={captureScreenshot}
disabled={isCapturing}
className="inline-flex items-center gap-2 text-sm text-blue-600 hover:text-blue-700 disabled:opacity-50"
>
{isCapturing ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Camera className="w-4 h-4" />
)}
{isCapturing ? "Capture en cours..." : "Nouvelle capture"}
</button>
</div>
{screenshot && (
<div className="border rounded-lg p-2">
<img
src={screenshot}
alt="Capture d'écran"
className="w-full h-32 object-cover rounded"
/>
<p className="text-xs text-slate-500 mt-2">
Capture automatique de la page actuelle
</p>
</div>
)}
</div>
{/* Informations automatiques */}
<div className="bg-slate-50 rounded-lg p-3">
<p className="text-sm font-medium mb-2">Informations techniques (ajoutées automatiquement)</p>
<div className="text-xs text-slate-600 space-y-1">
<div><strong>URL:</strong> {window.location.href}</div>
<div><strong>Navigateur:</strong> {navigator.userAgent.split(' ')[0]}</div>
<div><strong>Heure:</strong> {new Date().toLocaleString('fr-FR')}</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => setIsOpen(false)}
className="flex-1"
>
Annuler
</Button>
<Button
type="submit"
disabled={isSubmitting || !formData.subject.trim() || !formData.description.trim()}
className="flex-1 bg-gradient-to-r from-red-500 to-pink-600 hover:from-red-600 hover:to-pink-700"
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Envoi en cours...
</>
) : (
<>
<Send className="w-4 h-4 mr-2" />
Envoyer le rapport
</>
)}
</Button>
</div>
</form>
</div>
</div>
</div>
)}
</>
);
}