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 { 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 { supabase } from '@/lib/supabase'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { Button } from '@/components/ui/Button'
|
import { Button } from '@/components/ui/Button'
|
||||||
@ -111,11 +111,73 @@ export function Upload() {
|
|||||||
const [dragActive, setDragActive] = useState(false)
|
const [dragActive, setDragActive] = useState(false)
|
||||||
const audioInputRef = useRef<HTMLInputElement>(null)
|
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
|
// External mode
|
||||||
const [externalUrl, setExternalUrl] = useState('')
|
const [externalUrl, setExternalUrl] = useState('')
|
||||||
const [fetching, setFetching] = useState(false)
|
const [fetching, setFetching] = useState(false)
|
||||||
const [platform, setPlatform] = useState('')
|
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) => {
|
const handleAudioSelect = useCallback((file: File) => {
|
||||||
if (!file.type.startsWith('audio/')) {
|
if (!file.type.startsWith('audio/')) {
|
||||||
setError('Veuillez selectionner un fichier audio.')
|
setError('Veuillez selectionner un fichier audio.')
|
||||||
@ -321,10 +383,32 @@ export function Upload() {
|
|||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{mode === 'original' ? (
|
{mode === 'original' ? (
|
||||||
/* ---- ORIGINAL: Audio file picker ---- */
|
/* ---- ORIGINAL: Audio file picker + Recorder ---- */
|
||||||
!audioFile ? (
|
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
|
<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'}`}
|
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()}
|
onClick={() => audioInputRef.current?.click()}
|
||||||
onDragOver={(e) => { e.preventDefault(); setDragActive(true) }}
|
onDragOver={(e) => { e.preventDefault(); setDragActive(true) }}
|
||||||
onDragLeave={() => setDragActive(false)}
|
onDragLeave={() => setDragActive(false)}
|
||||||
@ -335,9 +419,9 @@ export function Upload() {
|
|||||||
if (file) handleAudioSelect(file)
|
if (file) handleAudioSelect(file)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UploadIcon size={40} className="mx-auto text-text-secondary mb-3" />
|
<UploadIcon size={32} className="mx-auto text-text-secondary mb-2" />
|
||||||
<p className="font-medium">Glissez votre fichier audio ici</p>
|
<p className="font-medium text-sm">Glissez votre fichier audio ici</p>
|
||||||
<p className="text-sm text-text-secondary mt-1">ou cliquez pour selectionner (MP3, WAV, M4A)</p>
|
<p className="text-xs text-text-secondary mt-1">ou cliquez pour selectionner (MP3, WAV, M4A)</p>
|
||||||
<input
|
<input
|
||||||
ref={audioInputRef}
|
ref={audioInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
@ -346,6 +430,31 @@ export function Upload() {
|
|||||||
onChange={(e) => e.target.files?.[0] && handleAudioSelect(e.target.files[0])}
|
onChange={(e) => e.target.files?.[0] && handleAudioSelect(e.target.files[0])}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="flex items-center gap-3 p-4 bg-primary/5 rounded-xl border border-primary/20">
|
||||||
<Music size={20} className="text-primary shrink-0" />
|
<Music size={20} className="text-primary shrink-0" />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user