import React, { useState, useRef, useEffect } from 'react'; import Cropper from 'react-cropper'; import 'cropperjs/dist/cropper.css'; import axios from 'axios'; import { IMaskInput } from 'react-imask'; // ─── Config du cropper par catégorie ───────────────────────────────────────── // aspectRatio : ratio width/height du cadre de rognage // outputWidth / outputHeight : dimensions de l'image sauvegardée // hint : message affiché à l'utilisateur const CROP_CONFIGS = { // Documents d'identité – format carte ID (85.6 x 54 mm) 'CNI': { aspectRatio: 85.6 / 54, outputWidth: 856, outputHeight: 540, hint: 'Format carte d\'identité (ratio 85×54)' }, 'Passeport': { aspectRatio: 3 / 4, outputWidth: 600, outputHeight: 800, hint: 'Format portrait passeport (3×4)' }, 'Permis de conduire':{ aspectRatio: 85.6 / 54, outputWidth: 856, outputHeight: 540, hint: 'Format carte de permis (ratio 85×54)' }, 'Documents': { aspectRatio: 3 / 4, outputWidth: 600, outputHeight: 800, hint: 'Format document portrait (3×4)' }, // Électronique – carré (pour montrer l\u2019écran / dos) 'Electronique': { aspectRatio: 1 / 1, outputWidth: 700, outputHeight: 700, hint: 'Format carré – idéal pour appareils électroniques' }, 'Téléphone': { aspectRatio: 9 / 16, outputWidth: 450, outputHeight: 800, hint: 'Format portrait – idéal pour smartphones' }, // Valeurs & clés – format paysage 'Valeurs': { aspectRatio: 4 / 3, outputWidth: 800, outputHeight: 600, hint: 'Format paysage – idéal pour portefeuille / contenu' }, 'Clés': { aspectRatio: 4 / 3, outputWidth: 800, outputHeight: 600, hint: 'Format paysage – idéal pour trousseau de clés' }, // Défaut (Autre, non défini) default: { aspectRatio: 16 / 9, outputWidth: 900, outputHeight: 506, hint: 'Format standard 16×9' }, }; const getCropConfig = (categoryId) => CROP_CONFIGS[categoryId] ?? CROP_CONFIGS['default']; export default function ObjetTrouveNew(props) { const [step, setStep] = useState(1); const [formData, setFormData] = useState({ title: '', category: '', location: '', adresse: props.userAdresse || '', latitude: '', longitude: '', dateFound: new Date().toISOString().slice(0, 16), description: '', documentType: '', deposeurTelephone: '', deposeurNom: '', deposeurDescription: '', }); const maxLengths = { title: 100, location: 100, adresse: 100, description: 500, deposeurNom: 80, deposeurDescription: 500 }; const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); const [imageFiles, setImageFiles] = useState([]); // Original files (mostly for names) const [dynamicFields, setDynamicFields] = useState({}); const [originalUrls, setOriginalUrls] = useState([]); // Original data URLs for re-cropping const [croppedBlobs, setCroppedBlobs] = useState([]); // Resulting blobs const [previewUrls, setPreviewUrls] = useState([]); // Cropped previews const [mainImageIndex, setMainImageIndex] = useState(0); const [activeCropIndex, setActiveCropIndex] = useState(null); const [tempPreviewUrl, setTempPreviewUrl] = useState(null); // For the image being cropped const cropperRef = useRef(null); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(null); const [categories, setCategories] = useState([]); useEffect(() => { axios.get('/api-categories') .then(response => setCategories(response.data)) .catch(error => console.error("Error fetching categories", error)); }, []); /*const categories = [ { id: 'Valeurs', label: 'Portefeuille', icon: 'wallet', desc: 'Cartes, espèces' }, { id: 'Clés', label: 'Clés', icon: 'key', desc: 'Maison, voiture' }, { id: 'Electronique', label: 'Électronique', icon: 'smartphone', desc: 'Téléphone, PC' }, { id: 'Documents', label: 'Documents', icon: 'description', desc: ', Passeport' }, { id: 'CNI', label: 'CNI', icon: 'badge', desc: 'Carte nationale d\'identité' }, { id: 'Passeport', label: 'Passeport', icon: 'book', desc: 'Document de voyage' }, { id: 'Permis de conduire', label: 'Permis', icon: 'drive_eta', desc: 'Permis de conduire' }, { id: 'Autre', label: 'Autre', icon: 'category', desc: 'Objet divers' }, ];*/ const validateField = (name, value) => { if (['title', 'location', 'description', 'deposeurDescription'].includes(name)) { if (value && value.trim().length > 0 && value.trim().length < 3) { return 'Ce champ doit contenir au moins 3 caractères.'; } } if (['description', 'deposeurDescription', 'title', 'location', 'adresse', 'deposeurNom'].includes(name)) { if (value && value.trim().length > maxLengths[name]) { return `Ce champ ne doit pas dépasser ${maxLengths[name]} caractères.`; } } if (name === 'deposeurTelephone') { if (value && value.trim().length > 0 && !/^\+221\s7\d\s\d{3}\s\d{2}\s\d{2}$/.test(value)) { return 'Veuillez saisir le numéro de téléphone complet.'; } } if (name === 'deposeurNom') { if (value && value.trim().length > 0 && value.trim().length < 2) { return 'Le nom doit contenir au moins 2 caractères.'; } } return null; }; const handleChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); if (touched[name]) { setErrors(prev => ({ ...prev, [name]: validateField(name, value) })); } }; const handleBlur = (e) => { const { name, value } = e.target; setTouched(prev => ({ ...prev, [name]: true })); setErrors(prev => ({ ...prev, [name]: validateField(name, value) })); }; const handleCategorySelect = (categoryId) => { setFormData(prev => ({ ...prev, category: categoryId })); }; const handleDynamicFieldChange = (fieldId, value) => { setDynamicFields(prev => ({ ...prev, [fieldId]: value })); }; const selectedCategory = props.categories?.find(c => c.id === formData.category) || null; const handleImageChange = (e) => { if (e.target.files && e.target.files.length > 0) { const file = e.target.files[0]; const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { setTempPreviewUrl(reader.result); setActiveCropIndex(imageFiles.length); // Next available index }; } }; const handleCrop = async () => { if (cropperRef.current && cropperRef.current.cropper) { const cropConfig = getCropConfig(formData.category); const canvas = cropperRef.current.cropper.getCroppedCanvas({ width: cropConfig.outputWidth, height: cropConfig.outputHeight }); if (canvas) { const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.9)); const dataUrl = canvas.toDataURL('image/jpeg'); if (activeCropIndex < imageFiles.length) { // Updating an existing image setCroppedBlobs(prev => { const next = [...prev]; next[activeCropIndex] = blob; return next; }); setPreviewUrls(prev => { const next = [...prev]; next[activeCropIndex] = dataUrl; return next; }); } else { // Adding a new image setCroppedBlobs(prev => [...prev, blob]); setPreviewUrls(prev => [...prev, dataUrl]); setOriginalUrls(prev => [...prev, tempPreviewUrl]); setImageFiles(prev => [...prev, { name: `image-${Date.now()}.jpg` }]); } setActiveCropIndex(null); setTempPreviewUrl(null); } } }; const cancelCrop = () => { setActiveCropIndex(null); setTempPreviewUrl(null); }; const removeImage = (index) => { setImageFiles(prev => prev.filter((_, i) => i !== index)); setOriginalUrls(prev => prev.filter((_, i) => i !== index)); setCroppedBlobs(prev => prev.filter((_, i) => i !== index)); setPreviewUrls(prev => prev.filter((_, i) => i !== index)); if (mainImageIndex === index) setMainImageIndex(0); else if (mainImageIndex > index) setMainImageIndex(mainImageIndex - 1); }; const editImage = (index) => { setTempPreviewUrl(originalUrls[index]); setActiveCropIndex(index); }; const resetForm = () => { setFormData({ title: '', category: '', location: '', adresse: props.userAdresse || '', latitude: '', longitude: '', dateFound: new Date().toISOString().slice(0, 16), description: '', documentType: '', deposeurTelephone: '', deposeurNom: '', deposeurDescription: '', }); setStep(1); setDynamicFields({}); setImageFiles([]); setOriginalUrls([]); setCroppedBlobs([]); setPreviewUrls([]); setSuccess(false); setError(null); setMainImageIndex(0); setActiveCropIndex(null); setTempPreviewUrl(null); setTimeout(scrollToForm, 100); }; const scrollToForm = () => { const formElement = document.getElementById('report-found-object-form'); if (formElement) { const y = formElement.getBoundingClientRect().top + window.scrollY - 100; window.scrollTo({ top: y, behavior: 'smooth' }); } else { window.scrollTo({ top: 0, behavior: 'smooth' }); } }; const handleNext = () => { setStep(prev => Math.min(prev + 1, 3)); setTimeout(scrollToForm, 100); }; const handlePrev = () => { setStep(prev => Math.max(prev - 1, 1)); setTimeout(scrollToForm, 100); }; const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); setError(null); const data = new FormData(); const requestData = { ...formData }; Object.keys(requestData).forEach(key => data.append(key, requestData[key])); data.append('mainImageIndex', mainImageIndex); data.append('dynamicFields', JSON.stringify(dynamicFields)); for (let i = 0; i < croppedBlobs.length; i++) { data.append('imageFiles[]', croppedBlobs[i], `image-${i}.jpg`); } try { const response = await axios.post(props.submitUrl, data); const result = response.data; if (result.success) { setSuccess(true); } else { setError(result.error || 'Une erreur est survenue'); } } catch (err) { setError('Erreur de connexion au serveur'); } finally { setLoading(false); } }; if (success) { return (
Merci de votre aide. Votre signalement est maintenant visible par la communauté.
Sélectionnez la catégorie qui correspond le mieux.
{cat.description}
Précisez le lieu exact pour faciliter l'identification par le propriétaire.
Ajoutez une photo claire pour certifier l'objet trouvé.
Ajouter