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 (

Signalement Publié !

Merci de votre aide. Votre signalement est maintenant visible par la communauté.

Voir mes objets
); } return (
Progression
Étape {step} sur 3
{error && (
{error}
)}
{/* Step 1 */} {step === 1 && (

Étape 1: Que contient l'objet ? *

Sélectionnez la catégorie qui correspond le mieux.

{props.categories.map(cat => (
handleCategorySelect(cat.id)} className={`card h-100 text-center cursor-pointer transition-all ${formData.category === cat.id ? 'border-primary' : 'border-light'}`} style={{ borderRadius: '15px', border: `2px solid ${formData.category === cat.id ? 'var(--vert-senegal)' : '#f8f9fa'}`, backgroundColor: formData.category === cat.id ? 'rgba(13, 127, 242, 0.05)' : 'white', cursor: 'pointer', padding: '10px' }} >
{cat.icon}
{cat.nom}

{cat.description}

))}
{formData.category === 'Autre' && (
Astuce : Indiquez le type d'objet directement dans le titre ci-dessous.
)}
{errors.title ? (
{errors.title}
) : ( Soyez précis pour faciliter la recherche (ex: marque, modèle). )}
= maxLengths.title ? 'text-danger font-weight-bold' : 'text-muted'}> {formData.title.length}/{maxLengths.title}
{errors.adresse ? (
{errors.adresse}
) : ( Cette adresse pourra être associée à votre profil. )}
= maxLengths.adresse ? 'text-danger font-weight-bold' : 'text-muted'}> {formData.adresse.length}/{maxLengths.adresse}
{selectedCategory?.fields?.length > 0 && (
Informations spécifiques ({selectedCategory.nom})
{selectedCategory.fields.map(field => (
handleDynamicFieldChange(field.id, e.target.value)} className="form-control" required={field.estObligatoire} />
))}
)} {props.isAgent && (
Informations du Déposeur (Optionnel)
{ setFormData(prev => ({ ...prev, deposeurTelephone: value })); }} onBlur={handleBlur} className={`form-control ${errors.deposeurTelephone ? 'is-invalid' : ''}`} placeholder="+221 7X XXX XX XX" /> {errors.deposeurTelephone && (
{errors.deposeurTelephone}
)}
{errors.deposeurNom && (
{errors.deposeurNom}
)}
= maxLengths.deposeurNom ? 'text-danger font-weight-bold' : 'text-muted'}> {formData.deposeurNom.length}/{maxLengths.deposeurNom}
{errors.deposeurDescription && (
{errors.deposeurDescription}
)}
= maxLengths.deposeurDescription ? 'text-danger font-weight-bold' : 'text-muted'}> {formData.deposeurDescription.length}/{maxLengths.deposeurDescription}
)}
)} {/* Step 2 */} {step === 2 && (

Étape 2: Où et quand l'avez-vous trouvé ?

Précisez le lieu exact pour faciliter l'identification par le propriétaire.

{errors.location ? (
{errors.location}
) : ( Nom du quartier, rue ou point de repère. )}
= maxLengths.location ? 'text-danger font-weight-bold' : 'text-muted'}> {formData.location.length}/{maxLengths.location}
)} {/* Step 3 */} {step === 3 && (

Étape 3: Description & Photo

Ajoutez une photo claire pour certifier l'objet trouvé.

{errors.description ? (
{errors.description}
) : ( Ajoutez tout détail utile (couleur, signes particuliers) sans trop en dire. )}
= maxLengths.description ? 'text-danger font-weight-bold' : 'text-muted'}> {formData.description.length}/{maxLengths.description}
{activeCropIndex !== null ? (
{/* Hint catégorie */} {(() => { const cfg = getCropConfig(formData.category); return (
{cfg.hint} Sortie : {cfg.outputWidth} × {cfg.outputHeight} px
); })()}
) : (
{previewUrls.map((url, index) => (
{`Preview
{mainImageIndex === index ? (
PRINCIPALE
) : ( )}
))} {imageFiles.length < 3 && (

Ajouter

)}
)} Sélectionnez jusqu'à 3 photos. Chaque photo doit être recadrée avant validation.
)}
); }