feat: simplify ready requests and add global search
Magent CI/CD / verify (push) Successful in 1m56s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 48s

This commit is contained in:
2026-09-15 15:15:35 +12:00
parent dd51332f3c
commit 4ba1a5763e
10 changed files with 600 additions and 49 deletions
@@ -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()
+190
View File
@@ -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;
@@ -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<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&apos;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&apos;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&apos;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 wont 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>
</>
}
+39 -35
View File
@@ -4,11 +4,13 @@ import PageHeading from '../../ui/PageHeading'
import RequestLanguage from './RequestLanguage'
import { lockBodyScroll } from '../../lib/scrollLock'
import LatestActivity from './LatestActivity'
import RequestIssueDialog from './RequestIssueDialog'
import Image from 'next/image'
import { useParams, useRouter } from 'next/navigation'
import { useEffect, useMemo, useState } from 'react'
import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth'
import { canAccess } from '../../lib/features'
type TimelineHop = {
service: string
@@ -325,6 +327,7 @@ export default function RequestTimelinePage() {
const [historyActions, setHistoryActions] = useState<ActionHistory[]>([])
const [operationProgress, setOperationProgress] = useState<OperationProgress | null>(null)
const [isAdmin, setIsAdmin] = useState(false)
const [canReportIssues, setCanReportIssues] = useState(false)
const awaitingMediaIndex = Boolean(
snapshot?.presentation?.pipeline?.some(
(stage) => stage.id === 'available' && stage.state === 'active'
@@ -386,6 +389,7 @@ export default function RequestTimelinePage() {
const me = await meResponse.json()
const viewerIsAdmin = me?.role === 'admin'
setIsAdmin(viewerIsAdmin)
setCanReportIssues(canAccess(me, 'issues'))
if (!snapshotResponse.ok) {
throw new Error(await readApiError(snapshotResponse, 'Unable to load this request.'))
}
@@ -537,6 +541,7 @@ export default function RequestTimelinePage() {
const mediaServerLink = availableStage?.state === 'complete' && availableStage.link
? availableStage.link
: null
const requestComplete = ['COMPLETED', 'AVAILABLE'].includes(snapshot.state) || availableStage?.state === 'complete'
const statusLabel = presentation.status?.label ?? fallbackStatusLabel(snapshot.state)
const statusMeaning = presentation.status?.meaning ?? snapshot.state_reason ?? 'Magent is checking this request.'
const download = presentation.download
@@ -815,40 +820,39 @@ export default function RequestTimelinePage() {
</div>
)}
{operationProgress && <LatestActivity operation={operationProgress} besideDownload={downloadVisible} onDismiss={() => setOperationProgress(null)} />}
<div className="request-overview-block request-next-step">
<div className="request-next-step-main">
<div className="request-next-step-copy">
<span className="request-overview-label">Next step</span>
<strong>{nextStep.title}</strong>
<p>{nextStep.description}</p>
<div className={`request-overview-block request-next-step ${requestComplete ? 'is-ready' : ''}`}>
{requestComplete ? (
<div className="request-ready-actions">
<section>
<span className="request-overview-label">Ready to watch</span>
<strong>Watch this now!</strong>
<p>Open {snapshot.title} directly in Grizzlyflix.</p>
{mediaServerLink ? <a className="request-watch-button" href={mediaServerLink} target="_blank" rel="noreferrer">Watch on Grizzlyflix <span aria-hidden="true">&rarr;</span></a> : <span className="request-ready-unavailable">The Grizzlyflix watch link is not configured.</span>}
</section>
<section>
<span className="request-overview-label">Need help?</span>
<strong>Is there a problem with this?</strong>
<p>Let us know what is wrong and we&apos;ll attach the title and request details automatically.</p>
{canReportIssues ? <RequestIssueDialog requestId={snapshot.request_id} title={snapshot.title} year={snapshot.year} requestType={snapshot.request_type} /> : <span className="request-ready-unavailable">Issue reporting is not enabled for your account.</span>}
</section>
</div>
{mediaServerLink && (
<a
className="request-watch-button"
href={mediaServerLink}
target="_blank"
rel="noreferrer"
>
Watch on Grizzlyflix <span aria-hidden="true">&rarr;</span>
</a>
)}
</div>
<div className="request-action-row">
{recommendedActions.map((action) => (
<button key={action.id} type="button" disabled={Boolean(busyAction)} onClick={() => void runAction(action)}>
{busyAction === action.id ? 'Working…' : action.label}
</button>
))}
<button
type="button"
className="request-recheck-button"
disabled={Boolean(busyAction)}
onClick={() => void recheckRequest()}
title="Recheck Seerr, the library collector, qBittorrent, and the media server"
>
{busyAction === 'recheck_pipeline' ? 'Rechecking…' : 'Recheck request'}
</button>
</div>
) : <>
<div className="request-next-step-main">
<div className="request-next-step-copy">
<span className="request-overview-label">Next step</span>
<strong>{nextStep.title}</strong>
<p>{nextStep.description}</p>
</div>
</div>
<div className="request-action-row">
{recommendedActions.map((action) => (
<button key={action.id} type="button" disabled={Boolean(busyAction)} onClick={() => void runAction(action)}>
{busyAction === action.id ? 'Working…' : action.label}
</button>
))}
<button type="button" className="request-recheck-button" disabled={Boolean(busyAction)} onClick={() => void recheckRequest()} title="Recheck Seerr, the library collector, qBittorrent, and the media server">{busyAction === 'recheck_pipeline' ? 'Rechecking…' : 'Recheck request'}</button>
</div>
</>}
</div>
{(actionMessage || actionError) && (
<div className={`request-action-feedback ${actionError ? 'is-error' : 'is-success'}`} role="status">
@@ -886,7 +890,7 @@ export default function RequestTimelinePage() {
</section>
)}
<section className="request-journey" aria-labelledby="request-journey-heading">
{!requestComplete && <section className="request-journey" aria-labelledby="request-journey-heading">
<div className="request-journey-heading">
<div>
<span className="section-kicker">Live collection path</span>
@@ -975,7 +979,7 @@ export default function RequestTimelinePage() {
})}
</div>
</section>
</section>}
{releasePickerOpen && (
<div className="request-release-modal-layer">
+2 -1
View File
@@ -4,6 +4,7 @@ import { usePathname } from 'next/navigation'
import BrandingLogo from './BrandingLogo'
import HeaderActions from './HeaderActions'
import HeaderIdentity from './HeaderIdentity'
import GlobalSearch from './GlobalSearch'
import SiteStatus from './SiteStatus'
import UserViewBanner from './UserViewBanner'
import WorkspaceNavigation from './WorkspaceNavigation'
@@ -15,7 +16,7 @@ export default function ApplicationChrome() {
<header className="header">
<div className="header-left"><a className="brand-link" href="/"><BrandingLogo className="brand-logo brand-logo--header" /><div className="brand-stack"><div className="brand">Magent</div><div className="tagline">GrizzlyFlix media operations</div></div></a></div>
<div className="header-right"><span className="beta-chip" title="Beta environment">Beta</span><HeaderIdentity /></div>
<div className="header-nav"><HeaderActions /></div>
<div className="header-nav"><GlobalSearch /><HeaderActions /></div>
</header>
<WorkspaceNavigation />
<UserViewBanner />
+146
View File
@@ -0,0 +1,146 @@
'use client'
import { useRouter } from 'next/navigation'
import { useEffect, useRef, useState } from 'react'
import { authFetch, getApiBase } from '../lib/auth'
import { canAccess } from '../lib/features'
import { useFeatureUser } from './FeatureGate'
type SearchResult = {
title: string
year?: number | null
type: 'movie' | 'tv'
tmdbId: number
requestId?: number | null
statusLabel?: string | null
}
export default function GlobalSearch() {
const router = useRouter()
const { user, ready } = useFeatureUser()
const root = useRef<HTMLDivElement>(null)
const requestVersion = useRef(0)
const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [open, setOpen] = useState(false)
const [searching, setSearching] = useState(false)
const [message, setMessage] = useState<string | null>(null)
const canSearch = canAccess(user, 'new_requests') || canAccess(user, 'issues')
const canOpenRequests = canAccess(user, 'requests')
const canCreateRequests = canAccess(user, 'new_requests')
useEffect(() => {
const close = (event: PointerEvent) => {
if (!root.current?.contains(event.target as Node)) setOpen(false)
}
document.addEventListener('pointerdown', close)
return () => document.removeEventListener('pointerdown', close)
}, [])
useEffect(() => {
const term = query.trim()
requestVersion.current += 1
const version = requestVersion.current
if (term.length < 2 || !canSearch) {
setResults([])
setSearching(false)
setMessage(null)
return
}
setSearching(true)
setMessage(null)
const controller = new AbortController()
const timer = window.setTimeout(async () => {
try {
const params = new URLSearchParams({ query: term })
const response = await authFetch(`${getApiBase()}/requests/search?${params.toString()}`, {
signal: controller.signal,
cache: 'no-store',
})
if (!response.ok) throw new Error('Search is unavailable right now.')
const payload = await response.json()
if (version !== requestVersion.current) return
const mapped = (Array.isArray(payload?.results) ? payload.results : [])
.filter((item: any) => ['movie', 'tv'].includes(item?.type) && Number(item?.tmdbId) > 0)
.slice(0, 7)
.map((item: any): SearchResult => ({
title: String(item?.title || 'Untitled'),
year: typeof item?.year === 'number' ? item.year : null,
type: item.type,
tmdbId: Number(item.tmdbId),
requestId: typeof item?.requestId === 'number' ? item.requestId : null,
statusLabel: typeof item?.statusLabel === 'string' ? item.statusLabel : null,
}))
setResults(mapped)
setMessage(mapped.length ? null : 'No matching titles found.')
} catch (error) {
if (controller.signal.aborted || version !== requestVersion.current) return
console.error(error)
setResults([])
setMessage('Search is unavailable right now.')
} finally {
if (version === requestVersion.current) setSearching(false)
}
}, 280)
return () => {
window.clearTimeout(timer)
controller.abort()
}
}, [query, canSearch])
if (!ready || !user || !canSearch) return null
const openResult = (result: SearchResult) => {
setOpen(false)
setQuery('')
if (result.requestId && canOpenRequests) {
router.push(`/requests/${result.requestId}`)
return
}
if (canCreateRequests) {
const params = new URLSearchParams({ type: result.type, query: result.title })
router.push(`/new-requests?${params.toString()}`)
return
}
router.push('/portal/issues')
}
const showResults = open && query.trim().length >= 2
return (
<div className="global-search" ref={root}>
<search>
<form onSubmit={(event) => {
event.preventDefault()
if (results[0]) openResult(results[0])
}}>
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="m16 16 4 4" /></svg>
<input
type="search"
role="combobox"
value={query}
placeholder="Search titles or requests"
aria-label="Search titles or requests"
aria-autocomplete="list"
aria-expanded={showResults}
aria-controls="global-search-results"
onFocus={() => setOpen(true)}
onKeyDown={(event) => { if (event.key === 'Escape') setOpen(false) }}
onChange={(event) => { setQuery(event.target.value); setOpen(true) }}
/>
{searching && <i className="global-search-spinner" aria-hidden="true" />}
</form>
</search>
{showResults && (
<div className="global-search-results" id="global-search-results" role="listbox">
{results.map((result) => (
<button type="button" role="option" aria-selected="false" key={`${result.type}:${result.tmdbId}`} onClick={() => openResult(result)}>
<span><strong>{result.title}</strong><small>{result.type === 'tv' ? 'TV show' : 'Movie'}{result.year ? ` · ${result.year}` : ''}</small></span>
<b>{result.requestId && canOpenRequests ? result.statusLabel || 'View request' : canCreateRequests ? 'New request' : 'Report issue'}</b>
</button>
))}
{!searching && message && <p role="status">{message}</p>}
</div>
)}
</div>
)
}
+10 -10
View File
@@ -36,16 +36,6 @@ export default function HeaderActions() {
]
const commonItems = [
{
href: '/insights',
label: 'My Stats',
match: (path: string) => path === '/insights' || path.startsWith('/insights/'),
},
{
href: '/',
label: 'My Requests',
match: (path: string) => path === '/' || path.startsWith('/requests/'),
},
...(showRequestsNav
? [
{
@@ -55,6 +45,16 @@ export default function HeaderActions() {
},
]
: []),
{
href: '/',
label: 'My Requests',
match: (path: string) => path === '/' || path.startsWith('/requests/'),
},
{
href: '/insights',
label: 'My Stats',
match: (path: string) => path === '/insights' || path.startsWith('/insights/'),
},
{
href: '/portal/issues',
label: 'Issues',
+2 -2
View File
@@ -15,9 +15,9 @@ type NavigationItem = {
}
const NAVIGATION: NavigationItem[] = [
{ href: '/insights', label: 'My Stats', shortLabel: 'Stats', icon: 'stats', match: (path) => path === '/insights' || path.startsWith('/insights/') },
{ href: '/', label: 'My Requests', shortLabel: 'Requests', icon: 'dashboard', match: (path) => path === '/' || path.startsWith('/requests/') },
{ href: '/new-requests', label: 'New Request', shortLabel: 'New', icon: 'media', match: (path) => path === '/new-requests' },
{ href: '/', label: 'My Requests', shortLabel: 'Requests', icon: 'dashboard', match: (path) => path === '/' || path.startsWith('/requests/') },
{ href: '/insights', label: 'My Stats', shortLabel: 'Stats', icon: 'stats', match: (path) => path === '/insights' || path.startsWith('/insights/') },
{ href: '/portal/issues', label: 'Issues', shortLabel: 'Issues', icon: 'issues', match: (path) => path.startsWith('/portal/issues') },
{ href: '/profile/invites', label: 'Invites', shortLabel: 'Invites', icon: 'invites', match: (path) => path.startsWith('/profile/invites') },
{ href: '/admin', label: 'Configuration', shortLabel: 'Config', icon: 'settings', adminOnly: true, match: (path) => path.startsWith('/admin') || path.startsWith('/users') },
+1 -1
View File
@@ -29,7 +29,7 @@ export default function WelcomePage() {
<header><span className="welcome-kicker">GrizzlyFlix + Magent</span><h1>Make yourself at home.</h1><p>Something to watch, or something to sort out?</p></header>
{error ? <div role="alert"><p>{error}</p><button type="button" onClick={() => window.location.reload()}>Try again</button> <a href="/login">Back to sign in</a></div> : !ready ? <p role="status">Getting things ready</p> : <div className="welcome-choices">
{url ? <a className="welcome-choice" href={url}><span className="welcome-icon" aria-hidden="true"></span><h2>Go to GrizzlyFlix</h2><p>Find your next favourite. Watch movies and TV shows.</p><strong>Lets watch <span aria-hidden="true"></span></strong></a> : <section className="welcome-choice"><span className="welcome-icon" aria-hidden="true"></span><h2>Go to GrizzlyFlix</h2><p>The watch link hasnt been set up yet. Please ask an admin to add the public playback URL.</p></section>}
<a className="welcome-choice" href="/"><span className="welcome-icon" aria-hidden="true"></span><h2>Manage your account</h2><p>Track requests, report a problem, or update your profile.</p><strong>Open My Requests <span aria-hidden="true"></span></strong></a>
<a className="welcome-choice" href="/"><span className="welcome-icon" aria-hidden="true"></span><h2>View stats &amp; requests</h2><p>Check your viewing stats, follow your requests, or make a new one.</p><strong>Open Magent <span aria-hidden="true"></span></strong></a>
</div>}
<footer>First time here? <a href="/how-it-works">Heres how it works</a>.</footer>
</main>