diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..ac7e01f
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,13 @@
+import type { Metadata } from 'next'
+
+export const metadata: Metadata = {
+ title: 'Ombrora — Telechargeur de videos',
+}
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
{children}
+
+ )
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..12a0357
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,10 @@
+import { SubmitForm } from '@/components/SubmitForm'
+
+export default function Home() {
+ return (
+
+ Ombrora
+
+
+ )
+}
diff --git a/src/app/status/[uuid]/page.tsx b/src/app/status/[uuid]/page.tsx
new file mode 100644
index 0000000..92287cb
--- /dev/null
+++ b/src/app/status/[uuid]/page.tsx
@@ -0,0 +1,15 @@
+import { StatusView } from '@/components/StatusView'
+
+export default async function StatusPage({
+ params,
+}: {
+ params: Promise<{ uuid: string }>
+}) {
+ const { uuid } = await params
+ return (
+
+ Statut du telechargement
+
+
+ )
+}
diff --git a/src/components/StatusView.tsx b/src/components/StatusView.tsx
new file mode 100644
index 0000000..b6e4493
--- /dev/null
+++ b/src/components/StatusView.tsx
@@ -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(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 Telechargement introuvable.
+ if (!data) return Chargement...
+
+ if (data.status === 'PENDING' || data.status === 'PROCESSING') {
+ return (
+
+
Statut : {data.status === 'PENDING' ? 'En attente' : 'En cours...'}
+
Cette page se rafraichit automatiquement toutes les 5 secondes.
+
+ )
+ }
+
+ if (data.status === 'DONE' && data.downloadToken) {
+ const sizeMb = data.fileSize
+ ? `(${(Number(data.fileSize) / 1_048_576).toFixed(1)} Mo)`
+ : ''
+ return (
+
+
Telechargement pret {sizeMb}
+
+ Telecharger {data.fileName}
+
+ {data.tokenExpiresAt && (
+
+ Lien valable jusqu'au{' '}
+ {new Date(data.tokenExpiresAt).toLocaleString('fr-FR')}
+
+ )}
+
+ )
+ }
+
+ if (data.status === 'FAILED') {
+ return (
+
+
Echec du telechargement.
+
+ {data.errorMsg}
+
+
+ )
+ }
+
+ if (data.status === 'FILE_DELETED') {
+ return (
+
+ Le fichier a expire et a ete supprime. Le telechargement n'est plus
+ disponible.
+
+ )
+ }
+
+ return Statut inconnu : {data.status}
+}
diff --git a/src/components/SubmitForm.tsx b/src/components/SubmitForm.tsx
new file mode 100644
index 0000000..8d03798
--- /dev/null
+++ b/src/components/SubmitForm.tsx
@@ -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(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 (
+
+ )
+}