Show request submission progress in a focused follow-up dialog
This commit is contained in:
@@ -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">→</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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user