120 lines
4.8 KiB
TypeScript
120 lines
4.8 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useRef, useState } from 'react'
|
||
import { authFetch, getApiBase } from '../../lib/auth'
|
||
import { lockBodyScroll } from '../../lib/scrollLock'
|
||
|
||
type Props = {
|
||
requestId: string
|
||
title: string
|
||
year?: number
|
||
requestType: string
|
||
}
|
||
|
||
const readError = async (response: Response) => {
|
||
try {
|
||
const payload = await response.json()
|
||
if (typeof payload?.detail === 'string' && payload.detail.trim()) return payload.detail
|
||
} catch {
|
||
// Use the friendly fallback below.
|
||
}
|
||
return 'Your report could not be submitted. Please try again.'
|
||
}
|
||
|
||
export default function RequestIssueDialog({ requestId, title, year, requestType }: Props) {
|
||
const dialog = useRef<HTMLDialogElement>(null)
|
||
const detailsInput = useRef<HTMLTextAreaElement>(null)
|
||
const [open, setOpen] = useState(false)
|
||
const [details, setDetails] = useState('')
|
||
const [submitting, setSubmitting] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [issueId, setIssueId] = useState<number | null>(null)
|
||
|
||
useEffect(() => {
|
||
if (!open) return
|
||
const previous = document.activeElement as HTMLElement | null
|
||
dialog.current?.showModal()
|
||
const unlock = lockBodyScroll()
|
||
window.setTimeout(() => detailsInput.current?.focus(), 0)
|
||
return () => {
|
||
dialog.current?.close()
|
||
unlock()
|
||
previous?.focus()
|
||
}
|
||
}, [open])
|
||
|
||
const close = () => {
|
||
if (submitting) return
|
||
setOpen(false)
|
||
setError(null)
|
||
if (issueId) {
|
||
setIssueId(null)
|
||
setDetails('')
|
||
}
|
||
}
|
||
|
||
const submit = async (event: React.FormEvent) => {
|
||
event.preventDefault()
|
||
const description = details.trim()
|
||
if (!description) {
|
||
setError('Tell us what is wrong so we know what to check.')
|
||
detailsInput.current?.focus()
|
||
return
|
||
}
|
||
setSubmitting(true)
|
||
setError(null)
|
||
try {
|
||
const response = await authFetch(`${getApiBase()}/portal/items`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
kind: 'issue',
|
||
title: `Problem with ${title}`,
|
||
description,
|
||
media_type: requestType === 'tv' ? 'tv' : 'movie',
|
||
year: year ?? null,
|
||
external_ref: `/requests/${requestId}`,
|
||
issue_type: 'general',
|
||
priority: 'normal',
|
||
}),
|
||
})
|
||
if (!response.ok) throw new Error(await readError(response))
|
||
const payload = await response.json()
|
||
const createdId = Number(payload?.item?.id)
|
||
if (!Number.isInteger(createdId) || createdId <= 0) throw new Error('The issue was created, but its reference could not be loaded.')
|
||
setIssueId(createdId)
|
||
} catch (caught) {
|
||
console.error(caught)
|
||
setError(caught instanceof Error ? caught.message : 'Your report could not be submitted. Please try again.')
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
return <>
|
||
<button type="button" className="request-problem-button" onClick={() => setOpen(true)}>Tell us what's wrong</button>
|
||
<dialog ref={dialog} className="request-issue-dialog" aria-labelledby="request-issue-title" onCancel={(event) => { event.preventDefault(); close() }}>
|
||
{issueId ? (
|
||
<div className="request-issue-success" role="status">
|
||
<span aria-hidden="true">✓</span>
|
||
<h2 id="request-issue-title">Thanks, we've got it</h2>
|
||
<p>Your report is now issue #{issueId}. You can follow its progress from the Issues page.</p>
|
||
<div><a href={`/portal/issues?item=${issueId}`}>View reported issue</a><button type="button" className="ghost-button" onClick={close}>Done</button></div>
|
||
</div>
|
||
) : (
|
||
<form onSubmit={submit}>
|
||
<header>
|
||
<div><span className="section-kicker">Report a problem</span><h2 id="request-issue-title">What's wrong?</h2></div>
|
||
<button type="button" className="ghost-button" onClick={close} disabled={submitting}>Close</button>
|
||
</header>
|
||
<div className="request-issue-media"><span>{requestType === 'tv' ? 'TV show' : 'Movie'}</span><strong>{title}{year ? ` (${year})` : ''}</strong><small>Request #{requestId} · Ready to watch</small></div>
|
||
<label htmlFor="request-issue-details">Tell us what happened<textarea ref={detailsInput} id="request-issue-details" rows={6} maxLength={10000} value={details} onChange={(event) => setDetails(event.target.value)} placeholder="For example: it won’t play, the audio is wrong, or the wrong version is showing…" disabled={submitting} /></label>
|
||
<p className="request-issue-help">The title and request details are included automatically.</p>
|
||
{error && <p className="error-banner" role="alert">{error}</p>}
|
||
<footer><button type="submit" disabled={submitting || !details.trim()}>{submitting ? 'Sending report…' : 'Send report'}</button></footer>
|
||
</form>
|
||
)}
|
||
</dialog>
|
||
</>
|
||
}
|