chore: standardize security and quality foundations
Magent CI/CD / verify (push) Failing after 9m34s
Magent CI/CD / deploy-beta (push) Skipped

This commit is contained in:
2026-09-17 20:03:47 +12:00
parent 5639dbcb83
commit f852e7c941
127 changed files with 17928 additions and 10741 deletions
+135 -102
View File
@@ -1,146 +1,179 @@
'use client'
"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'
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
}
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')
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)
}, [])
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
const term = query.trim();
requestVersion.current += 1;
const version = requestVersion.current;
if (term.length < 2 || !canSearch) {
setResults([])
setSearching(false)
setMessage(null)
return
setResults([]);
setSearching(false);
setMessage(null);
return;
}
setSearching(true)
setMessage(null)
const controller = new AbortController()
setSearching(true);
setMessage(null);
const controller = new AbortController();
const timer = window.setTimeout(async () => {
try {
const params = new URLSearchParams({ query: term })
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
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)
.filter(
(item: Record<string, unknown>) =>
typeof item.type === "string" && ["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.')
.map(
(item: Record<string, unknown>): SearchResult => ({
title: String(item?.title || "Untitled"),
year: typeof item?.year === "number" ? item.year : null,
type: item.type === "tv" ? "tv" : "movie",
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.')
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)
if (version === requestVersion.current) setSearching(false);
}
}, 280)
}, 280);
return () => {
window.clearTimeout(timer)
controller.abort()
}
}, [query, canSearch])
window.clearTimeout(timer);
controller.abort();
};
}, [query, canSearch]);
if (!ready || !user || !canSearch) return null
if (!ready || !user || !canSearch) return null;
const openResult = (result: SearchResult) => {
setOpen(false)
setQuery('')
setOpen(false);
setQuery("");
if (result.requestId && canOpenRequests) {
router.push(`/requests/${result.requestId}`)
return
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
const params = new URLSearchParams({ type: result.type, query: result.title });
router.push(`/new-requests?${params.toString()}`);
return;
}
router.push('/portal/issues')
}
router.push("/portal/issues");
};
const showResults = open && query.trim().length >= 2
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>
<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
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>
)
);
}