264 lines
No EOL
9.7 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
} |