Show request submission progress in a focused follow-up dialog
Magent CI/CD / verify (push) Successful in 1m53s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 55s

This commit is contained in:
2026-09-15 14:02:08 +12:00
parent c194db167a
commit 6a84e68a03
3 changed files with 63 additions and 22 deletions
+33 -20
View File
@@ -1,6 +1,8 @@
'use client'
import PageHeading from '../ui/PageHeading'
import { lockBodyScroll } from '../lib/scrollLock'
import './request-progress.css'
import { useEffect, useRef, useState } from 'react'
import { useRouter } from 'next/navigation'
@@ -96,12 +98,6 @@ const apiError = async (response: Response, fallback: string) => {
return fallback
}
const formatDuration = (milliseconds?: number | null) => {
if (milliseconds == null) return null
if (milliseconds < 1000) return `${Math.round(milliseconds)} ms`
return `${(milliseconds / 1000).toFixed(1)} s`
}
export default function NewRequestClient() {
const router = useRouter()
const searchSectionRef = useRef<HTMLElement | null>(null)
@@ -118,6 +114,16 @@ export default function NewRequestClient() {
const [selectedSeasons, setSelectedSeasons] = useState<number[]>([])
const [acceptOriginalLanguage, setAcceptOriginalLanguage] = useState<boolean | null>(null)
const [submitting, setSubmitting] = useState(false)
const [progressOpen, setProgressOpen] = useState(false)
const progressDialog = useRef<HTMLDialogElement>(null)
useEffect(() => {
if (!progressOpen) return
const previous = document.activeElement as HTMLElement | null
progressDialog.current?.showModal()
const unlock = lockBodyScroll()
return () => { progressDialog.current?.close(); unlock(); previous?.focus() }
}, [progressOpen])
const [operation, setOperation] = useState<OperationProgress | null>(null)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
@@ -269,12 +275,13 @@ export default function NewRequestClient() {
}
const submitRequest = async () => {
if (!selected || !options) return
if (!selected || !options || submitting) return
if (options.media.originalLanguage && acceptOriginalLanguage === null) { setError('Choose an audio language option before requesting.'); return }
if (selected.type === 'tv' && selectedSeasons.length === 0) {
setError('Select at least one season.')
return
}
setProgressOpen(true)
setSubmitting(true)
setError(null)
setSuccess(null)
@@ -309,7 +316,7 @@ export default function NewRequestClient() {
setResults((current) => current.map((item) => item.tmdbId === selected.tmdbId && item.type === selected.type
? { ...item, requestId, statusLabel: payload?.statusLabel ?? item.statusLabel }
: item))
setSuccess(requestId ? `Request #${requestId} has been accepted by Seerr.` : 'Your request has been accepted by Seerr.')
setSuccess('Your request has been received.')
} catch (caught) {
setError(caught instanceof Error ? caught.message : 'The request could not be submitted.')
} finally {
@@ -336,6 +343,22 @@ export default function NewRequestClient() {
return (
<main className="card request-portal-page">
<dialog ref={progressDialog} className="create-request-dialog" aria-labelledby="create-progress-title" onCancel={() => setProgressOpen(false)} onClose={() => setProgressOpen(false)}>
<div className="create-progress-header"><span>Request progress</span><button type="button" className="ghost-button" onClick={() => setProgressOpen(false)} aria-label="Close request progress">Close</button></div>
<div className="create-progress-body" aria-live="polite" aria-atomic="true">
{submitting && <span className="create-progress-spinner" aria-hidden="true" />}
<h2 id="create-progress-title">{submitting ? 'Sending your request' : success ? 'Request received' : 'Your request needs attention'}</h2>
<p className="create-progress-title">{selected?.title}</p>
<p>{submitting ? (operation?.events.some(event => event.service === 'Sonarr' || event.service === 'Radarr') ? 'Setting up your title for collection. Please wait.' : 'Checking your selection and sending it to the request service. Please wait.') : success ? 'Your request is now in the pipeline. Follow it to see approval, download progress and when it is ready to watch.' : error || 'We could not confirm the result. Check My requests before trying again.'}</p>
{submitting && <div className="create-progress-track" role="progressbar" aria-label="Submitting request"><span /></div>}
{success && <div className="create-progress-stage"><span>Current stage</span><strong>{selected?.statusLabel || 'Request received'}</strong></div>}
</div>
<div className="create-progress-actions">
{!submitting && <button type="button" className="create-progress-follow" onClick={() => router.push(selected?.requestId ? `/requests/${selected.requestId}` : '/')} >{success ? 'Follow your request' : 'Check My requests'} <span aria-hidden="true">&rarr;</span></button>}
{!submitting && <button type="button" className="ghost-button" onClick={() => setProgressOpen(false)}>{success ? 'Back to browsing' : 'Back to request'}</button>}
{submitting && <small>You can close this window. Submission will continue while you stay on this page.</small>}
</div>
</dialog>
<PageHeading title="New request" description="Find a movie or TV show and choose what you want to watch." />
<ol className="request-master-stepper" aria-label="New request progress">
@@ -513,20 +536,10 @@ export default function NewRequestClient() {
</div>
) : null}
{operation && (
<div className={`request-submit-progress is-${operation.status}`} aria-live="polite">
<header><div><span>Remote activity</span><strong>{operation.status === 'running' ? 'Sending your request' : operation.status === 'complete' ? 'Request hand-off complete' : 'Request hand-off needs attention'}</strong></div>{formatDuration(operation.duration_ms) && <small>{formatDuration(operation.duration_ms)}</small>}</header>
<div>
{operation.events.map((event) => (
<p key={event.id} className={`is-${event.state}`}><i aria-hidden="true" /><span><strong>{event.service}</strong>{event.message}</span><small>{formatDuration(event.duration_ms)}{event.status_code ? ` · HTTP ${event.status_code}` : ''}</small></p>
))}
{operation.events.length === 0 && <p className="is-active"><i aria-hidden="true" /><span><strong>Magent</strong>Preparing the request</span></p>}
</div>
</div>
)}
{operation && <button type="button" className="ghost-button" onClick={() => setProgressOpen(true)}>View request progress</button>}
{success && selected.requestId && (
<div className="request-complete-actions"><button type="button" onClick={() => router.push(`/requests/${selected.requestId}`)}>Track request #{selected.requestId}</button><button type="button" className="ghost-button" onClick={() => resetAfterType(selected.type)}>Request something else</button></div>
<div className="request-complete-actions"><button type="button" onClick={() => router.push(`/requests/${selected.requestId}`)}>Follow your request</button><button type="button" className="ghost-button" onClick={() => resetAfterType(selected.type)}>Request something else</button></div>
)}
</section>
)}
@@ -0,0 +1,18 @@
.create-request-dialog { width:min(560px,calc(100vw - 32px)); max-height:90dvh; overflow:auto; padding:28px; border:1px solid #514d62; border-radius:20px; background:#1b1b22; color:#eeeaf5; box-shadow:0 30px 100px #0009; }
.create-request-dialog::backdrop { background:#080910c9; backdrop-filter:blur(6px); }
.create-progress-header { display:flex; align-items:center; justify-content:space-between; gap:16px; color:#79e0eb; font-size:13px; }
.create-progress-body { padding:24px 0; }
.create-progress-body h2 { font-size:clamp(25px,4vw,34px); margin:12px 0; }
.create-progress-body p { color:#c3bfce; line-height:1.7; overflow-wrap:anywhere; }
.create-progress-body .create-progress-title { font-size:20px; color:#fff; font-weight:600; }
.create-progress-spinner { display:block; width:42px; height:42px; border:4px solid #ffffff20; border-top-color:#70e0e4; border-radius:50%; animation:create-spin .8s linear infinite; }
.create-progress-track { height:6px; background:#ffffff15; overflow:hidden; border-radius:6px; margin-top:24px; }
.create-progress-track span { display:block; width:35%; height:100%; background:#8edee5; animation:create-track 1.5s ease-in-out infinite alternate; }
.create-progress-stage { display:grid; gap:8px; border:1px solid #4c536a; border-radius:12px; padding:18px; background:#242938; }
.create-progress-stage span { color:#b9b6c6; font-size:12px; }
.create-progress-actions { display:grid; gap:12px; }
.create-progress-actions .create-progress-follow { padding:18px; background:#c5b8ff; color:#191629; font-size:18px; font-weight:700; border-radius:12px; }
.create-progress-actions small { color:#b9b6c6; line-height:1.6; }
@keyframes create-spin { to { transform:rotate(360deg); } }
@keyframes create-track { to { transform:translateX(185%); } }
@media(prefers-reduced-motion:reduce) { .create-progress-spinner,.create-progress-track span { animation:none; } }