144 lines
4.5 KiB
TypeScript
144 lines
4.5 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useTranslations, useLocale } from 'next-intl'
|
|
|
|
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
|
|
}
|
|
|
|
function Spinner() {
|
|
return (
|
|
<svg className="animate-spin w-8 h-8 text-violet-600 dark:text-violet-400" viewBox="0 0 24 24" fill="none" aria-hidden>
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
export function StatusView({ uuid }: { uuid: string }) {
|
|
const t = useTranslations('status')
|
|
const locale = useLocale()
|
|
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])
|
|
|
|
const cardClass = 'max-w-md w-full mx-auto rounded-2xl border border-gray-100 dark:border-slate-800 bg-gray-50 dark:bg-slate-900 p-8'
|
|
|
|
if (notFound) {
|
|
return (
|
|
<div className={cardClass}>
|
|
<p className="text-gray-600 dark:text-gray-400 text-center">{t('notFound')}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!data) {
|
|
return (
|
|
<div className={`${cardClass} flex flex-col items-center gap-4`}>
|
|
<Spinner />
|
|
<p className="text-gray-500 dark:text-gray-400 text-sm">{t('loading')}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (data.status === 'PENDING' || data.status === 'PROCESSING') {
|
|
return (
|
|
<div className={`${cardClass} flex flex-col items-center gap-4`}>
|
|
<Spinner />
|
|
<p className="font-medium text-gray-900 dark:text-gray-50">
|
|
{data.status === 'PENDING' ? t('pending') : t('processing')}
|
|
</p>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-center">{t('polling')}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (data.status === 'DONE' && data.downloadToken) {
|
|
const sizeMb = data.fileSize
|
|
? `(${(Number(data.fileSize) / 1_048_576).toFixed(1)} MB)`
|
|
: ''
|
|
return (
|
|
<div className={`${cardClass} flex flex-col items-center gap-5`}>
|
|
<div className="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-green-600 dark:text-green-400" aria-hidden>
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>
|
|
</div>
|
|
<p className="font-medium text-gray-900 dark:text-gray-50">
|
|
{t('ready')} {sizeMb}
|
|
</p>
|
|
<a
|
|
href={`/api/download/${data.downloadToken}`}
|
|
download={data.fileName ?? undefined}
|
|
className="w-full text-center rounded-xl bg-violet-600 hover:bg-violet-700 dark:bg-violet-500 dark:hover:bg-violet-600 text-white font-medium py-3 transition-colors"
|
|
>
|
|
{t('download')} {data.fileName}
|
|
</a>
|
|
{data.tokenExpiresAt && (
|
|
<p className="text-xs text-gray-400 dark:text-slate-500">
|
|
{t('linkExpires')}{' '}
|
|
{new Date(data.tokenExpiresAt).toLocaleString(locale)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (data.status === 'FAILED') {
|
|
return (
|
|
<div className={cardClass}>
|
|
<p className="font-medium text-red-600 dark:text-red-400 mb-3">{t('failed')}</p>
|
|
<pre className="text-xs bg-red-50 dark:bg-red-950/40 border border-red-100 dark:border-red-900/50 rounded-lg p-4 overflow-x-auto text-red-700 dark:text-red-300 whitespace-pre-wrap">
|
|
{data.errorMsg}
|
|
</pre>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (data.status === 'FILE_DELETED') {
|
|
return (
|
|
<div className={cardClass}>
|
|
<p className="text-gray-600 dark:text-gray-400 text-center">{t('deleted')}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className={cardClass}>
|
|
<p className="text-gray-500 dark:text-gray-400 text-center">{t('unknown')}: {data.status}</p>
|
|
</div>
|
|
)
|
|
}
|