veza/apps/web/src/features/upload/components/upload-modal/useUploadModal.ts
senke 0ff8146413 refactor(upload): decompose UploadModal into upload-modal module
- Add upload-modal/ with useUploadModal, constants, and presentational
  components: Dropzone, FileDisplay, Progress, ErrorAlert, MetadataForm
- Re-export from UploadModal.tsx for backward compatibility

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-05 21:03:07 +01:00

231 lines
7.2 KiB
TypeScript

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<File | null>(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [errorCode, setErrorCode] = useState<string | null>(null);
const [isRetryable, setIsRetryable] = useState(false);
const [retryCount, setRetryCount] = useState(0);
const [success, setSuccess] = useState(false);
const [formData, setFormData] = useState<UploadFormData>({
file: null,
title: '',
artist: '',
album: '',
genre: '',
});
const [formErrors, setFormErrors] = useState<Partial<Record<keyof UploadFormData, string>>>({});
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<HTMLInputElement>) => {
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<Record<keyof UploadFormData, string>>,
register,
handleSubmit,
onSubmit,
getValues,
getRootProps,
getInputProps,
isDragActive,
handleClose,
handleRetry,
handleRemoveFile,
isRateLimited,
};
}