veza/apps/web/src/features/upload/components/UploadModal.tsx

331 lines
11 KiB
TypeScript
Raw Normal View History

2025-12-22 21:00:50 +00:00
import { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { useForm } from 'react-hook-form';
import { Dialog, DialogBody, DialogFooter } from '@/components/ui/dialog';
2025-12-22 21:00:50 +00:00
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert } from '@/components/ui/alert';
import { Upload, X, FileAudio, AlertCircle, CheckCircle2 } from 'lucide-react';
import { uploadTrack, type TrackMetadata } from '@/features/tracks/api/trackApi';
import { useQueryClient } from '@tanstack/react-query';
import { LIBRARY_KEYS } from '@/features/library/hooks/useMyTracks';
2025-12-22 21:00:50 +00:00
export interface UploadModalProps {
open: boolean;
onClose: () => void;
}
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB
const ACCEPTED_AUDIO_TYPES = {
'audio/mpeg': ['.mp3'],
'audio/wav': ['.wav'],
'audio/ogg': ['.ogg'],
'audio/flac': ['.flac'],
'audio/mp4': ['.m4a'],
'audio/aac': ['.aac'],
};
type UploadFormData = {
file: File | null;
title: string;
artist: string;
album: string;
genre: string;
};
export function UploadModal({ open, onClose }: UploadModalProps) {
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 [success, setSuccess] = useState(false);
const queryClient = useQueryClient();
const {
register,
handleSubmit,
setValue,
getValues,
formState: { errors },
reset,
} = useForm<UploadFormData>({
defaultValues: {
file: null,
title: '',
artist: '',
album: '',
genre: '',
},
});
const onDrop = useCallback(
(acceptedFiles: File[]) => {
const selectedFile = acceptedFiles[0];
if (selectedFile) {
setFile(selectedFile);
setError(null);
setSuccess(false);
// Mettre à jour le formulaire avec setValue pour que react-hook-form connaisse le fichier
setValue('file', selectedFile, { shouldValidate: true });
// Pré-remplir le titre avec le nom du fichier (sans extension)
const fileNameWithoutExt = selectedFile.name.replace(/\.[^/.]+$/, '');
// Lecture à la demande avec getValues, pas de re-render
const currentTitle = getValues('title');
if (!currentTitle) {
setValue('title', fileNameWithoutExt, { shouldValidate: true });
}
}
},
[setValue, getValues],
);
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 onSubmit = async (data: UploadFormData) => {
console.log('📝 Form submitting...');
console.log('🚀 Form submitted!', data);
if (!data.file) {
setError('Veuillez sélectionner un fichier');
return;
}
setIsUploading(true);
setError(null);
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 uploadTrack(
2025-12-22 21:00:50 +00:00
data.file,
trackMetadata,
(progress) => {
setUploadProgress(progress);
},
);
setSuccess(true);
setUploadProgress(100);
// Invalider les queries pour rafraîchir la liste
queryClient.invalidateQueries({ queryKey: LIBRARY_KEYS.all });
queryClient.invalidateQueries({ queryKey: ['tracks'] });
// Fermer après 1.5 secondes
setTimeout(() => {
handleClose();
}, 1500);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Erreur lors de l'upload";
setError(errorMessage);
setUploadProgress(0);
} finally {
setIsUploading(false);
}
};
const handleClose = () => {
if (!isUploading) {
setFile(null);
setUploadProgress(0);
setError(null);
setSuccess(false);
reset();
onClose();
}
};
const handleRemoveFile = () => {
setFile(null);
setError(null);
setSuccess(false);
setUploadProgress(0);
setValue('file', null, { shouldValidate: true });
};
return (
<Dialog open={open} onClose={handleClose} title="Uploader un fichier audio" size="lg">
<form
id="upload-track-form"
onSubmit={handleSubmit(onSubmit, (errors) => {
console.error('❌ Form Errors:', errors);
})}
>
<DialogBody>
<div className="space-y-6">
{/* Zone de Drag & Drop */}
{!file ? (
<div
{...getRootProps()}
className={`
2025-12-22 21:00:50 +00:00
border-2 border-dashed rounded-lg p-12 text-center cursor-pointer
transition-colors
${isDragActive ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'}
hover:border-primary hover:bg-primary/5
`}
>
<input {...getInputProps()} />
<FileAudio className="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<p className="text-lg font-medium mb-2">
{isDragActive ? 'Déposez le fichier ici' : 'Glissez-déposez un fichier audio'}
</p>
<p className="text-sm text-muted-foreground mb-4">
ou cliquez pour sélectionner
</p>
<p className="text-xs text-muted-foreground">
Formats acceptés: MP3, WAV, OGG, FLAC, M4A, AAC (max 100 MB)
</p>
</div>
) : (
<div className="border rounded-lg p-4" data-testid="upload-file-display">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<FileAudio className="h-8 w-8 text-primary" />
<div>
<p className="font-medium" data-testid="upload-file-name">{file.name}</p>
<p className="text-sm text-muted-foreground">
{(file.size / 1024 / 1024).toFixed(2)} MB
</p>
</div>
2025-12-22 21:00:50 +00:00
</div>
{!isUploading && (
<Button
type="button"
variant="ghost"
size="icon"
onClick={handleRemoveFile}
className="h-8 w-8"
>
<X className="h-4 w-4" />
</Button>
)}
2025-12-22 21:00:50 +00:00
</div>
</div>
)}
2025-12-22 21:00:50 +00:00
{/* Progress Bar */}
{isUploading && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span>Upload en cours...</span>
<span>{uploadProgress}%</span>
</div>
<Progress value={uploadProgress} />
2025-12-22 21:00:50 +00:00
</div>
)}
2025-12-22 21:00:50 +00:00
{/* Messages d'erreur */}
{error && (
<Alert variant="destructive" data-testid="upload-error">
<AlertCircle className="h-4 w-4" />
<span>{error}</span>
</Alert>
)}
2025-12-22 21:00:50 +00:00
{/* Message de succès */}
{success && (
<Alert className="bg-green-50 border-green-200 text-green-800">
<CheckCircle2 className="h-4 w-4" />
<span>Fichier uploadé avec succès !</span>
</Alert>
)}
2025-12-22 21:00:50 +00:00
{/* Formulaire de métadonnées */}
{file && !isUploading && !success && (
<div className="space-y-4 border-t pt-4">
<h3 className="font-medium">Métadonnées (optionnel)</h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="title">Titre *</Label>
<Input
id="title"
{...register('title')}
placeholder="Titre du morceau"
/>
{errors.title && (
<p className="text-sm text-destructive">{errors.title.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="artist">Artiste</Label>
<Input
id="artist"
{...register('artist')}
placeholder="Nom de l'artiste"
/>
</div>
<div className="space-y-2">
<Label htmlFor="album">Album</Label>
<Input
id="album"
{...register('album')}
placeholder="Nom de l'album"
/>
</div>
<div className="space-y-2">
<Label htmlFor="genre">Genre</Label>
<Input
id="genre"
{...register('genre')}
placeholder="Genre musical"
/>
</div>
</div>
</div>
)}
</div>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={handleClose} disabled={isUploading} type="button">
{success ? 'Fermer' : 'Annuler'}
</Button>
{!success && (
<Button
type="submit"
form="upload-track-form"
disabled={!file || isUploading}
className="gap-2"
onClick={() => {
console.log('🔘 Button clicked');
}}
>
<Upload className="h-4 w-4" />
{isUploading ? 'Upload en cours...' : 'Uploader'}
</Button>
)}
</DialogFooter>
</form>
</Dialog>
);
}