ARTICLE AD BOX
In my component, I am trying to upload/edit media. When I submit the form, in Firestore, it sets the previous media to null and just updates the new media. See snippets below, the handleRemoveMedia and the handleAddMedia are in the component, and the handleEditMedia is in my hook
function handleRemoveMedia(type, index) { const currentMedia = getValues('media') || {}; if (type === 'galleryPhotos') { const currentPhotos = currentMedia.galleryPhotos || []; const newPhotos = [...currentPhotos]; newPhotos.splice(index, 1); setValue('media', { ...currentMedia, galleryPhotos: newPhotos }); } if (type === 'additionalVideos') { const currentVideos = currentMedia.additionalVideos || []; const newVideos = [...currentVideos]; newVideos.splice(index, 1); setValue('media', { ...currentMedia, additionalVideos: newVideos }); } if (type === 'coverMedia') { setValue('media', { ...currentMedia, coverMedia: null }); } } function handleAddMedia(type, event) { let files = []; if (event?.target?.files) { files = Array.from(event.target.files); } else if (Array.isArray(event)) { files = event; } else { files = [event]; } const currentMedia = getValues('media') || {}; if (type === 'coverMedia') { const file = files[0]; if (!file) return; const maxSize = 50 * 1024 * 1024; if (file.size > maxSize) { alert('Cover media file must not exceed 50MB.'); return; } const previewUrl = URL.createObjectURL(file); const coverMediaObj = { file, previewUrl, type: file.type.startsWith('image/') ? 'image' : 'video', }; setValue('media', { ...currentMedia, coverMedia: coverMediaObj, }); } if (type === 'galleryPhotos') { const validFiles = files.filter( (file) => file.type && ['image/jpeg', 'image/png', 'image/jpg'].includes(file.type) ); const currentPhotos = currentMedia.galleryPhotos || []; if (validFiles.length + currentPhotos.length > 6) { alert('You can only upload a maximum of 6 images.'); return; } const newPhotos = validFiles.map((file) => { const previewUrl = URL.createObjectURL(file); return { file, previewUrl }; }); setValue('media', { ...currentMedia, galleryPhotos: [...currentPhotos, ...newPhotos], }); } if (type === 'additionalVideos') { const maxSize = 50 * 1024 * 1024; const validFiles = files.filter((file) => file.type && file.type.startsWith('video/')); const oversized = validFiles.find((file) => file.size > maxSize); if (oversized) { alert('Each video file must not exceed 50MB.'); return; } const currentVideos = currentMedia.additionalVideos || []; if (validFiles.length + currentVideos.length > 6) { alert('You can only upload a maximum of 6 videos.'); return; } const newVideos = validFiles.map((file) => { const previewUrl = URL.createObjectURL(file); return { file, previewUrl }; }); setValue('media', { ...currentMedia, additionalVideos: [...currentVideos, ...newVideos], }); } } // Here is the JSX: <Typography sx={{ mb: 2, fontWeight: 'bold', color: '#F24F14' }}>Media</Typography> <Stack spacing={2} sx={{ mb: 3 }}> <Typography sx={{ fontWeight: 'bold' }}>Cover Media</Typography> {!watch('media')?.coverMedia && ( <Button variant="outlined" component="label" sx={{ borderRadius: '50px', py: 1 }}> Add Cover Media <input type="file" accept="image/*,video/*" hidden multiple onChange={(e) => handleAddMedia('coverMedia', e)} /> </Button> )} {watch('media')?.coverMedia && ( <Box sx={{ position: 'relative', width: 200, height: 150, mb: 2, borderRadius: 2, overflow: 'hidden', background: watch('media').coverMedia?.type === 'image' ? undefined : '#000', }} > {watch('media').coverMedia?.type === 'image' ? ( <img src={watch('media').coverMedia.previewUrl || watch('media').coverMedia} alt="Cover" style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: 8 }} /> ) : ( <video src={watch('media').coverMedia.previewUrl || watch('media').coverMedia} controls style={{ width: '100%', height: '100%', display: 'block', borderRadius: 0 }} > <track kind="captions" label="English captions" srcLang="en" src="" default /> </video> )} <IconButton sx={{ position: 'absolute', top: 8, right: 8, backgroundColor: 'rgba(255,255,255,0.9)', width: 28, height: 28, '&:hover': { backgroundColor: 'rgba(255,255,255,1)' }, }} onClick={() => handleRemoveMedia('coverMedia')} > <Iconify icon={'solar:trash-bin-trash-linear'} width={18} height={18} style={{ color: '#F24F14' }} /> </IconButton> </Box> )} <Typography sx={{ fontWeight: 'bold', mt: 2 }}>Gallery Photos</Typography> {(watch('media')?.galleryPhotos?.length ?? 0) < 6 && ( <Button variant="outlined" component="label" sx={{ borderRadius: '50px', py: 1 }}> Add Gallery Photos <input type="file" accept="image/*" hidden multiple onChange={(e) => handleAddMedia('galleryPhotos', e)} /> </Button> )} <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}> {watch('media')?.galleryPhotos?.length > 0 && watch('media').galleryPhotos.map((photo, index) => ( <Box key={index} sx={{ position: 'relative', mb: 2 }}> <img src={photo.previewUrl || photo.url || photo} alt={`Gallery ${index + 1}`} style={{ width: 200, height: 150, objectFit: 'cover', borderRadius: 8 }} /> <IconButton sx={{ position: 'absolute', top: 8, right: 8, backgroundColor: 'rgba(255,255,255,0.9)', width: 28, height: 28, '&:hover': { backgroundColor: 'rgba(255,255,255,1)' }, }} onClick={() => handleRemoveMedia('galleryPhotos', index)} > <Iconify icon={'solar:trash-bin-trash-linear'} width={18} height={18} style={{ color: '#F24F14' }} /> </IconButton> </Box> ))} </Box> <Typography sx={{ fontWeight: 'bold', mt: 2 }}>Additional Videos</Typography> {(watch('media')?.additionalVideos?.length ?? 0) < 6 && ( <Button variant="outlined" component="label" sx={{ borderRadius: '50px', py: 1 }}> Add Additional Videos <input type="file" accept="video/*" hidden multiple onChange={(e) => handleAddMedia('additionalVideos', e)} /> </Button> )} <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}> {watch('media')?.additionalVideos?.length > 0 && watch('media').additionalVideos.map((video, index) => ( <Box key={index} sx={{ position: 'relative', mb: 2 }}> <Box sx={{ width: 200, height: 150, borderRadius: 2, overflow: 'hidden', background: '#000' }}> <video src={video.previewUrl || video.url || video} controls style={{ width: '100%', height: '100%', display: 'block', borderRadius: 0 }} > <track kind="captions" label="English captions" srcLang="en" src="" default /> </video> </Box> <IconButton sx={{ position: 'absolute', top: 8, right: 8, backgroundColor: 'rgba(255,255,255,0.9)', width: 28, height: 28, '&:hover': { backgroundColor: 'rgba(255,255,255,1)' }, }} onClick={() => handleRemoveMedia('additionalVideos', index)} > <Iconify icon={'solar:trash-bin-trash-linear'} width={18} height={18} style={{ color: '#F24F14' }} /> </IconButton> </Box> ))} </Box> </Stack> // handleEditMedia in hook: const handleEditListing = async (listing, formData) => { try { const coverMediaToDelete = listing.media.coverMedia && formData.media.coverMedia?.url !== listing.media.coverMedia.url ? listing.media.coverMedia : null; const galleryImagesToDelete = []; for (let i = 0; i < listing.media.galleryPhotos.length; i += 1) { const oldImage = listing.media.galleryPhotos[i]; let imageFound = false; for (let j = 0; j < formData.media.galleryPhotos.length; j += 1) { const newImg = formData.media.galleryPhotos[j]; if (newImg.url === oldImage.url) { imageFound = true; break; } } if (!imageFound) { galleryImagesToDelete.push(oldImage); } } const galleryVideosToDelete = []; for (let i = 0; i < listing.media.additionalVideos.length; i += 1) { const oldVideo = listing.media.additionalVideos[i]; let videoFound = false; for (let j = 0; j < formData.media.additionalVideos.length; j += 1) { const newVid = formData.media.additionalVideos[j]; if (newVid.url === oldVideo.url) { videoFound = true; break; } } if (!videoFound) { galleryVideosToDelete.push(oldVideo); } } if (coverMediaToDelete) { await deleteCoverMedia(listing.id, coverMediaToDelete.name); } if (galleryImagesToDelete.length > 0) { await Promise.all( galleryImagesToDelete.map(async (url) => { try { await deleteGalleryMedia(listing.id, url); } catch (error) { console.error('Error deleting gallery image:', error); } }) ); } if (galleryVideosToDelete.length > 0) { await Promise.all( galleryVideosToDelete.map(async (url) => { try { await deleteVideoMedia(listing.id, url); } catch (error) { console.error('Error deleting gallery video:', error); } }) ); } const galleryImageUrls = await Promise.all( formData.media.galleryPhotos.map(async (photo) => { try { if (photo.file instanceof File) { return await uploadGalleryMedia(listing.id, photo.file); } if (photo.url) { return photo; } return null; } catch (error) { console.error('Error uploading gallery image:', error); return null; } }) ); const galleryVideoUrls = await Promise.all( formData.media.additionalVideos.map(async (video) => { try { if (video.file instanceof File) { return await uploadVideoMedia(listing.id, video.file); } if (video.url) { return video; } return null; } catch (error) { console.error('Error uploading gallery video:', error); return null; } }) ); let coverMediaUrl; if (formData.media.coverMedia?.file instanceof File) { coverMediaUrl = await uploadCoverMedia(listing.id, formData.media.coverMedia.file); } else if (formData.media.coverMedia?.url) { coverMediaUrl = formData.media.coverMedia; } else { coverMediaUrl = formData.media.coverMedia || null; } const listingData = { ...listing, basicDetails: { ...listing.basicDetails, experienceName: formData.experienceName, category: formData.category, secondaryCategory: formData.secondaryCategory, location: formData.location, shortDescription: formData.shortDescription, }, experienceDetails: { ...listing.experienceDetails, whatGuestsWillDo: formData.whatGuestsWillDo, whatMakesItSpecial: formData.whatMakesItSpecial, whoThisIsFor: formData.whoThisIsFor, privateBooking: formData.privateBooking, minimumSeatThreshold: formData.minimumSeatThreshold, maximumGroupSize: formData.maximumGroupSize, perPersonPrice: formData.perPersonPrice, duration: { type: formData.durationType, hours: formData.durationHours, minutes: formData.durationMinutes, }, whatsIncluded: formData.whatsIncluded, whatsNotIncluded: formData.whatsNotIncluded, languagesOffered: formData.languagesOffered, guestRequirements: formData.guestRequirements, whatToBring: formData.whatToBring, specialNotes: formData.specialNotes, }, schedule: { ...listing.schedule, sameScheduleEveryday: formData.sameScheduleEveryday, operatingDays: formData.operatingDays, timeSlots: formData.timeSlots, maxBookingsPerSlot: formData.maxBookingsPerSlot, }, pricings: { ...listing.pricings, currency: formData.currency, basePrice: formData.basePrice, additionalFees: formData.additionalFees, offerDiscount: formData.offerDiscount, discountPercentage: formData.discountPercentage, minGuests: formData.minGuests, maxGuests: formData.maxGuests, cancellationPolicy: formData.cancellationPolicy, }, media: { ...listing.media, galleryPhotos: galleryImageUrls, additionalVideos: galleryVideoUrls, coverMedia: coverMediaUrl, }, status: listingStatuses.PENDING, verification: { ...listing.verification, safetySecurity: { ...listing.verification.safetySecurity, securityPresence: formData.securityPresence, fireSafetyCompliance: formData.fireSafetyCompliance, licensedGuide: formData.licensedGuide, maxTravellersPerGuide: formData.maxTravellersPerGuide, safetyEquipmentProvided: formData.safetyEquipmentProvided, }, healthEmergency: { ...listing.verification.healthEmergency, firstAidStaff: formData.firstAidStaff, emergencyContactNumber: formData.emergencyContactNumber, healthProtocols: formData.healthProtocols, }, connectivity: { ...listing.verification.connectivity, mobileSignal: formData.mobileSignal, wifiAvailable: formData.wifiAvailable, gpsTracking: formData.gpsTracking, }, continuityAccess: { ...listing.verification.continuityAccess, backupVehicle: formData.backupVehicle, roadAccessType: formData.roadAccessType, backupPower: formData.backupPower, }, sustainabilityHygiene: { ...listing.verification.sustainabilityHygiene, wttcSafeTravelBadge: formData.wttcSafeTravelBadge, wttcBusinessName: formData.wttcBusinessName, gstcEcoCertified: formData.gstcEcoCertified, gstcBusinessName: formData.gstcBusinessName, hasCommunityPartnership: formData.hasCommunityPartnership, communityPartnerships: formData.communityPartnerships, }, inclusion: { ...listing.verification.inclusion, minimumAgeAccepted: formData.minimumAgeAccepted, womenSafetyMeasures: formData.womenSafetyMeasures, wheelchairAccessible: formData.wheelchairAccessible, mobilityAssistanceAvailable: formData.mobilityAssistanceAvailable, visualHearingSupport: formData.visualHearingSupport, }, }, }; await updateListing(listing.id, listingData); showSuccess('Listing updated successfully!'); } catch (error) { showError('Failed to update listing!'); } };