import { useState, useRef, useCallback, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { Upload as UploadIcon, Music, X, Image, Mic, Link2, Loader2, Circle, Square, MicOff, ShieldAlert } from 'lucide-react' import { supabase } from '@/lib/supabase' import { useAuthStore } from '@/stores/auth' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' type Mode = 'choose' | 'original' | 'external' interface OEmbedData { title: string description: string thumbnail: string duration: number audioUrl: string platform: string author: string authorAvatar: string } const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001' async function fetchVideoMeta(url: string): Promise { try { const res = await fetch(`${API_URL}/api/metadata?url=${encodeURIComponent(url)}`) if (!res.ok) return null return await res.json() } catch { return null } } export function Upload() { const { user } = useAuthStore() const navigate = useNavigate() const [mode, setMode] = useState('choose') // Shared fields const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [coverPreview, setCoverPreview] = useState(null) const [coverFile, setCoverFile] = useState(null) const [uploading, setUploading] = useState(false) const [error, setError] = useState('') // Original mode const [audioFile, setAudioFile] = useState(null) const [duration, setDuration] = useState(0) const [dragActive, setDragActive] = useState(false) const audioInputRef = useRef(null) // Recording mode const [isRecording, setIsRecording] = useState(false) const [recordingTime, setRecordingTime] = useState(0) const [micPermission, setMicPermission] = useState<'granted' | 'denied' | 'prompt' | 'checking'>('checking') const mediaRecorderRef = useRef(null) const chunksRef = useRef([]) const timerRef = useRef | null>(null) // External mode const [externalUrl, setExternalUrl] = useState('') const [fetching, setFetching] = useState(false) const [platform, setPlatform] = useState('') const [author, setAuthor] = useState('') const [authorAvatar, setAuthorAvatar] = useState('') // Check microphone permission when entering original mode useEffect(() => { if (mode !== 'original') return let permStatus: PermissionStatus | null = null async function checkMicPermission() { try { permStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName }) setMicPermission(permStatus.state as 'granted' | 'denied' | 'prompt') permStatus.onchange = () => setMicPermission(permStatus!.state as 'granted' | 'denied' | 'prompt') } catch { // Permissions API not supported or blocked by Permissions Policy — assume prompt setMicPermission('prompt') } } checkMicPermission() return () => { if (permStatus) permStatus.onchange = null } }, [mode]) // Cleanup recording on unmount useEffect(() => { return () => { if (timerRef.current) clearInterval(timerRef.current) if (mediaRecorderRef.current?.state === 'recording') { mediaRecorderRef.current.stop() mediaRecorderRef.current.stream.getTracks().forEach(t => t.stop()) } } }, []) async function startRecording() { setError('') try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) setMicPermission('granted') const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }) mediaRecorderRef.current = mediaRecorder chunksRef.current = [] mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data) } mediaRecorder.onstop = () => { stream.getTracks().forEach(t => t.stop()) if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null } const blob = new Blob(chunksRef.current, { type: 'audio/webm' }) const file = new File([blob], `enregistrement-${Date.now()}.webm`, { type: 'audio/webm' }) setAudioFile(file) setDuration(recordingTime) if (!title) setTitle('Mon enregistrement') setIsRecording(false) } mediaRecorder.start(1000) setIsRecording(true) setRecordingTime(0) timerRef.current = setInterval(() => setRecordingTime(t => t + 1), 1000) } catch (err) { const e = err as DOMException if (e.name === 'NotAllowedError') { setMicPermission('denied') setError('Acces au microphone refuse. Autorisez le microphone dans les parametres de votre navigateur puis rechargez la page.') } else if (e.name === 'NotFoundError') { setError('Aucun microphone detecte. Verifiez qu\'un micro est branche a votre appareil.') } else { setError('Impossible d\'acceder au microphone. Verifiez les permissions de votre navigateur.') } } } function stopRecording() { if (mediaRecorderRef.current?.state === 'recording') { mediaRecorderRef.current.stop() } } function formatRecordTime(s: number) { const m = Math.floor(s / 60) const sec = s % 60 return `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}` } const handleAudioSelect = useCallback((file: File) => { if (!file.type.startsWith('audio/')) { setError('Veuillez selectionner un fichier audio.') return } setAudioFile(file) setError('') const audio = new Audio() audio.src = URL.createObjectURL(file) audio.addEventListener('loadedmetadata', () => { setDuration(Math.floor(audio.duration)) URL.revokeObjectURL(audio.src) }) if (!title) setTitle(file.name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ')) }, [title]) function handleCoverSelect(file: File) { if (!file.type.startsWith('image/')) return setCoverFile(file) setCoverPreview(URL.createObjectURL(file)) } // Auto-fetch metadata when URL changes (debounced) useEffect(() => { if (mode !== 'external') return const url = externalUrl.trim() if (!url) { setPlatform('') setCoverPreview(null) setTitle('') setDescription('') setDuration(0) setAuthor('') setAuthorAvatar('') return } // Basic URL validation before fetching if (!url.startsWith('http://') && !url.startsWith('https://')) return const timeout = setTimeout(async () => { setFetching(true) setError('') const meta = await fetchVideoMeta(url) if (!meta) { setError('URL non reconnue. Formats supportes : YouTube, Spotify, Dailymotion, SoundCloud.') setFetching(false) return } setTitle(meta.title || '') setDescription(meta.description || '') if (meta.thumbnail) setCoverPreview(meta.thumbnail) if (meta.duration) setDuration(meta.duration) if (meta.author) setAuthor(meta.author) if (meta.authorAvatar) setAuthorAvatar(meta.authorAvatar) setPlatform(meta.platform) setFetching(false) }, 600) return () => clearTimeout(timeout) }, [externalUrl, mode]) async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!user) return setUploading(true) setError('') let audio_url = '' const timestamp = Date.now() if (mode === 'original') { if (!audioFile) return const audioPath = `${user.id}/${timestamp}-${audioFile.name}` const { error: audioErr } = await supabase.storage .from('podcasts') .upload(audioPath, audioFile) if (audioErr) { setError("Erreur lors de l'upload audio: " + audioErr.message) setUploading(false) return } const { data: audioUrlData } = supabase.storage.from('podcasts').getPublicUrl(audioPath) audio_url = audioUrlData.publicUrl } else { audio_url = externalUrl.trim() } let cover_url: string | null = coverPreview if (coverFile) { const coverPath = `${user.id}/${timestamp}-cover-${coverFile.name}` const { error: coverErr } = await supabase.storage .from('covers') .upload(coverPath, coverFile) if (!coverErr) { const { data } = supabase.storage.from('covers').getPublicUrl(coverPath) cover_url = data.publicUrl } } const { data: podcast, error: insertErr } = await supabase .from('podcasts') .insert({ creator_id: user.id, title, description, audio_url, duration_seconds: duration, cover_url, ...(mode === 'external' && author ? { external_author: author, external_author_avatar: authorAvatar || null } : {}), }) .select() .single() if (insertErr) { setError('Erreur: ' + insertErr.message) setUploading(false) return } navigate(`/podcast/${podcast.id}`) } if (!user) { return (

Connectez-vous pour publier un podcast.

) } if (mode === 'choose') { return (

Publier un podcast

Choisissez comment vous souhaitez partager votre contenu

{/* Original content card */} {/* External content card */}
) } return (

{mode === 'original' ? 'Uploader un fichier audio' : 'Partager un contenu existant'}

{mode === 'original' ? ( /* ---- ORIGINAL: Audio file picker + Recorder ---- */ isRecording ? ( /* Recording in progress */

{formatRecordTime(recordingTime)}

Enregistrement en cours...

) : !audioFile ? (
{/* Drag & drop zone */}
audioInputRef.current?.click()} onDragOver={(e) => { e.preventDefault(); setDragActive(true) }} onDragLeave={() => setDragActive(false)} onDrop={(e) => { e.preventDefault() setDragActive(false) const file = e.dataTransfer.files[0] if (file) handleAudioSelect(file) }} >

Glissez votre fichier audio ici

ou cliquez pour selectionner (MP3, WAV, M4A)

e.target.files?.[0] && handleAudioSelect(e.target.files[0])} />
ou
{/* Record button */} {micPermission === 'denied' ? (

Microphone bloque

Cliquez sur l'icone dans la barre d'adresse de votre navigateur pour autoriser l'acces au microphone, puis rechargez la page.

) : ( )}
) : (

{audioFile.name}

{(audioFile.size / 1024 / 1024).toFixed(1)} Mo {duration > 0 && ` — ${Math.floor(duration / 60)}min ${duration % 60}s`}

) ) : ( /* ---- EXTERNAL: URL input ---- */
setExternalUrl(e.target.value)} placeholder="https://youtube.com/watch?v=... ou https://open.spotify.com/episode/..." /> {fetching && (
)}
{platform && coverPreview && (

{title || 'Sans titre'}

{author && {author}} {author && duration > 0 && ·} {duration > 0 && {Math.floor(duration / 60)}:{(duration % 60).toString().padStart(2, '0')}}
{platform} Auto-detecte
)}
)} {/* Shared fields — hidden in external mode until metadata is fetched */} {(mode === 'original' || platform) && (
setTitle(e.target.value)} required placeholder="Le titre de votre podcast" />