feat(release): publish minimal self-contained Magent source

This commit is contained in:
Magent release tooling
2026-09-19 16:58:12 +12:00
commit 5fa5d45535
272 changed files with 79305 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
"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: Record<string, unknown>) =>
typeof item.type === "string" && ["movie", "tv"].includes(item.type) && Number(item.tmdbId) > 0,
)
.slice(0, 7)
.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.");
} 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>
);
}