592 lines
25 KiB
TypeScript
592 lines
25 KiB
TypeScript
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<OEmbedData | null> {
|
|
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<Mode>('choose')
|
|
|
|
// Shared fields
|
|
const [title, setTitle] = useState('')
|
|
const [description, setDescription] = useState('')
|
|
const [coverPreview, setCoverPreview] = useState<string | null>(null)
|
|
const [coverFile, setCoverFile] = useState<File | null>(null)
|
|
const [uploading, setUploading] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
// Original mode
|
|
const [audioFile, setAudioFile] = useState<File | null>(null)
|
|
const [duration, setDuration] = useState(0)
|
|
const [dragActive, setDragActive] = useState(false)
|
|
const audioInputRef = useRef<HTMLInputElement>(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<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('')
|
|
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 (
|
|
<div className="text-center py-16">
|
|
<p className="text-text-secondary mb-4">Connectez-vous pour publier un podcast.</p>
|
|
<Button onClick={() => navigate('/auth')}>Se connecter</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (mode === 'choose') {
|
|
return (
|
|
<div className="max-w-2xl mx-auto">
|
|
<h1 className="text-2xl font-heading font-bold mb-2">Publier un podcast</h1>
|
|
<p className="text-text-secondary mb-8">Choisissez comment vous souhaitez partager votre contenu</p>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
{/* Original content card */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setMode('original')}
|
|
className="group relative overflow-hidden rounded-2xl border-2 border-border hover:border-primary/40 bg-surface p-6 text-left transition-all duration-300 hover:shadow-[0_8px_30px_rgba(91,76,219,0.12)] cursor-pointer"
|
|
>
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-primary/[0.08] to-transparent rounded-bl-full transition-all duration-300 group-hover:w-40 group-hover:h-40" />
|
|
<div className="absolute bottom-0 left-0 w-20 h-20 bg-gradient-to-tr from-accent/[0.05] to-transparent rounded-tr-full" />
|
|
|
|
<div className="relative z-10">
|
|
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-primary to-[#7B6AEF] flex items-center justify-center mb-4 shadow-[0_4px_20px_rgba(91,76,219,0.25)] transition-transform duration-300 group-hover:scale-110 group-hover:rotate-[-4deg]">
|
|
<Mic size={24} className="text-white" />
|
|
</div>
|
|
<h3 className="font-heading font-bold text-lg mb-1.5">Contenu original</h3>
|
|
<p className="text-sm text-text-secondary leading-relaxed">
|
|
Uploadez votre propre fichier audio. Partagez votre voix, votre histoire, votre podcast.
|
|
</p>
|
|
<div className="flex gap-1.5 mt-4">
|
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-primary/10 text-primary">MP3</span>
|
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-primary/10 text-primary">WAV</span>
|
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-primary/10 text-primary">M4A</span>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* External content card */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setMode('external')}
|
|
className="group relative overflow-hidden rounded-2xl border-2 border-border hover:border-mint/40 bg-surface p-6 text-left transition-all duration-300 hover:shadow-[0_8px_30px_rgba(76,199,178,0.12)] cursor-pointer"
|
|
>
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-mint/[0.08] to-transparent rounded-bl-full transition-all duration-300 group-hover:w-40 group-hover:h-40" />
|
|
<div className="absolute bottom-0 left-0 w-20 h-20 bg-gradient-to-tr from-sun/[0.05] to-transparent rounded-tr-full" />
|
|
|
|
<div className="relative z-10">
|
|
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-mint to-[#3BB8A0] flex items-center justify-center mb-4 shadow-[0_4px_20px_rgba(76,199,178,0.25)] transition-transform duration-300 group-hover:scale-110 group-hover:rotate-[4deg]">
|
|
<Link2 size={24} className="text-white" />
|
|
</div>
|
|
<h3 className="font-heading font-bold text-lg mb-1.5">Contenu existant</h3>
|
|
<p className="text-sm text-text-secondary leading-relaxed">
|
|
Partagez une video ou un podcast deja en ligne. On recupere tout automatiquement.
|
|
</p>
|
|
<div className="flex gap-1.5 mt-4">
|
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">YouTube</span>
|
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">Spotify</span>
|
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">Dailymotion</span>
|
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">SoundCloud</span>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-2xl mx-auto">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setMode('choose')
|
|
setAudioFile(null)
|
|
setExternalUrl('')
|
|
setTitle('')
|
|
setDescription('')
|
|
setCoverPreview(null)
|
|
setCoverFile(null)
|
|
setDuration(0)
|
|
setError('')
|
|
setPlatform('')
|
|
setAuthor('')
|
|
setAuthorAvatar('')
|
|
}}
|
|
className="text-sm text-text-secondary hover:text-text transition-colors cursor-pointer"
|
|
>
|
|
← Retour
|
|
</button>
|
|
<h1 className="text-2xl font-heading font-bold">
|
|
{mode === 'original' ? 'Uploader un fichier audio' : 'Partager un contenu existant'}
|
|
</h1>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{mode === 'original' ? (
|
|
/* ---- 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 */}
|
|
{micPermission === 'denied' ? (
|
|
<div className="w-full rounded-2xl border-2 border-accent/20 bg-accent/[0.03] p-5">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-12 h-12 rounded-2xl bg-gray-200 flex items-center justify-center shrink-0">
|
|
<MicOff size={20} className="text-text-secondary" />
|
|
</div>
|
|
<div>
|
|
<p className="font-heading font-bold text-text-secondary">Microphone bloque</p>
|
|
<p className="text-xs text-text-secondary mt-0.5">
|
|
Cliquez sur l'icone <ShieldAlert size={12} className="inline -mt-0.5" /> dans la barre d'adresse de votre navigateur pour autoriser l'acces au microphone, puis rechargez la page.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<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">
|
|
{micPermission === 'granted'
|
|
? 'Votre micro est pret — cliquez pour commencer'
|
|
: 'Votre navigateur demandera l\'acces au microphone'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<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" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium truncate">{audioFile.name}</p>
|
|
<p className="text-xs text-text-secondary">
|
|
{(audioFile.size / 1024 / 1024).toFixed(1)} Mo
|
|
{duration > 0 && ` — ${Math.floor(duration / 60)}min ${duration % 60}s`}
|
|
</p>
|
|
</div>
|
|
<button type="button" onClick={() => { setAudioFile(null); setDuration(0) }} className="text-text-secondary hover:text-text cursor-pointer">
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
)
|
|
) : (
|
|
/* ---- EXTERNAL: URL input ---- */
|
|
<div className="space-y-3">
|
|
<div className="relative">
|
|
<Input
|
|
id="url"
|
|
label="URL de la video ou du podcast"
|
|
value={externalUrl}
|
|
onChange={(e) => setExternalUrl(e.target.value)}
|
|
placeholder="https://youtube.com/watch?v=... ou https://open.spotify.com/episode/..."
|
|
/>
|
|
{fetching && (
|
|
<div className="absolute right-3 bottom-3">
|
|
<Loader2 size={18} className="animate-spin text-text-secondary" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{platform && coverPreview && (
|
|
<div className="flex items-center gap-3 p-3 bg-mint/5 rounded-xl border border-mint/20">
|
|
<img src={coverPreview} alt="" className="w-16 h-16 rounded-lg object-cover shrink-0" />
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium truncate">{title || 'Sans titre'}</p>
|
|
<div className="flex items-center gap-2 mt-0.5 text-xs text-text-secondary">
|
|
{author && <span className="truncate">{author}</span>}
|
|
{author && duration > 0 && <span>·</span>}
|
|
{duration > 0 && <span className="shrink-0">{Math.floor(duration / 60)}:{(duration % 60).toString().padStart(2, '0')}</span>}
|
|
</div>
|
|
<div className="flex items-center gap-1.5 mt-1">
|
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-mint/10 text-mint">
|
|
{platform}
|
|
</span>
|
|
<span className="text-[11px] font-medium px-2 py-0.5 rounded-full bg-primary/10 text-primary">
|
|
Auto-detecte
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Shared fields — hidden in external mode until metadata is fetched */}
|
|
{(mode === 'original' || platform) && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-[1fr_auto] gap-6">
|
|
<div className="space-y-4">
|
|
<Input id="title" label="Titre" value={title} onChange={(e) => setTitle(e.target.value)} required placeholder="Le titre de votre podcast" />
|
|
<div className="flex flex-col gap-1.5">
|
|
<label htmlFor="desc" className="text-[13px] font-semibold text-text tracking-wide">Description</label>
|
|
<textarea
|
|
id="desc"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="De quoi parle votre podcast ?"
|
|
rows={4}
|
|
className="rounded-2xl border-2 border-border bg-surface-warm/50 px-4 py-3 text-sm text-text placeholder:text-text-secondary/40 outline-none resize-none transition-all duration-200 focus:border-primary focus:bg-surface focus:shadow-[0_0_0_4px_rgba(91,76,219,0.08)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-center gap-2">
|
|
<label className="text-[13px] font-semibold text-text tracking-wide">Couverture</label>
|
|
<div
|
|
className="w-32 h-32 rounded-xl border-2 border-dashed border-border hover:border-primary/30 flex items-center justify-center cursor-pointer overflow-hidden transition-colors"
|
|
onClick={() => {
|
|
const input = document.createElement('input')
|
|
input.type = 'file'
|
|
input.accept = 'image/*'
|
|
input.onchange = () => input.files?.[0] && handleCoverSelect(input.files[0])
|
|
input.click()
|
|
}}
|
|
>
|
|
{coverPreview ? (
|
|
<img src={coverPreview} alt="" className="w-full h-full object-cover" />
|
|
) : (
|
|
<Image size={24} className="text-text-secondary" />
|
|
)}
|
|
</div>
|
|
{coverPreview && mode === 'external' && !coverFile && (
|
|
<p className="text-[11px] text-text-secondary">Auto-detectee</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{error && <p className="text-sm text-accent">{error}</p>}
|
|
|
|
{(mode === 'original' || platform) && (
|
|
<Button
|
|
type="submit"
|
|
size="lg"
|
|
className="w-full"
|
|
disabled={
|
|
(mode === 'original' ? !audioFile : !externalUrl.trim()) || !title || uploading
|
|
}
|
|
>
|
|
{uploading ? 'Publication en cours...' : 'Publier le podcast'}
|
|
</Button>
|
|
)}
|
|
</form>
|
|
</div>
|
|
)
|
|
}
|