feat: add submission form and status page

This commit is contained in:
2026-08-10 14:51:20 +02:00
parent 479ce1b921
commit 612459a4ec
5 changed files with 246 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
'use client'
import { useEffect, useState } from 'react'
type DownloadData = {
uuid: string
status: 'PENDING' | 'PROCESSING' | 'DONE' | 'FAILED' | 'FILE_DELETED'
format: string
quality: string
fileName: string | null
fileSize: string | null
errorMsg: string | null
downloadToken: string | null
tokenExpiresAt: string | null
}
export function StatusView({ uuid }: { uuid: string }) {
const [data, setData] = useState<DownloadData | null>(null)
const [notFound, setNotFound] = useState(false)
useEffect(() => {
let active = true
async function poll() {
const res = await fetch(`/api/downloads/${uuid}`)
if (!active) return
if (res.status === 404) {
setNotFound(true)
return
}
const body = (await res.json()) as DownloadData
setData(body)
if (body.status === 'PENDING' || body.status === 'PROCESSING') {
setTimeout(poll, 5_000)
}
}
poll()
return () => { active = false }
}, [uuid])
if (notFound) return <p>Telechargement introuvable.</p>
if (!data) return <p>Chargement...</p>
if (data.status === 'PENDING' || data.status === 'PROCESSING') {
return (
<div>
<p>Statut : {data.status === 'PENDING' ? 'En attente' : 'En cours...'}</p>
<p>Cette page se rafraichit automatiquement toutes les 5 secondes.</p>
</div>
)
}
if (data.status === 'DONE' && data.downloadToken) {
const sizeMb = data.fileSize
? `(${(Number(data.fileSize) / 1_048_576).toFixed(1)} Mo)`
: ''
return (
<div>
<p>Telechargement pret {sizeMb}</p>
<a
href={`/api/download/${data.downloadToken}`}
download={data.fileName ?? undefined}
>
Telecharger {data.fileName}
</a>
{data.tokenExpiresAt && (
<p style={{ fontSize: '0.85rem', color: '#666' }}>
Lien valable jusqu&apos;au{' '}
{new Date(data.tokenExpiresAt).toLocaleString('fr-FR')}
</p>
)}
</div>
)
}
if (data.status === 'FAILED') {
return (
<div>
<p>Echec du telechargement.</p>
<pre style={{ background: '#fee', padding: '0.5rem', overflowX: 'auto' }}>
{data.errorMsg}
</pre>
</div>
)
}
if (data.status === 'FILE_DELETED') {
return (
<p>
Le fichier a expire et a ete supprime. Le telechargement n&apos;est plus
disponible.
</p>
)
}
return <p>Statut inconnu : {data.status}</p>
}
+107
View File
@@ -0,0 +1,107 @@
'use client'
import { useState, FormEvent } from 'react'
import { useRouter } from 'next/navigation'
const FORMATS = ['mp4', 'mp3', 'webm', 'mkv']
const QUALITIES = ['best', '1080p', '720p', '480p', '360p']
export function SubmitForm() {
const router = useRouter()
const [url, setUrl] = useState('')
const [format, setFormat] = useState('mp4')
const [quality, setQuality] = useState('best')
const [subtitles, setSubtitles] = useState(false)
const [extraArgs, setExtraArgs] = useState('')
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
async function handleSubmit(e: FormEvent) {
e.preventDefault()
setError(null)
setLoading(true)
const res = await fetch('/api/downloads', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
url,
format,
quality,
subtitles,
extraArgs: extraArgs.trim() || null,
}),
})
setLoading(false)
if (res.status === 429) {
setError('Trop de soumissions. Reessayez dans une heure.')
return
}
if (!res.ok) {
const body = await res.json().catch(() => ({}))
setError((body as { error?: string }).error ?? 'Erreur lors de la soumission.')
return
}
const { uuid } = (await res.json()) as { uuid: string }
router.push(`/status/${uuid}`)
}
return (
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem', maxWidth: 480 }}>
<label>
URL de la video
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
required
placeholder="https://www.youtube.com/watch?v=..."
style={{ display: 'block', width: '100%' }}
/>
</label>
<label>
Format
<select value={format} onChange={(e) => setFormat(e.target.value)} style={{ display: 'block' }}>
{FORMATS.map((f) => <option key={f}>{f}</option>)}
</select>
</label>
<label>
Qualite
<select value={quality} onChange={(e) => setQuality(e.target.value)} style={{ display: 'block' }}>
{QUALITIES.map((q) => <option key={q}>{q}</option>)}
</select>
</label>
<label>
<input
type="checkbox"
checked={subtitles}
onChange={(e) => setSubtitles(e.target.checked)}
/>{' '}
Telecharger les sous-titres (fr, en)
</label>
<label>
Options avancees (optionnel, JSON)
<input
type="text"
value={extraArgs}
onChange={(e) => setExtraArgs(e.target.value)}
placeholder='["--sponsorblock-remove","all"]'
style={{ display: 'block', width: '100%' }}
/>
</label>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit" disabled={loading}>
{loading ? 'Envoi...' : 'Telecharger'}
</button>
</form>
)
}