import { useState, useCallback } from 'react'; import { useDropzone } from 'react-dropzone'; import { useQueryClient } from '@tanstack/react-query'; import { tracksApi, type TrackMetadata } from '@/services/api/tracks'; import { LIBRARY_KEYS } from '@/features/library/hooks/useMyTracks'; import { logger } from '@/utils/logger'; import { useIsRateLimited } from '@/hooks/useIsRateLimited'; import { ACCEPTED_AUDIO_TYPES, MAX_FILE_SIZE, MAX_RETRY_ATTEMPTS, type UploadFormData, } from './constants'; export interface UseUploadModalProps { open: boolean; onClose: () => void; } export function useUploadModal({ onClose }: UseUploadModalProps) { const [file, setFile] = useState(null); const [uploadProgress, setUploadProgress] = useState(0); const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(null); const [errorCode, setErrorCode] = useState(null); const [isRetryable, setIsRetryable] = useState(false); const [retryCount, setRetryCount] = useState(0); const [success, setSuccess] = useState(false); const [formData, setFormData] = useState({ file: null, title: '', artist: '', album: '', genre: '', }); const [formErrors, setFormErrors] = useState>>({}); const isRateLimited = useIsRateLimited(); const queryClient = useQueryClient(); const reset = useCallback(() => { setFormData({ file: null, title: '', artist: '', album: '', genre: '' }); setFormErrors({}); }, []); const handleClose = useCallback(() => { if (!isUploading) { setFile(null); setUploadProgress(0); setError(null); setErrorCode(null); setIsRetryable(false); setRetryCount(0); setSuccess(false); reset(); onClose(); } }, [isUploading, reset, onClose]); const setValue = useCallback((name: keyof UploadFormData, value: UploadFormData[keyof UploadFormData]) => { setFormData((prev) => ({ ...prev, [name]: value })); }, []); const getValues = useCallback(() => formData, [formData]); const performUpload = useCallback( async (data: UploadFormData, attemptNumber = 1) => { if (!data.file) { setError('Veuillez sélectionner un fichier'); setErrorCode(null); setIsRetryable(false); return; } setIsUploading(true); setError(null); setErrorCode(null); setIsRetryable(false); setSuccess(false); setUploadProgress(0); try { const trackMetadata: TrackMetadata = { title: data.title || data.file.name.replace(/\.[^/.]+$/, ''), artist: data.artist, album: data.album, genre: data.genre, is_public: false, }; await tracksApi.create(data.file, trackMetadata, (progress) => { setUploadProgress(progress); }); setSuccess(true); setUploadProgress(100); setRetryCount(0); queryClient.invalidateQueries({ queryKey: LIBRARY_KEYS.all }); queryClient.invalidateQueries({ queryKey: ['tracks'] }); setTimeout(() => handleClose(), 1500); } catch (err) { let errorMessage = "Erreur lors de l'upload"; let errorCodeValue: string | null = null; let retryable = false; if (err instanceof Error) { errorMessage = err.message; const isNetworkError = /network|réseau|timeout|econnaborted|etimedout|se connecter/i.test(errorMessage); const isServerError = /serveur|server|500|503|502/i.test(errorMessage); const isValidationError = /format|taille|invalide|trop volumineux|non supporté|400|413|415/i.test(errorMessage); if (isNetworkError) { errorCodeValue = 'NETWORK'; retryable = attemptNumber < MAX_RETRY_ATTEMPTS; } else if (isServerError) { errorCodeValue = 'SERVER'; retryable = attemptNumber < MAX_RETRY_ATTEMPTS; } else if (isValidationError) { errorCodeValue = 'VALIDATION'; retryable = false; } } setError(errorMessage); setErrorCode(errorCodeValue); setIsRetryable(retryable); setUploadProgress(0); setRetryCount(attemptNumber); } finally { setIsUploading(false); } }, [queryClient, handleClose], ); const onDrop = useCallback( (acceptedFiles: File[]) => { const selectedFile = acceptedFiles[0]; if (selectedFile) { setFile(selectedFile); setError(null); setSuccess(false); setValue('file', selectedFile); const fileNameWithoutExt = selectedFile.name.replace(/\.[^/.]+$/, ''); if (!formData.title) setValue('title', fileNameWithoutExt); } }, [setValue, formData.title], ); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: ACCEPTED_AUDIO_TYPES, maxSize: MAX_FILE_SIZE, multiple: false, onError: (err) => setError(`Erreur lors de la sélection du fichier: ${err.message}`), onDropRejected: (fileRejections) => { const rejection = fileRejections[0]; if (rejection?.errors[0]?.code === 'file-too-large') { setError('Le fichier est trop volumineux (max 100 MB)'); } else if (rejection?.errors[0]?.code === 'file-invalid-type') { setError( 'Format de fichier non supporté. Formats acceptés: MP3, WAV, OGG, FLAC, M4A, AAC', ); } else { setError( rejection?.errors[0]?.message || 'Erreur lors de la sélection du fichier', ); } }, }); const register = (name: keyof UploadFormData) => ({ value: formData[name] instanceof File ? '' : (formData[name] as string | undefined) || '', onChange: (e: React.ChangeEvent) => { setFormData((prev) => ({ ...prev, [name]: e.target.value })); }, }); const handleSubmit = useCallback( (onValid: (data: UploadFormData) => void, onInvalid?: (errors: unknown) => void) => (e: React.FormEvent) => { e.preventDefault(); if (!formData.file) { setFormErrors({ file: 'Veuillez sélectionner un fichier' }); onInvalid?.({ file: { message: 'Veuillez sélectionner un fichier' } }); return; } setFormErrors({}); onValid(formData); }, [formData], ); const onSubmit = useCallback( async (data: UploadFormData) => { await performUpload(data, 1); }, [performUpload], ); const handleRetry = useCallback(() => { performUpload(getValues(), retryCount + 1); }, [retryCount, getValues, performUpload]); const handleRemoveFile = useCallback(() => { setFile(null); setError(null); setSuccess(false); setUploadProgress(0); setValue('file', null); }, [setValue]); return { file, uploadProgress, isUploading, error, errorCode, isRetryable, retryCount, success, formData, formErrors: formErrors as Partial>, register, handleSubmit, onSubmit, getValues, getRootProps, getInputProps, isDragActive, handleClose, handleRetry, handleRemoveFile, isRateLimited, }; }