diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx index 3773b9f..4d78529 100644 --- a/client/src/pages/Settings.tsx +++ b/client/src/pages/Settings.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import DashboardLayout from "@/components/DashboardLayout"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -25,7 +25,7 @@ import { Upload, User } from "lucide-react"; -import { useRef, useState as useStateAlias } from "react"; +// useRef, useCallback already imported above import { toast } from "sonner"; import { Checkbox } from "@/components/ui/checkbox"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; @@ -34,13 +34,93 @@ function SignaturesSection() { const utils = trpc.useUtils(); const { data: signatures, isLoading } = trpc.signatures.list.useQuery(); - const [firstName, setFirstName] = useStateAlias(""); - const [lastName, setLastName] = useStateAlias(""); - const [previewUrl, setPreviewUrl] = useStateAlias(null); - const [fileData, setFileData] = useStateAlias(null); - const [fileName, setFileName] = useStateAlias(""); - const [mimeType, setMimeType] = useStateAlias("image/png"); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [mode, setMode] = useState<"file" | "draw">("draw"); + const [previewUrl, setPreviewUrl] = useState(null); + const [fileData, setFileData] = useState(null); + const [fileName, setFileName] = useState(""); + const [mimeType, setMimeType] = useState("image/png"); const fileInputRef = useRef(null); + const canvasRef = useRef(null); + const isDrawingRef = useRef(false); + const lastPosRef = useRef<{ x: number; y: number } | null>(null); + + // Initialize canvas + useEffect(() => { + if (mode !== "draw") return; + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.strokeStyle = "#1e293b"; + ctx.lineWidth = 2; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + }, [mode]); + + const getPos = useCallback((e: React.MouseEvent | React.TouchEvent, canvas: HTMLCanvasElement) => { + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + if ("touches" in e) { + const touch = e.touches[0]; + return { x: (touch.clientX - rect.left) * scaleX, y: (touch.clientY - rect.top) * scaleY }; + } + return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY }; + }, []); + + const startDrawing = useCallback((e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault(); + const canvas = canvasRef.current; + if (!canvas) return; + isDrawingRef.current = true; + lastPosRef.current = getPos(e, canvas); + }, [getPos]); + + const draw = useCallback((e: React.MouseEvent | React.TouchEvent) => { + e.preventDefault(); + if (!isDrawingRef.current) return; + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx || !lastPosRef.current) return; + const pos = getPos(e, canvas); + ctx.beginPath(); + ctx.moveTo(lastPosRef.current.x, lastPosRef.current.y); + ctx.lineTo(pos.x, pos.y); + ctx.stroke(); + lastPosRef.current = pos; + }, [getPos]); + + const stopDrawing = useCallback(() => { + isDrawingRef.current = false; + lastPosRef.current = null; + }, []); + + const clearCanvas = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + setFileData(null); + setPreviewUrl(null); + }, []); + + const captureCanvas = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const dataUrl = canvas.toDataURL("image/png"); + const base64 = dataUrl.split(",")[1]; + setFileData(base64); + setPreviewUrl(dataUrl); + setFileName("signature-dessinee.png"); + setMimeType("image/png"); + }, []); const uploadMutation = trpc.signatures.upload.useMutation({ onSuccess: () => { @@ -96,11 +176,23 @@ function SignaturesSection() { toast.error("Veuillez renseigner le prénom et le nom"); return; } - if (!fileData) { - toast.error("Veuillez sélectionner une image de signature"); + // In draw mode, capture canvas first + let finalFileData = fileData; + let finalFileName = fileName; + let finalMimeType = mimeType; + if (mode === "draw") { + const canvas = canvasRef.current; + if (!canvas) { toast.error("Erreur canvas"); return; } + const dataUrl = canvas.toDataURL("image/png"); + finalFileData = dataUrl.split(",")[1]; + finalFileName = "signature-dessinee.png"; + finalMimeType = "image/png"; + } + if (!finalFileData) { + toast.error(mode === "draw" ? "Veuillez dessiner votre signature" : "Veuillez sélectionner une image de signature"); return; } - uploadMutation.mutate({ firstName: firstName.trim(), lastName: lastName.trim(), fileName, fileData, mimeType }); + uploadMutation.mutate({ firstName: firstName.trim(), lastName: lastName.trim(), fileName: finalFileName, fileData: finalFileData, mimeType: finalMimeType }); }; return ( @@ -124,6 +216,8 @@ function SignaturesSection() { {/* Add form */}

Ajouter une signature

+ + {/* Nom / Prénom */}
@@ -146,38 +240,100 @@ function SignaturesSection() { />
-
- -
fileInputRef.current?.click()} + + {/* Mode selector */} +
+
- {previewUrl && ( - - )} + Dessiner + +
+ + {/* Draw mode */} + {mode === "draw" && ( +
+ +
+ +
+ Signez ici +
+
+ +
+ )} + + {/* File mode */} + {mode === "file" && ( +
+ +
fileInputRef.current?.click()} + > + {previewUrl ? ( + Aperçu signature + ) : ( +
+ + Cliquez pour sélectionner une image (PNG, JPG, GIF...) + Taille max : 2 Mo +
+ )} + +
+ {previewUrl && ( + + )} +
+ )} +