import React, { useState, useRef, useEffect, useCallback } from 'react'; import Cropper from 'react-cropper'; import 'cropperjs/dist/cropper.css'; import axios from 'axios'; import { IMaskInput } from 'react-imask'; export default function ObjetTrouveEdit(props) { const [formData, setFormData] = useState({ id: props.initialData?.id || '', title: props.initialData?.titre || '', category: props.initialData?.categoryId || '', location: props.initialData?.lieuTrouve || '', dateFound: props.initialData?.trouveAt || '', description: props.initialData?.description || '', deposeurTelephone: props.initialData?.deposeurTelephone || '', deposeurNom: props.initialData?.deposeurNom || '', deposeurDescription: props.initialData?.deposeurDescription || '', adresse: props.initialData?.adresse || '', }); const maxLengths = { title: 100, location: 100, adresse: 100, description: 500, deposeurNom: 80, deposeurDescription: 500 }; const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); // Each slot can be: { id: 123, raw: 'file.jpg', public: 'file_p.jpg', isMasked: true, isNew: false, rawFile: null, publicFile: null } const [slots, setSlots] = useState(() => { const initial = (props.initialData?.images || []).map(img => ({ id: img.id, raw: img.raw, public: img.public, isMasked: img.isMasked, isNew: false, rawFile: null, publicFile: null, position: img.position })); // Sort by position return initial.sort((a, b) => a.position - b.position); }); const [mainImageIndex, setMainImageIndex] = useState(() => { const main = slots.findIndex(s => s.position === 0); return main !== -1 ? main : 0; }); const [dynamicFields, setDynamicFields] = useState(props.initialDynamicFields || {}); const [activeCrop, setActiveCrop] = useState(null); // { index: 0, type: 'raw' | 'public', src: 'data:...' } const cropperRef = useRef(null); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(null); const validateField = (name, value) => { const safeValue = value || ''; if (['description', 'deposeurDescription', 'title', 'location', 'adresse', 'deposeurNom'].includes(name)) { if (safeValue && safeValue.trim().length > maxLengths[name]) { return `Ce champ ne doit pas dépasser ${maxLengths[name]} caractères.`; } } if (['title', 'location', 'description', 'deposeurDescription'].includes(name)) { if (safeValue && safeValue.trim().length > 0 && safeValue.trim().length < 3) { return 'Ce champ doit contenir au moins 3 caractères.'; } } if (name === 'deposeurTelephone') { const phoneDigits = safeValue.replace(/\D/g, ''); const unmasked = safeValue.replace(/[\s\-_]/g, ''); if (safeValue && safeValue.trim().length > 0) { if (phoneDigits.length < 12 || !/^\+221(7[05678]|30|33)\d{7}$/.test(unmasked)) { return 'Veuillez saisir le numéro de téléphone complet Sénégalais.'; } } } if (name === 'deposeurNom') { if (safeValue && safeValue.trim().length > 0 && safeValue.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 })); const errorMsg = validateField(name, value); setErrors(prev => ({ ...prev, [name]: errorMsg })); }; const handleBlur = (e) => { const { name, value } = e.target; setTouched(prev => ({ ...prev, [name]: true })); const errorMsg = validateField(name, value); setErrors(prev => ({ ...prev, [name]: errorMsg })); }; const handleDynamicFieldChange = (fieldId, value) => { setDynamicFields(prev => ({ ...prev, [fieldId]: value })); }; const selectedCategory = props.categories?.find(c => c.id == formData.category) || null; const handleFileSelect = (index, type, 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 = () => { setActiveCrop({ index, type, src: reader.result }); }; } }; const handleCropExec = async () => { if (cropperRef.current && cropperRef.current.cropper) { const canvas = cropperRef.current.cropper.getCroppedCanvas({ width: 800, height: 600 }); if (canvas) { const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.9)); const dataUrl = canvas.toDataURL('image/jpeg'); setSlots(prev => { const next = [...prev]; const slot = { ...(next[activeCrop.index] || { isNew: true, position: activeCrop.index }) }; slot.rawPreview = dataUrl; slot.rawFile = blob; next[activeCrop.index] = slot; return next; }); setActiveCrop(null); } } }; const removeSlot = (index) => { if (slots.length <= 1) { setError("Il doit y avoir au moins une image."); return; } setSlots(prev => prev.filter((_, i) => i !== index)); if (mainImageIndex === index) setMainImageIndex(0); else if (mainImageIndex > index) setMainImageIndex(mainImageIndex - 1); }; const handleSubmit = async (e) => { e.preventDefault(); if (slots.length === 0) { setError("Veuillez ajouter au moins une image."); return; } setLoading(true); setError(null); const data = new FormData(); Object.keys(formData).forEach(key => data.append(key, formData[key])); data.append('mainImageIndex', mainImageIndex); data.append('dynamicFields', JSON.stringify(dynamicFields)); // Metadata about slots const slotsMetadata = slots.map((slot, index) => ({ id: slot.id || null, isNew: !!slot.isNew, hasRawFile: !!slot.rawFile, })); data.append('slotsMetadata', JSON.stringify(slotsMetadata)); slots.forEach((slot, index) => { if (slot.rawFile) data.append(`rawFiles[${index}]`, slot.rawFile, `raw-${index}.jpg`); }); try { const response = await axios.post(props.submitUrl, data); if (response.data.success) { if (e.nativeEvent.submitter?.name === 'saveAndExit') { window.location.href = props.redirectUrl; } else { setSuccess(true); } } else { setError(response.data.error || 'Une erreur est survenue'); } } catch (err) { setError(err.response?.data?.error || 'Erreur lors de la mise à jour'); } finally { setLoading(false); } }; if (success) { return (
Éditez les informations et gérez les visuels publics/privés.