diff --git a/frontend/app/new-requests/NewRequestClient.tsx b/frontend/app/new-requests/NewRequestClient.tsx index 3527ac2..6d11c11 100644 --- a/frontend/app/new-requests/NewRequestClient.tsx +++ b/frontend/app/new-requests/NewRequestClient.tsx @@ -132,6 +132,17 @@ export default function NewRequestClient() { if (!getToken()) router.push('/login') }, [router]) + useEffect(() => { + const params = new URLSearchParams(window.location.search) + const requestedType = params.get('type') + const requestedQuery = params.get('query')?.trim() + if ((requestedType === 'movie' || requestedType === 'tv') && requestedQuery) { + setMediaType(requestedType) + setQuery(requestedQuery) + window.setTimeout(() => searchSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }), 80) + } + }, []) + const selectedTitleId = selected?.tmdbId useEffect(() => { if (selectedTitleId) configureSectionRef.current?.focus() diff --git a/frontend/app/ops-redesign.css b/frontend/app/ops-redesign.css index b1d5f72..9c3a79c 100644 --- a/frontend/app/ops-redesign.css +++ b/frontend/app/ops-redesign.css @@ -3541,6 +3541,196 @@ textarea:focus { .config-subsection-nav { top: 62px; } } +/* Global title search */ +.header-nav { + display: flex; + min-width: 0; + align-items: center; + justify-content: center; + gap: 10px; +} +.header-nav .header-actions { flex: 0 0 auto; width: auto; } +.global-search { + position: relative; + z-index: 20; + flex: 0 1 240px; + width: 240px; + min-width: 170px; +} +.global-search form { + position: relative; + display: flex; + align-items: center; +} +.global-search form > svg { + position: absolute; + left: 11px; + width: 17px; + height: 17px; + fill: none; + stroke: var(--ops-muted); + stroke-linecap: round; + stroke-width: 1.8; + pointer-events: none; +} +.global-search input { + width: 100%; + height: 36px; + padding: 7px 34px 7px 36px; + border: 1px solid var(--ops-line); + border-radius: 8px; + background: var(--ops-panel-2); + color: var(--ops-text); + font-size: 0.78rem; +} +.global-search input:focus { + border-color: rgba(126, 215, 255, 0.58); + outline: 2px solid rgba(14, 165, 233, 0.16); +} +.global-search input::placeholder { color: var(--ops-muted); } +.global-search-spinner { + position: absolute; + right: 11px; + width: 14px; + height: 14px; + border: 2px solid var(--ops-line); + border-top-color: var(--ops-primary-2); + border-radius: 50%; + animation: request-operation-spin 0.75s linear infinite; +} +.global-search-results { + position: absolute; + inset: calc(100% + 8px) 0 auto; + display: grid; + max-height: min(440px, calc(100vh - 92px)); + overflow-y: auto; + padding: 6px; + border: 1px solid var(--ops-line); + border-radius: 10px; + background: #1c1b1d; + box-shadow: 0 20px 48px rgba(0, 0, 0, 0.46); +} +.global-search-results button { + display: flex; + width: 100%; + min-height: 56px; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 9px 10px; + border: 0; + border-radius: 7px; + background: transparent; + color: var(--ops-text); + text-align: left; +} +.global-search-results button:hover, +.global-search-results button:focus-visible { background: var(--ops-panel-3); } +.global-search-results button > span { display: grid; min-width: 0; gap: 2px; } +.global-search-results strong { overflow: hidden; font-size: 0.8rem; text-overflow: ellipsis; white-space: nowrap; } +.global-search-results small { color: var(--ops-muted); font-size: 0.66rem; } +.global-search-results b { flex: 0 0 auto; color: var(--ops-primary-2); font-size: 0.63rem; } +.global-search-results p { margin: 0; padding: 14px 10px; color: var(--ops-muted); font-size: 0.75rem; } + +/* The finished request becomes a compact watch-or-report destination. */ +.request-next-step.is-ready { padding: 0; } +.request-ready-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.request-ready-actions > section { + display: grid; + align-content: start; + gap: 8px; + min-width: 0; + padding: 20px; +} +.request-ready-actions > section + section { border-left: 1px solid var(--ops-line-soft); } +.request-ready-actions > section > strong { color: var(--ops-text); font-size: 1.12rem; } +.request-ready-actions > section > p { min-height: 44px; margin: 0; color: var(--ops-muted); line-height: 1.5; } +.request-ready-actions .request-watch-button, +.request-problem-button { + width: 100%; + min-width: 0; + min-height: 46px; + margin-top: 5px; +} +.request-problem-button { + border: 1px solid rgba(126, 215, 255, 0.36); + border-radius: var(--ops-radius); + background: rgba(79, 70, 229, 0.18); + color: var(--ops-text); + font-weight: 750; +} +.request-problem-button:hover { border-color: rgba(126, 215, 255, 0.7); background: rgba(79, 70, 229, 0.28); } +.request-ready-unavailable { margin-top: 5px; padding: 12px; border: 1px solid var(--ops-line); border-radius: var(--ops-radius); color: var(--ops-muted); font-size: 0.75rem; } + +.request-issue-dialog { + width: min(620px, calc(100vw - 28px)); + max-height: min(760px, calc(100vh - 28px)); + margin: auto; + padding: 0; + overflow: auto; + border: 1px solid var(--ops-line); + border-radius: 12px; + background: #201f21; + color: var(--ops-text); + box-shadow: 0 28px 80px rgba(0, 0, 0, 0.58); +} +.request-issue-dialog::backdrop { background: rgba(5, 5, 8, 0.76); backdrop-filter: blur(5px); } +.request-issue-dialog form { display: grid; gap: 18px; padding: 22px; } +.request-issue-dialog form > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; } +.request-issue-dialog h2 { margin: 3px 0 0; font-size: 1.55rem; } +.request-issue-media { display: grid; gap: 4px; padding: 14px; border: 1px solid var(--ops-line); border-radius: 9px; background: var(--ops-panel-2); } +.request-issue-media > span, +.request-issue-media > small { color: var(--ops-muted); font-size: 0.7rem; } +.request-issue-media > strong { font-size: 1rem; } +.request-issue-dialog label { display: grid; gap: 8px; color: var(--ops-text); font-size: 0.8rem; font-weight: 750; } +.request-issue-dialog textarea { width: 100%; resize: vertical; border-color: var(--ops-line); background: var(--ops-panel-2); color: var(--ops-text); line-height: 1.5; } +.request-issue-help { margin: -10px 0 0; color: var(--ops-muted); font-size: 0.72rem; } +.request-issue-dialog footer { display: flex; justify-content: flex-end; } +.request-issue-dialog footer > button, +.request-issue-success a { min-height: 42px; padding: 10px 18px; } +.request-issue-success { display: grid; justify-items: center; gap: 12px; padding: 34px 24px; text-align: center; } +.request-issue-success > span { display: grid; width: 52px; height: 52px; place-items: center; border: 1px solid rgba(72, 224, 178, 0.5); border-radius: 50%; color: var(--request-green); font-size: 1.5rem; } +.request-issue-success h2, +.request-issue-success p { margin: 0; } +.request-issue-success p { max-width: 50ch; color: var(--ops-muted); line-height: 1.55; } +.request-issue-success > div { display: flex; gap: 10px; margin-top: 8px; } +.request-issue-success a { display: inline-flex; align-items: center; border-radius: var(--ops-radius); background: var(--ops-primary); color: var(--ops-primary-2); font-weight: 750; text-decoration: none; } + +@media (max-width: 1250px) { + .global-search { flex-basis: 190px; width: 190px; } +} + +@media (max-width: 980px) { + .header { height: 108px; grid-template-rows: 58px 50px; } + .header-left, + .header-right { grid-row: 1; } + .header-nav { + display: flex; + grid-column: 1 / -1; + grid-row: 2; + width: 100%; + } + .header-nav .header-actions { display: none; } + .global-search { flex: 1 1 auto; width: 100%; max-width: none; } + .global-search-results { max-height: min(420px, calc(100vh - 190px)); } + .page { padding-top: 108px; } +} + +@media (max-width: 680px) { + .header { height: 104px; grid-template-rows: 54px 50px; } + .page { padding-top: 104px; } + .global-search input { height: 38px; } + .request-ready-actions { grid-template-columns: 1fr; } + .request-ready-actions > section + section { border-top: 1px solid var(--ops-line-soft); border-left: 0; } + .request-ready-actions > section > p { min-height: 0; } + .request-issue-dialog form { padding: 18px; } + .request-issue-success > div { width: 100%; flex-direction: column; } + .request-issue-success a { justify-content: center; } +} + /* Guided issue reporting */ .issue-portal-page { display: grid; diff --git a/frontend/app/requests/[id]/RequestIssueDialog.tsx b/frontend/app/requests/[id]/RequestIssueDialog.tsx new file mode 100644 index 0000000..39deafe --- /dev/null +++ b/frontend/app/requests/[id]/RequestIssueDialog.tsx @@ -0,0 +1,119 @@ +'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(null) + const detailsInput = useRef(null) + const [open, setOpen] = useState(false) + const [details, setDetails] = useState('') + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) + const [issueId, setIssueId] = useState(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 <> + + { event.preventDefault(); close() }}> + {issueId ? ( +
+ +

Thanks, we've got it

+

Your report is now issue #{issueId}. You can follow its progress from the Issues page.

+ +
+ ) : ( +
+
+
Report a problem

What's wrong?

+ +
+
{requestType === 'tv' ? 'TV show' : 'Movie'}{title}{year ? ` (${year})` : ''}Request #{requestId} · Ready to watch
+