feat: add in-app audio recording in upload page
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 28s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 28s
This commit is contained in:
parent
c76b5e7d9c
commit
cc4ced3076
@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useCallback } from 'react'
|
||||
import { useState, useRef, useCallback, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Upload as UploadIcon, Music, X, Image, Mic, Link2, Loader2, Sparkles } from 'lucide-react'
|
||||
import { Upload as UploadIcon, Music, X, Image, Mic, Link2, Loader2, Sparkles, Circle, Square } from 'lucide-react'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
@ -111,11 +111,73 @@ export function Upload() {
|
||||
const [dragActive, setDragActive] = useState(false)
|
||||
const audioInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Recording mode
|
||||
const [isRecording, setIsRecording] = useState(false)
|
||||
const [recordingTime, setRecordingTime] = useState(0)
|
||||
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
|
||||
const chunksRef = useRef<Blob[]>([])
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
// External mode
|
||||
const [externalUrl, setExternalUrl] = useState('')
|
||||
const [fetching, setFetching] = useState(false)
|
||||
const [platform, setPlatform] = useState('')
|
||||
|
||||
// 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() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
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 {
|
||||
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.')
|
||||
@ -321,30 +383,77 @@ export function Upload() {
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{mode === 'original' ? (
|
||||
/* ---- ORIGINAL: Audio file picker ---- */
|
||||
!audioFile ? (
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-2xl p-12 text-center transition-colors cursor-pointer ${dragActive ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/30'}`}
|
||||
onClick={() => 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)
|
||||
}}
|
||||
>
|
||||
<UploadIcon size={40} className="mx-auto text-text-secondary mb-3" />
|
||||
<p className="font-medium">Glissez votre fichier audio ici</p>
|
||||
<p className="text-sm text-text-secondary mt-1">ou cliquez pour selectionner (MP3, WAV, M4A)</p>
|
||||
<input
|
||||
ref={audioInputRef}
|
||||
type="file"
|
||||
accept="audio/*"
|
||||
className="hidden"
|
||||
onChange={(e) => e.target.files?.[0] && handleAudioSelect(e.target.files[0])}
|
||||
/>
|
||||
/* ---- ORIGINAL: Audio file picker + Recorder ---- */
|
||||
isRecording ? (
|
||||
/* Recording in progress */
|
||||
<div className="border-2 border-accent/30 bg-accent/[0.03] rounded-2xl p-8 text-center">
|
||||
<div className="relative inline-flex items-center justify-center mb-4">
|
||||
<div className="absolute w-20 h-20 rounded-full bg-accent/20 animate-ping" />
|
||||
<div className="relative w-16 h-16 rounded-full bg-gradient-to-br from-accent to-[#D04B3A] flex items-center justify-center">
|
||||
<Mic size={28} className="text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-2xl font-heading font-bold tabular-nums mb-1">{formatRecordTime(recordingTime)}</p>
|
||||
<p className="text-sm text-text-secondary mb-5">Enregistrement en cours...</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={stopRecording}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-full bg-accent text-white font-semibold hover:bg-accent/90 transition-colors cursor-pointer shadow-[0_4px_20px_rgba(232,96,76,0.3)]"
|
||||
>
|
||||
<Square size={16} fill="white" />
|
||||
Arreter l'enregistrement
|
||||
</button>
|
||||
</div>
|
||||
) : !audioFile ? (
|
||||
<div className="space-y-3">
|
||||
{/* Drag & drop zone */}
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-2xl p-8 text-center transition-colors cursor-pointer ${dragActive ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/30'}`}
|
||||
onClick={() => 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)
|
||||
}}
|
||||
>
|
||||
<UploadIcon size={32} className="mx-auto text-text-secondary mb-2" />
|
||||
<p className="font-medium text-sm">Glissez votre fichier audio ici</p>
|
||||
<p className="text-xs text-text-secondary mt-1">ou cliquez pour selectionner (MP3, WAV, M4A)</p>
|
||||
<input
|
||||
ref={audioInputRef}
|
||||
type="file"
|
||||
accept="audio/*"
|
||||
className="hidden"
|
||||
onChange={(e) => e.target.files?.[0] && handleAudioSelect(e.target.files[0])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
<span className="text-xs text-text-secondary">ou</span>
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
</div>
|
||||
|
||||
{/* Record button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={startRecording}
|
||||
className="w-full group relative overflow-hidden rounded-2xl border-2 border-border hover:border-accent/30 bg-surface p-5 text-left transition-all duration-300 hover:shadow-[0_8px_30px_rgba(232,96,76,0.1)] cursor-pointer"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-bl from-accent/[0.06] to-transparent rounded-bl-full transition-all duration-300 group-hover:w-32 group-hover:h-32" />
|
||||
<div className="relative z-10 flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-accent to-[#D04B3A] flex items-center justify-center shadow-[0_4px_16px_rgba(232,96,76,0.25)] transition-transform duration-300 group-hover:scale-110">
|
||||
<Circle size={20} className="text-white" fill="white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-heading font-bold">Enregistrer directement</p>
|
||||
<p className="text-xs text-text-secondary">Utilisez votre micro pour enregistrer un podcast</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3 p-4 bg-primary/5 rounded-xl border border-primary/20">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user