Process 1 build 0803262229

This commit is contained in:
2026-03-08 22:30:49 +13:00
parent f830fc1296
commit 3609f44607
6 changed files with 259 additions and 176 deletions
+1 -1
View File
@@ -1 +1 @@
0803262216
0803262229
File diff suppressed because one or more lines are too long
+22 -1
View File
@@ -6565,6 +6565,27 @@ textarea {
gap: 16px;
}
.portal-workspace-switch {
display: inline-flex;
gap: 8px;
align-items: center;
}
.portal-workspace-switch button {
border: 1px solid var(--line);
border-radius: 10px;
background: var(--panel-soft);
color: var(--text);
padding: 8px 12px;
font-weight: 600;
}
.portal-workspace-switch button.is-active {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(107, 146, 255, 0.25);
background: rgba(107, 146, 255, 0.12);
}
.portal-overview-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -6689,7 +6710,7 @@ textarea {
.portal-toolbar {
display: grid;
grid-template-columns: 160px 180px minmax(0, 1fr) auto;
grid-template-columns: 180px minmax(0, 1fr) auto;
gap: 10px;
align-items: end;
}
+231 -169
View File
@@ -80,12 +80,6 @@ type DiscoveryResult = {
backdropPath?: string | null
}
const KIND_OPTIONS = [
{ value: 'request', label: 'Request' },
{ value: 'issue', label: 'Issue' },
{ value: 'feature', label: 'Feature' },
] as const
const STATUS_OPTIONS = [
{ value: 'new', label: 'New' },
{ value: 'triaging', label: 'Triaging' },
@@ -131,6 +125,31 @@ const MEDIA_TYPE_OPTIONS = [
{ value: 'tv', label: 'TV' },
] as const
const WORKSPACE_OPTIONS = [
{ value: 'request', label: 'Requests' },
{ value: 'issue', label: 'Issues' },
] as const
const REQUEST_FILTER_STATUS_OPTIONS = [
{ value: 'pending', label: 'Pending approval' },
{ value: 'approved', label: 'Approved' },
{ value: 'processing', label: 'Processing' },
{ value: 'partially_available', label: 'Partially available' },
{ value: 'available', label: 'Available' },
{ value: 'failed', label: 'Failed' },
{ value: 'declined', label: 'Declined' },
] as const
const ISSUE_FILTER_STATUS_OPTIONS = [
{ value: 'new', label: 'New' },
{ value: 'triaging', label: 'Triaging' },
{ value: 'planned', label: 'Planned' },
{ value: 'in_progress', label: 'In progress' },
{ value: 'blocked', label: 'Blocked' },
{ value: 'done', label: 'Done' },
{ value: 'closed', label: 'Closed' },
] as const
const formatDate = (value?: string | null) => {
if (!value) return 'Never'
const parsed = new Date(value)
@@ -161,13 +180,13 @@ export default function PortalPage() {
const [status, setStatus] = useState<string | null>(null)
const [totalItems, setTotalItems] = useState(0)
const [hasMore, setHasMore] = useState(false)
const [workspace, setWorkspace] = useState<'request' | 'issue'>('request')
const [filterKind, setFilterKind] = useState('')
const [filterKind, setFilterKind] = useState<'request' | 'issue'>('request')
const [filterStatus, setFilterStatus] = useState('')
const [filterMine, setFilterMine] = useState(false)
const [filterSearch, setFilterSearch] = useState('')
const [createKind, setCreateKind] = useState<'request' | 'issue' | 'feature'>('request')
const [createTitle, setCreateTitle] = useState('')
const [createDescription, setCreateDescription] = useState('')
const [createMediaType, setCreateMediaType] = useState('')
@@ -196,6 +215,9 @@ export default function PortalPage() {
const [requestingTmdbIds, setRequestingTmdbIds] = useState<Record<string, boolean>>({})
const isAdmin = me?.role === 'admin'
const visibleKindCount = Number(overview?.overview?.by_kind?.[workspace] ?? 0)
const workspaceLabel = workspace === 'request' ? 'request' : 'issue'
const workspaceLabelPlural = workspace === 'request' ? 'requests' : 'issues'
useEffect(() => {
if (typeof window === 'undefined') return
@@ -285,7 +307,7 @@ export default function PortalPage() {
limit: '60',
offset: '0',
})
if (filterKind) params.set('kind', filterKind)
params.set('kind', filterKind)
if (filterStatus) params.set('status', filterStatus)
if (filterMine) params.set('mine', '1')
const trimmedSearch = filterSearch.trim()
@@ -468,6 +490,17 @@ export default function PortalPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterKind, filterStatus, filterMine, filterSearch])
useEffect(() => {
setFilterKind(workspace)
setFilterStatus('')
setCreateMediaType('')
setCreateYear('')
setSelectedItemId(null)
setSelectedItem(null)
setComments([])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [workspace])
useEffect(() => {
if (selectedItemId == null) return
void loadItem(selectedItemId)
@@ -495,11 +528,11 @@ export default function PortalPage() {
setStatus(null)
try {
const payload: Record<string, any> = {
kind: createKind,
kind: workspace,
title: createTitle,
description: createDescription,
media_type: createMediaType || null,
year: createYear.trim() ? toPositiveInt(createYear) : null,
media_type: workspace === 'request' ? createMediaType || null : null,
year: workspace === 'request' && createYear.trim() ? toPositiveInt(createYear) : null,
external_ref: createExternalRef || null,
priority: createPriority,
}
@@ -520,7 +553,7 @@ export default function PortalPage() {
}
const data = await response.json()
const item = data?.item as PortalItem | undefined
setStatus('Portal item created.')
setStatus(workspace === 'request' ? 'Request item created.' : 'Issue item created.')
setCreateTitle('')
setCreateDescription('')
setCreateMediaType('')
@@ -649,97 +682,118 @@ export default function PortalPage() {
<div>
<h1>Request portal</h1>
<p className="lede">
Raise requests, issues, and feature ideas. Track progress and keep discussion in one place.
Manage requests and issues in separate workflows.
</p>
</div>
</div>
<section className="portal-workspace-switch">
{WORKSPACE_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
className={workspace === option.value ? 'is-active' : ''}
onClick={() => setWorkspace(option.value)}
>
{option.label}
</button>
))}
</section>
{error && <div className="error-banner">{error}</div>}
{status && <div className="status-banner">{status}</div>}
<section className="admin-panel portal-discovery-panel">
<div className="user-directory-panel-header">
<div>
<h2>Search and request content</h2>
<p className="lede">
Search Seerr content directly, then submit a request in one click.
</p>
{workspace === 'request' ? (
<section className="admin-panel portal-discovery-panel">
<div className="user-directory-panel-header">
<div>
<h2>Search and request content</h2>
<p className="lede">
Search Seerr content directly, then submit a request in one click.
</p>
</div>
</div>
</div>
<form className="portal-discovery-form" onSubmit={runDiscoverySearch}>
<input
value={discoverQuery}
onChange={(event) => setDiscoverQuery(event.target.value)}
placeholder="Search movies or TV shows"
/>
<button type="submit" disabled={discoverLoading}>
{discoverLoading ? 'Searching…' : 'Search'}
</button>
</form>
{discoverError && <div className="error-banner">{discoverError}</div>}
<div className="portal-discovery-results">
{discoverLoading ? (
<div className="status-banner">Searching Seerr</div>
) : discoverResults.length === 0 ? (
<div className="status-banner">No discovery results yet.</div>
) : (
discoverResults.map((item, index) => {
const key = `${item.type ?? 'unknown'}:${item.tmdbId ?? index}`
const requesting = Boolean(requestingTmdbIds[key])
const poster = resolveTmdbArtworkUrl(item.posterPath, 'w185')
const hasRequest = typeof item.requestId === 'number' && item.requestId > 0
return (
<div key={key} className="portal-discovery-item">
<div className="portal-discovery-media">
{poster ? <img src={poster} alt="" loading="lazy" /> : <div className="poster-fallback">No artwork</div>}
</div>
<div className="portal-discovery-main">
<div className="portal-discovery-title-row">
<strong>{item.title || 'Untitled'}</strong>
<span className="small-pill">{item.type ?? 'unknown'}</span>
{item.year ? <span className="small-pill is-muted">{item.year}</span> : null}
<form className="portal-discovery-form" onSubmit={runDiscoverySearch}>
<input
value={discoverQuery}
onChange={(event) => setDiscoverQuery(event.target.value)}
placeholder="Search movies or TV shows"
/>
<button type="submit" disabled={discoverLoading}>
{discoverLoading ? 'Searching…' : 'Search'}
</button>
</form>
{discoverError && <div className="error-banner">{discoverError}</div>}
<div className="portal-discovery-results">
{discoverLoading ? (
<div className="status-banner">Searching Seerr</div>
) : discoverResults.length === 0 ? (
<div className="status-banner">No discovery results yet.</div>
) : (
discoverResults.map((item, index) => {
const key = `${item.type ?? 'unknown'}:${item.tmdbId ?? index}`
const requesting = Boolean(requestingTmdbIds[key])
const poster = resolveTmdbArtworkUrl(item.posterPath, 'w185')
const hasRequest = typeof item.requestId === 'number' && item.requestId > 0
return (
<div key={key} className="portal-discovery-item">
<div className="portal-discovery-media">
{poster ? <img src={poster} alt="" loading="lazy" /> : <div className="poster-fallback">No artwork</div>}
</div>
<p>
<div className="portal-discovery-main">
<div className="portal-discovery-title-row">
<strong>{item.title || 'Untitled'}</strong>
<span className="small-pill">{item.type ?? 'unknown'}</span>
{item.year ? <span className="small-pill is-muted">{item.year}</span> : null}
</div>
<p>
{hasRequest ? (
<>
Already requested
{item.statusLabel ? ` · ${item.statusLabel}` : ''}
{item.requestId ? ` · #${item.requestId}` : ''}
</>
) : (
'Not requested yet'
)}
</p>
</div>
<div className="portal-discovery-actions">
{hasRequest ? (
<>
Already requested
{item.statusLabel ? ` · ${item.statusLabel}` : ''}
{item.requestId ? ` · #${item.requestId}` : ''}
</>
<button
type="button"
onClick={() => router.push(`/requests/${item.requestId}`)}
>
Open request
</button>
) : (
'Not requested yet'
<button
type="button"
onClick={() => void requestDiscoveryItem(item)}
disabled={requesting || !item.tmdbId || !item.type}
>
{requesting ? 'Requesting…' : 'Request'}
</button>
)}
</p>
</div>
</div>
<div className="portal-discovery-actions">
{hasRequest ? (
<button
type="button"
onClick={() => router.push(`/requests/${item.requestId}`)}
>
Open request
</button>
) : (
<button
type="button"
onClick={() => void requestDiscoveryItem(item)}
disabled={requesting || !item.tmdbId || !item.type}
>
{requesting ? 'Requesting…' : 'Request'}
</button>
)}
</div>
</div>
)
})
)}
</div>
</section>
)
})
)}
</div>
</section>
) : (
<section className="admin-panel">
<div className="status-banner">
Issue workspace is for reporting problems and tracking resolution separately from content requests.
</div>
</section>
)}
<section className="portal-overview-grid">
<div className="portal-overview-card">
<span>Total items</span>
<strong>{Number(overview?.overview?.total_items ?? totalItems ?? 0)}</strong>
<span>Total {workspace === 'request' ? 'requests' : 'issues'}</span>
<strong>{visibleKindCount}</strong>
</div>
<div className="portal-overview-card">
<span>Total comments</span>
@@ -756,26 +810,13 @@ export default function PortalPage() {
</section>
<section className="admin-panel portal-create-panel">
<h2>Create item</h2>
<h2>{workspace === 'request' ? 'Create request item' : 'Create issue item'}</h2>
<p className="lede">
Use <strong>Request</strong> for new content, <strong>Issue</strong> for broken behavior, and <strong>Feature</strong> for improvements.
{workspace === 'request'
? 'Create and track request-related notes in a dedicated request workflow.'
: 'Create and track operational issues in a dedicated issue workflow.'}
</p>
<form onSubmit={createItem} className="admin-form compact-form portal-form-grid">
<label>
<span>Type</span>
<select
value={createKind}
onChange={(event) =>
setCreateKind(event.target.value as 'request' | 'issue' | 'feature')
}
>
{KIND_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label>
<span>Priority</span>
<select
@@ -797,7 +838,11 @@ export default function PortalPage() {
required
value={createTitle}
onChange={(event) => setCreateTitle(event.target.value)}
placeholder="Short summary of the request or issue"
placeholder={
workspace === 'request'
? 'Short summary of the request item'
: 'Short summary of the issue'
}
/>
</label>
<label className="portal-field-span-2">
@@ -807,28 +852,36 @@ export default function PortalPage() {
rows={4}
value={createDescription}
onChange={(event) => setCreateDescription(event.target.value)}
placeholder="Add details, expected behavior, and any context."
/>
</label>
<label>
<span>Media type</span>
<select value={createMediaType} onChange={(event) => setCreateMediaType(event.target.value)}>
{MEDIA_TYPE_OPTIONS.map((option) => (
<option key={option.value || 'none'} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label>
<span>Year</span>
<input
value={createYear}
onChange={(event) => setCreateYear(event.target.value)}
inputMode="numeric"
placeholder="Optional"
placeholder={
workspace === 'request'
? 'Add request context, expected media, and notes.'
: 'Describe the issue, impact, and expected behavior.'
}
/>
</label>
{workspace === 'request' && (
<>
<label>
<span>Media type</span>
<select value={createMediaType} onChange={(event) => setCreateMediaType(event.target.value)}>
{MEDIA_TYPE_OPTIONS.map((option) => (
<option key={option.value || 'none'} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label>
<span>Year</span>
<input
value={createYear}
onChange={(event) => setCreateYear(event.target.value)}
inputMode="numeric"
placeholder="Optional"
/>
</label>
</>
)}
<label className="portal-field-span-2">
<span>External reference</span>
<input
@@ -839,29 +892,22 @@ export default function PortalPage() {
</label>
<div className="admin-inline-actions portal-field-span-2">
<button type="submit" disabled={creating}>
{creating ? 'Creating…' : 'Create portal item'}
{creating
? 'Creating…'
: workspace === 'request'
? 'Create request item'
: 'Create issue item'}
</button>
</div>
</form>
</section>
<section className="portal-toolbar">
<label>
<span>Type</span>
<select value={filterKind} onChange={(event) => setFilterKind(event.target.value)}>
<option value="">All</option>
{KIND_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label>
<span>Status</span>
<select value={filterStatus} onChange={(event) => setFilterStatus(event.target.value)}>
<option value="">All</option>
{STATUS_OPTIONS.map((option) => (
{(workspace === 'request' ? REQUEST_FILTER_STATUS_OPTIONS : ISSUE_FILTER_STATUS_OPTIONS).map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
@@ -873,7 +919,11 @@ export default function PortalPage() {
<input
value={filterSearch}
onChange={(event) => setFilterSearch(event.target.value)}
placeholder="Title, description, or item id"
placeholder={
workspace === 'request'
? 'Search request items by title, description, or id'
: 'Search issue items by title, description, or id'
}
/>
</label>
<label className="inline-checkbox portal-mine-toggle">
@@ -890,15 +940,17 @@ export default function PortalPage() {
<section className="admin-panel portal-list-panel">
<div className="user-directory-panel-header">
<div>
<h2>Items</h2>
<h2>{workspace === 'request' ? 'Requests' : 'Issues'}</h2>
<p className="lede">
{totalItems} total
{totalItems} total {workspaceLabelPlural}
{hasMore ? ' (showing first 60)' : ''}
</p>
</div>
</div>
{items.length === 0 ? (
<div className="status-banner">No portal items match this filter.</div>
<div className="status-banner">
No {workspaceLabelPlural} match this filter.
</div>
) : (
<div className="portal-item-list">
{items.map((item) => (
@@ -935,16 +987,22 @@ export default function PortalPage() {
<section className="admin-panel portal-detail-panel">
{!selectedItemId ? (
<div className="status-banner">Select an item to view details.</div>
<div className="status-banner">
Select a {workspaceLabel} to view details.
</div>
) : loadingItem ? (
<div className="status-banner">Loading details</div>
) : !selectedItem ? (
<div className="status-banner">Item not found.</div>
<div className="status-banner">
{workspace === 'request' ? 'Request' : 'Issue'} not found.
</div>
) : (
<>
<div className="user-directory-panel-header">
<div>
<h2>Item #{selectedItem.id}</h2>
<h2>
{selectedItem.kind === 'request' ? 'Request' : 'Issue'} #{selectedItem.id}
</h2>
<p className="lede">
Created by {selectedItem.created_by_username} on {formatDate(selectedItem.created_at)}
</p>
@@ -979,29 +1037,33 @@ export default function PortalPage() {
disabled={!selectedItem.permissions?.can_edit}
/>
</label>
<label>
<span>Media type</span>
<select
value={editMediaType}
onChange={(event) => setEditMediaType(event.target.value)}
disabled={!selectedItem.permissions?.can_edit}
>
{MEDIA_TYPE_OPTIONS.map((option) => (
<option key={option.value || 'none'} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label>
<span>Year</span>
<input
value={editYear}
onChange={(event) => setEditYear(event.target.value)}
inputMode="numeric"
disabled={!selectedItem.permissions?.can_edit}
/>
</label>
{selectedItem.kind === 'request' ? (
<>
<label>
<span>Media type</span>
<select
value={editMediaType}
onChange={(event) => setEditMediaType(event.target.value)}
disabled={!selectedItem.permissions?.can_edit}
>
{MEDIA_TYPE_OPTIONS.map((option) => (
<option key={option.value || 'none'} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<label>
<span>Year</span>
<input
value={editYear}
onChange={(event) => setEditYear(event.target.value)}
inputMode="numeric"
disabled={!selectedItem.permissions?.can_edit}
/>
</label>
</>
) : null}
<label className="portal-field-span-2">
<span>External reference</span>
<input
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "magent-frontend",
"version": "0803262216",
"version": "0803262229",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "magent-frontend",
"version": "0803262216",
"version": "0803262229",
"dependencies": {
"next": "16.1.6",
"react": "19.2.4",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "magent-frontend",
"private": true,
"version": "0803262216",
"version": "0803262229",
"scripts": {
"dev": "next dev",
"build": "next build",