Replace native selects with NovaSelect

This commit is contained in:
2026-05-01 07:45:37 +02:00
parent 67be537c86
commit 35011001ba
55 changed files with 3136 additions and 1662 deletions

View File

@@ -1,6 +1,8 @@
import React from 'react'
import { Head, usePage } from '@inertiajs/react'
import CollectionCard from '../../components/profile/collections/CollectionCard'
import Checkbox from '../../components/ui/Checkbox'
import NovaSelect from '../../components/ui/NovaSelect'
const DEFAULT_SEARCH_FILTERS = {
q: '',
@@ -262,13 +264,7 @@ function BulkActionsPanel({
<div className="mt-4 grid gap-4 lg:grid-cols-4">
<SearchField label="Action">
<select value={form.action} onChange={(event) => onFormChange('action', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="archive">Archive</option>
<option value="assign_campaign">Assign campaign</option>
<option value="update_lifecycle">Update lifecycle</option>
<option value="request_ai_review">Request AI review</option>
<option value="mark_editorial_review">Mark editorial review</option>
</select>
<NovaSelect value={form.action} onChange={(val) => onFormChange('action', val)} searchable={false} options={[{ value: 'archive', label: 'Archive' }, { value: 'assign_campaign', label: 'Assign campaign' }, { value: 'update_lifecycle', label: 'Update lifecycle' }, { value: 'request_ai_review', label: 'Request AI review' }, { value: 'mark_editorial_review', label: 'Mark editorial review' }]} />
</SearchField>
{form.action === 'assign_campaign' ? (
@@ -284,11 +280,7 @@ function BulkActionsPanel({
{form.action === 'update_lifecycle' ? (
<SearchField label="Lifecycle state">
<select value={form.lifecycle_state} onChange={(event) => onFormChange('lifecycle_state', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="draft">Draft</option>
<option value="published">Published</option>
<option value="archived">Archived</option>
</select>
<NovaSelect value={form.lifecycle_state} onChange={(val) => onFormChange('lifecycle_state', val)} searchable={false} options={[{ value: 'draft', label: 'Draft' }, { value: 'published', label: 'Published' }, { value: 'archived', label: 'Archived' }]} />
</SearchField>
) : null}
@@ -344,15 +336,13 @@ function SearchResults({ state, endpoints, selectedIds, onToggleSelected }) {
{state.collections.map((collection) => (
<div key={collection.id} className="space-y-3 rounded-[28px] border border-white/10 bg-[#0d1726] p-5">
<div className="flex items-center justify-between gap-3">
<label className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">
<input
type="checkbox"
<div className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">
<Checkbox
checked={selectedIds.includes(collection.id)}
onChange={() => onToggleSelected(collection.id)}
className="h-4 w-4 rounded border-white/20 bg-[#09111d] text-sky-400 focus:ring-sky-300/30"
label="Select"
/>
Select
</label>
</div>
</div>
<CollectionCard collection={collection} isOwner />
<div className="flex flex-wrap gap-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-400">
@@ -584,56 +574,27 @@ export default function CollectionDashboard() {
</SearchField>
<SearchField label="Type">
<select value={searchFilters.type} onChange={(event) => updateFilter('type', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any type</option>
{(Array.isArray(filterOptions.types) ? filterOptions.types : []).map((option) => (
<option key={option} value={option}>{titleize(option)}</option>
))}
</select>
<NovaSelect value={searchFilters.type} onChange={(val) => updateFilter('type', val)} placeholder="Any type" options={(Array.isArray(filterOptions.types) ? filterOptions.types : []).map((o) => ({ value: o, label: titleize(o) }))} />
</SearchField>
<SearchField label="Visibility">
<select value={searchFilters.visibility} onChange={(event) => updateFilter('visibility', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any visibility</option>
{(Array.isArray(filterOptions.visibilities) ? filterOptions.visibilities : []).map((option) => (
<option key={option} value={option}>{titleize(option)}</option>
))}
</select>
<NovaSelect value={searchFilters.visibility} onChange={(val) => updateFilter('visibility', val)} placeholder="Any visibility" options={(Array.isArray(filterOptions.visibilities) ? filterOptions.visibilities : []).map((o) => ({ value: o, label: titleize(o) }))} />
</SearchField>
<SearchField label="Lifecycle">
<select value={searchFilters.lifecycle_state} onChange={(event) => updateFilter('lifecycle_state', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any lifecycle</option>
{(Array.isArray(filterOptions.lifecycleStates) ? filterOptions.lifecycleStates : []).map((option) => (
<option key={option} value={option}>{titleize(option)}</option>
))}
</select>
<NovaSelect value={searchFilters.lifecycle_state} onChange={(val) => updateFilter('lifecycle_state', val)} placeholder="Any lifecycle" options={(Array.isArray(filterOptions.lifecycleStates) ? filterOptions.lifecycleStates : []).map((o) => ({ value: o, label: titleize(o) }))} />
</SearchField>
<SearchField label="Workflow">
<select value={searchFilters.workflow_state} onChange={(event) => updateFilter('workflow_state', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any workflow</option>
{(Array.isArray(filterOptions.workflowStates) ? filterOptions.workflowStates : []).map((option) => (
<option key={option} value={option}>{titleize(option)}</option>
))}
</select>
<NovaSelect value={searchFilters.workflow_state} onChange={(val) => updateFilter('workflow_state', val)} placeholder="Any workflow" options={(Array.isArray(filterOptions.workflowStates) ? filterOptions.workflowStates : []).map((o) => ({ value: o, label: titleize(o) }))} />
</SearchField>
<SearchField label="Health">
<select value={searchFilters.health_state} onChange={(event) => updateFilter('health_state', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any health state</option>
{(Array.isArray(filterOptions.healthStates) ? filterOptions.healthStates : []).map((option) => (
<option key={option} value={option}>{titleize(option)}</option>
))}
</select>
<NovaSelect value={searchFilters.health_state} onChange={(val) => updateFilter('health_state', val)} placeholder="Any health state" options={(Array.isArray(filterOptions.healthStates) ? filterOptions.healthStates : []).map((o) => ({ value: o, label: titleize(o) }))} />
</SearchField>
<SearchField label="Placement">
<select value={searchFilters.placement_eligibility} onChange={(event) => updateFilter('placement_eligibility', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#09111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="">Any placement state</option>
<option value="1">Eligible</option>
<option value="0">Blocked</option>
</select>
<NovaSelect value={searchFilters.placement_eligibility} onChange={(val) => updateFilter('placement_eligibility', val)} placeholder="Any placement state" searchable={false} options={[{ value: '1', label: 'Eligible' }, { value: '0', label: 'Blocked' }]} />
</SearchField>
<div className="flex items-end gap-3 xl:col-span-1">

View File

@@ -1,7 +1,8 @@
import React from 'react'
import { usePage } from '@inertiajs/react'
import { router, usePage } from '@inertiajs/react'
import CollectionCard from '../../components/profile/collections/CollectionCard'
import SeoHead from '../../components/seo/SeoHead'
import NovaSelect from '../../components/ui/NovaSelect'
const SEARCH_SELECT_OPTIONS = {
type: [
@@ -174,6 +175,33 @@ function SearchPanel({ search }) {
const options = search.options || {}
const chips = activeSearchChips(filters)
const [localFilters, setLocalFilters] = React.useState({
q: filters.q || '',
type: filters.type || '',
sort: filters.sort || 'trending',
category: filters.category || '',
mode: filters.mode || '',
style: filters.style || '',
lifecycle_state: filters.lifecycle_state || '',
theme: filters.theme || '',
health_state: filters.health_state || '',
color: filters.color || '',
campaign_key: filters.campaign_key || '',
program_key: filters.program_key || '',
quality_tier: filters.quality_tier || '',
})
function updateFilter(key, val) {
setLocalFilters((curr) => ({ ...curr, [key]: val }))
}
function handleSubmit(event) {
event.preventDefault()
const params = {}
Object.entries(localFilters).forEach(([k, v]) => { if (v) params[k] = v })
router.get('/collections/search', params)
}
return (
<section className="mt-8 rounded-[32px] border border-white/10 bg-white/[0.04] p-6 backdrop-blur-sm md:p-7">
<div className="flex flex-wrap items-center justify-between gap-3">
@@ -183,75 +211,20 @@ function SearchPanel({ search }) {
</div>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-semibold text-slate-300">{search?.meta?.total ?? 0} results</span>
</div>
<form method="GET" action="/collections/search" className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<input name="q" defaultValue={filters.q || ''} placeholder="Search title or summary" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35 xl:col-span-2" />
<select name="type" defaultValue={filters.type || ''} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35">
<option value="">All types</option>
<option value="personal">Personal</option>
<option value="community">Community</option>
<option value="editorial">Editorial</option>
</select>
<select name="sort" defaultValue={filters.sort || 'trending'} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35">
<option value="trending">Trending</option>
<option value="recent">Recent</option>
<option value="quality">Quality</option>
<option value="evergreen">Evergreen</option>
</select>
<select name="category" defaultValue={filters.category || ''} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35">
<option value="">Any category</option>
{(options.category || []).map((item) => (
<option key={`category-${item.value}`} value={item.value}>{item.label}</option>
))}
</select>
<select name="mode" defaultValue={filters.mode || ''} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35">
<option value="">Any curation mode</option>
<option value="manual">Manual</option>
<option value="smart">Smart</option>
</select>
<select name="style" defaultValue={filters.style || ''} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35">
<option value="">Any style signal</option>
{(options.style || []).map((item) => (
<option key={`style-${item.value}`} value={item.value}>{item.label}</option>
))}
</select>
<select name="lifecycle_state" defaultValue={filters.lifecycle_state || ''} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35">
<option value="">Any lifecycle</option>
<option value="published">Published</option>
<option value="featured">Featured</option>
<option value="archived">Archived</option>
</select>
<select name="theme" defaultValue={filters.theme || ''} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35">
<option value="">Any theme</option>
{(options.theme || []).map((item) => (
<option key={`theme-${item.value}`} value={item.value}>{item.label}</option>
))}
</select>
<select name="health_state" defaultValue={filters.health_state || ''} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35">
<option value="">Any quality state</option>
<option value="healthy">Healthy</option>
<option value="needs_metadata">Needs metadata</option>
<option value="stale">Stale</option>
<option value="low_content">Low content</option>
<option value="broken_items">Broken items</option>
<option value="weak_cover">Weak cover</option>
<option value="low_engagement">Low engagement</option>
<option value="duplicate_risk">Duplicate risk</option>
<option value="merge_candidate">Merge candidate</option>
</select>
<select name="color" defaultValue={filters.color || ''} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35">
<option value="">Any color palette</option>
{(options.color || []).map((item) => (
<option key={`color-${item.value}`} value={item.value}>{item.label}</option>
))}
</select>
<input name="campaign_key" defaultValue={filters.campaign_key || ''} placeholder="Campaign key" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35" />
<input name="program_key" defaultValue={filters.program_key || ''} placeholder="Program key" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35" />
<select name="quality_tier" defaultValue={filters.quality_tier || ''} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35">
<option value="">Any quality tier</option>
{(options.quality_tier || []).map((item) => (
<option key={`quality-tier-${item.value}`} value={item.value}>{item.label}</option>
))}
</select>
<form onSubmit={handleSubmit} className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<input value={localFilters.q} onChange={(e) => updateFilter('q', e.target.value)} placeholder="Search title or summary" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35 xl:col-span-2" />
<NovaSelect value={localFilters.type} onChange={(val) => updateFilter('type', val)} placeholder="All types" searchable={false} options={[{ value: 'personal', label: 'Personal' }, { value: 'community', label: 'Community' }, { value: 'editorial', label: 'Editorial' }]} />
<NovaSelect value={localFilters.sort} onChange={(val) => updateFilter('sort', val)} searchable={false} options={[{ value: 'trending', label: 'Trending' }, { value: 'recent', label: 'Recent' }, { value: 'quality', label: 'Quality' }, { value: 'evergreen', label: 'Evergreen' }]} />
<NovaSelect value={localFilters.category} onChange={(val) => updateFilter('category', val)} placeholder="Any category" options={(options.category || []).map((item) => ({ value: item.value, label: item.label }))} />
<NovaSelect value={localFilters.mode} onChange={(val) => updateFilter('mode', val)} placeholder="Any curation mode" searchable={false} options={[{ value: 'manual', label: 'Manual' }, { value: 'smart', label: 'Smart' }]} />
<NovaSelect value={localFilters.style} onChange={(val) => updateFilter('style', val)} placeholder="Any style signal" options={(options.style || []).map((item) => ({ value: item.value, label: item.label }))} />
<NovaSelect value={localFilters.lifecycle_state} onChange={(val) => updateFilter('lifecycle_state', val)} placeholder="Any lifecycle" searchable={false} options={[{ value: 'published', label: 'Published' }, { value: 'featured', label: 'Featured' }, { value: 'archived', label: 'Archived' }]} />
<NovaSelect value={localFilters.theme} onChange={(val) => updateFilter('theme', val)} placeholder="Any theme" options={(options.theme || []).map((item) => ({ value: item.value, label: item.label }))} />
<NovaSelect value={localFilters.health_state} onChange={(val) => updateFilter('health_state', val)} placeholder="Any quality state" searchable={false} options={[{ value: 'healthy', label: 'Healthy' }, { value: 'needs_metadata', label: 'Needs metadata' }, { value: 'stale', label: 'Stale' }, { value: 'low_content', label: 'Low content' }, { value: 'broken_items', label: 'Broken items' }, { value: 'weak_cover', label: 'Weak cover' }, { value: 'low_engagement', label: 'Low engagement' }, { value: 'duplicate_risk', label: 'Duplicate risk' }, { value: 'merge_candidate', label: 'Merge candidate' }]} />
<NovaSelect value={localFilters.color} onChange={(val) => updateFilter('color', val)} placeholder="Any color palette" options={(options.color || []).map((item) => ({ value: item.value, label: item.label }))} />
<input value={localFilters.campaign_key} onChange={(e) => updateFilter('campaign_key', e.target.value)} placeholder="Campaign key" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35" />
<input value={localFilters.program_key} onChange={(e) => updateFilter('program_key', e.target.value)} placeholder="Program key" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35" />
<NovaSelect value={localFilters.quality_tier} onChange={(val) => updateFilter('quality_tier', val)} placeholder="Any quality tier" options={(options.quality_tier || []).map((item) => ({ value: item.value, label: item.label }))} />
<div className="md:col-span-2 xl:col-span-4 flex flex-wrap gap-3">
<button type="submit" className="inline-flex items-center gap-2 rounded-2xl border border-sky-300/20 bg-sky-400/10 px-5 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15"><i className="fa-solid fa-magnifying-glass fa-fw" />Apply filters</button>
<a href="/collections/search" className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-5 py-3 text-sm font-semibold text-white transition hover:bg-white/[0.07]"><i className="fa-solid fa-rotate-left fa-fw" />Reset</a>

View File

@@ -2,6 +2,8 @@ import React, { useEffect, useMemo, useState } from 'react'
import { Head, usePage } from '@inertiajs/react'
import CollectionCard from '../../components/profile/collections/CollectionCard'
import CollectionVisibilityBadge from '../../components/profile/collections/CollectionVisibilityBadge'
import Checkbox from '../../components/ui/Checkbox'
import NovaSelect from '../../components/ui/NovaSelect'
function getCsrfToken() {
if (typeof document === 'undefined') return ''
@@ -466,15 +468,19 @@ function MemberCard({ member, onRoleChange, onRemove, onAccept, onDecline, onTra
</div>
<div className="mt-3 flex flex-wrap gap-2">
{member?.can_revoke ? (
<select
value={member?.role}
onChange={(event) => onRoleChange?.(member, event.target.value)}
className="rounded-xl border border-white/10 bg-[#0d1726] px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white"
>
<option value="editor">Editor</option>
<option value="contributor">Contributor</option>
<option value="viewer">Viewer</option>
</select>
<div className="min-w-[150px]">
<NovaSelect
value={member?.role}
onChange={(value) => onRoleChange?.(member, value)}
options={[
{ value: 'editor', label: 'Editor' },
{ value: 'contributor', label: 'Contributor' },
{ value: 'viewer', label: 'Viewer' },
]}
searchable={false}
className="text-xs font-semibold uppercase tracking-[0.14em]"
/>
</div>
) : null}
{member?.can_accept ? <button type="button" onClick={() => onAccept?.(member)} className="rounded-xl border border-emerald-400/20 bg-emerald-400/10 px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-emerald-100">Accept</button> : null}
{member?.can_decline ? <button type="button" onClick={() => onDecline?.(member)} className="rounded-xl border border-white/12 bg-white/[0.05] px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white">Decline</button> : null}
@@ -528,28 +534,27 @@ function LayoutModuleCard({ module, index, total, onToggle, onSlotChange, onMove
<div className="text-sm font-semibold text-white">{module.label}</div>
<p className="mt-1 text-sm leading-relaxed text-slate-400">{module.description}</p>
</div>
<label className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white">
<input
type="checkbox"
<div className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-semibold uppercase tracking-[0.14em] text-white">
<Checkbox
checked={module.enabled}
disabled={module.locked}
onChange={(event) => onToggle(module.key, event.target.checked)}
label={module.locked ? 'Required' : (module.enabled ? 'Enabled' : 'Disabled')}
/>
{module.locked ? 'Required' : (module.enabled ? 'Enabled' : 'Disabled')}
</label>
</div>
</div>
<div className="mt-4 grid gap-3 md:grid-cols-[minmax(0,1fr)_auto]">
<Field label="Placement">
<select
<NovaSelect
value={module.slot}
onChange={(event) => onSlotChange(module.key, event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35"
>
{module.slots.map((slot) => (
<option key={slot} value={slot}>{slot === 'full' ? 'Full width' : slot === 'main' ? 'Main column' : 'Sidebar'}</option>
))}
</select>
onChange={(value) => onSlotChange(module.key, value)}
options={module.slots.map((slot) => ({
value: slot,
label: slot === 'full' ? 'Full width' : slot === 'main' ? 'Main column' : 'Sidebar',
}))}
searchable={false}
/>
</Field>
<div className="flex flex-wrap items-end gap-2">
@@ -646,27 +651,21 @@ function SmartRuleRow({
<div className="mt-4 grid gap-4 md:grid-cols-[1fr_180px_minmax(0,1.15fr)]">
<Field label="Field">
<select
<NovaSelect
value={rule.field}
onChange={(event) => onFieldChange(event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35"
>
{fieldOptions.map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
onChange={(value) => onFieldChange(value)}
options={fieldOptions.map((option) => ({ value: option.value, label: option.label }))}
searchable={false}
/>
</Field>
<Field label="Operator">
<select
<NovaSelect
value={rule.operator}
onChange={(event) => onOperatorChange(event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35"
>
{operatorOptions.map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
onChange={(value) => onOperatorChange(value)}
options={operatorOptions.map((option) => ({ value: option.value, label: option.label }))}
searchable={false}
/>
</Field>
{rule.field === 'created_at' ? (
@@ -688,23 +687,20 @@ function SmartRuleRow({
</Field>
) : rule.field === 'is_featured' || rule.field === 'is_mature' ? (
<Field label="Value">
<select
<NovaSelect
value={rule.value ? 'true' : 'false'}
onChange={(event) => onValueChange(event.target.value === 'true')}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35"
>
{rule.field === 'is_featured' ? (
<>
<option value="true">Featured only</option>
<option value="false">Not featured</option>
</>
) : (
<>
<option value="true">Mature only</option>
<option value="false">Not mature</option>
</>
)}
</select>
onChange={(value) => onValueChange(value === 'true')}
options={rule.field === 'is_featured'
? [
{ value: 'true', label: 'Featured only' },
{ value: 'false', label: 'Not featured' },
]
: [
{ value: 'true', label: 'Mature only' },
{ value: 'false', label: 'Not mature' },
]}
searchable={false}
/>
</Field>
) : rule.field === 'tags' ? (
<Field label="Value" help="Type a tag name exactly as it appears on your artworks.">
@@ -718,16 +714,13 @@ function SmartRuleRow({
</Field>
) : valueOptions.length ? (
<Field label="Value">
<select
<NovaSelect
value={rule.value}
onChange={(event) => onValueChange(event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35"
>
<option value="">Select one</option>
{valueOptions.map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
onChange={(value) => onValueChange(value)}
options={valueOptions.map((option) => ({ value: option.value, label: option.label }))}
placeholder="Select one"
searchable={false}
/>
</Field>
) : (
<Field label="Value">
@@ -1917,11 +1910,7 @@ export default function CollectionManage() {
/>
</Field>
<Field label="Visibility">
<select value={form.visibility} onChange={(event) => updateForm('visibility', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="public">Public visible to everyone</option>
<option value="unlisted">Unlisted accessible by link only</option>
<option value="private">Private only you can see it</option>
</select>
<NovaSelect value={form.visibility} onChange={(val) => updateForm('visibility', val)} searchable={false} options={[{ value: 'public', label: 'Public — visible to everyone' }, { value: 'unlisted', label: 'Unlisted — accessible by link only' }, { value: 'private', label: 'Private — only you can see it' }]} />
</Field>
</div>
@@ -1965,12 +1954,7 @@ export default function CollectionManage() {
/>
</Field>
<Field label="Presentation Style">
<select value={form.presentation_style} onChange={(event) => updateForm('presentation_style', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="standard">Standard</option>
<option value="editorial_grid">Editorial Grid</option>
<option value="hero_grid">Hero Grid</option>
<option value="masonry">Masonry</option>
</select>
<NovaSelect value={form.presentation_style} onChange={(val) => updateForm('presentation_style', val)} searchable={false} options={[{ value: 'standard', label: 'Standard' }, { value: 'editorial_grid', label: 'Editorial Grid' }, { value: 'hero_grid', label: 'Hero Grid' }, { value: 'masonry', label: 'Masonry' }]} />
</Field>
</div>
@@ -1986,73 +1970,49 @@ export default function CollectionManage() {
<div className="grid gap-5 md:grid-cols-2">
<Field label="Emphasis Mode">
<select value={form.emphasis_mode} onChange={(event) => updateForm('emphasis_mode', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="cover_heavy">Cover Heavy</option>
<option value="balanced">Balanced</option>
<option value="artwork_first">Artwork First</option>
</select>
<NovaSelect value={form.emphasis_mode} onChange={(val) => updateForm('emphasis_mode', val)} searchable={false} options={[{ value: 'cover_heavy', label: 'Cover Heavy' }, { value: 'balanced', label: 'Balanced' }, { value: 'artwork_first', label: 'Artwork First' }]} />
</Field>
<Field label="Theme">
<select value={form.theme_token} onChange={(event) => updateForm('theme_token', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="default">Default</option>
<option value="subtle-blue">Subtle Blue</option>
<option value="violet">Violet</option>
<option value="amber">Amber</option>
</select>
<NovaSelect value={form.theme_token} onChange={(val) => updateForm('theme_token', val)} searchable={false} options={[{ value: 'default', label: 'Default' }, { value: 'subtle-blue', label: 'Subtle Blue' }, { value: 'violet', label: 'Violet' }, { value: 'amber', label: 'Amber' }]} />
</Field>
</div>
<div className="grid gap-5 md:grid-cols-2">
{!isSmartMode ? (
<Field label="Sort Order" help="Manual keeps the display order under your direct control.">
<select value={form.sort_mode} onChange={(event) => updateForm('sort_mode', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="manual">Manual</option>
<option value="newest">Newest first</option>
<option value="oldest">Oldest first</option>
<option value="popular">Most popular</option>
</select>
<NovaSelect value={form.sort_mode} onChange={(val) => updateForm('sort_mode', val)} searchable={false} options={[{ value: 'manual', label: 'Manual' }, { value: 'newest', label: 'Newest first' }, { value: 'oldest', label: 'Oldest first' }, { value: 'popular', label: 'Most popular' }]} />
</Field>
) : (
<Field label="Match Mode" help="All rules must match, or any one rule is enough.">
<select
<NovaSelect
value={smartRules.match}
onChange={(event) => setSmartRules((current) => ({ ...current, match: event.target.value }))}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35"
>
<option value="all">All rules</option>
<option value="any">Any rule</option>
</select>
onChange={(val) => setSmartRules((current) => ({ ...current, match: val }))}
searchable={false}
options={[{ value: 'all', label: 'All rules' }, { value: 'any', label: 'Any rule' }]}
/>
</Field>
)}
{!isSmartMode ? (
<Field label="Cover Artwork" help={attachedCoverOptions.length ? 'Choose a cover from artworks already attached to this collection.' : 'Attach artworks first to pick a manual cover.'}>
<select
value={form.cover_artwork_id}
onChange={(event) => updateForm('cover_artwork_id', event.target.value)}
<NovaSelect
value={String(form.cover_artwork_id || '')}
onChange={(val) => updateForm('cover_artwork_id', val)}
disabled={!attachedCoverOptions.length}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 disabled:cursor-not-allowed disabled:text-slate-500"
>
<option value="">Automatic cover</option>
{attachedCoverOptions.map((artwork) => (
<option key={artwork.id} value={artwork.id}>{artwork.title}</option>
))}
</select>
placeholder="Automatic cover"
options={attachedCoverOptions.map((a) => ({ value: String(a.id), label: a.title }))}
/>
</Field>
) : (
<Field label="Smart Sort" help="How matching artworks should be ordered in this collection.">
<select
<NovaSelect
value={smartRules.sort}
onChange={(event) => {
setSmartRules((current) => ({ ...current, sort: event.target.value }))
updateForm('sort_mode', event.target.value)
onChange={(val) => {
setSmartRules((current) => ({ ...current, sort: val }))
updateForm('sort_mode', val)
}}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35"
>
{(smartRuleOptions?.sort_options || []).map((option) => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
options={(smartRuleOptions?.sort_options || [])}
/>
</Field>
)}
</div>
@@ -2061,48 +2021,32 @@ export default function CollectionManage() {
<AdvancedSection title="Collaboration & Access" icon="fa-user-group" defaultOpen={mode === 'edit'}>
<div className="grid gap-5 md:grid-cols-2">
<Field label="Collection Type">
<select value={form.type} onChange={(event) => updateForm('type', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="personal">Personal</option>
<option value="community">Community</option>
<option value="editorial">Editorial</option>
</select>
<NovaSelect value={form.type} onChange={(val) => updateForm('type', val)} searchable={false} options={[{ value: 'personal', label: 'Personal' }, { value: 'community', label: 'Community' }, { value: 'editorial', label: 'Editorial' }]} />
</Field>
<Field label="Collaboration Mode">
<select value={form.collaboration_mode} onChange={(event) => updateForm('collaboration_mode', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="closed">Closed curated by you only</option>
<option value="invite_only">Invite only</option>
<option value="open">Open submissions</option>
</select>
<NovaSelect value={form.collaboration_mode} onChange={(val) => updateForm('collaboration_mode', val)} searchable={false} options={[{ value: 'closed', label: 'Closed — curated by you only' }, { value: 'invite_only', label: 'Invite only' }, { value: 'open', label: 'Open submissions' }]} />
</Field>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.allow_submissions} onChange={(event) => updateForm('allow_submissions', event.target.checked)} />
Allow submissions
</label>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.allow_comments} onChange={(event) => updateForm('allow_comments', event.target.checked)} />
Allow comments
</label>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.allow_saves} onChange={(event) => updateForm('allow_saves', event.target.checked)} />
Allow saves
</label>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.commercial_eligibility} onChange={(event) => updateForm('commercial_eligibility', event.target.checked)} />
Commercially eligible
</label>
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<Checkbox checked={form.allow_submissions} onChange={(event) => updateForm('allow_submissions', event.target.checked)} label="Allow submissions" />
</div>
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<Checkbox checked={form.allow_comments} onChange={(event) => updateForm('allow_comments', event.target.checked)} label="Allow comments" />
</div>
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<Checkbox checked={form.allow_saves} onChange={(event) => updateForm('allow_saves', event.target.checked)} label="Allow saves" />
</div>
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<Checkbox checked={form.commercial_eligibility} onChange={(event) => updateForm('commercial_eligibility', event.target.checked)} label="Commercially eligible" />
</div>
</div>
{form.type === 'editorial' ? (
<div className="grid gap-5 md:grid-cols-3">
<Field label="Editorial Owner" help="Choose whether this editorial lives under the current curator, another staff account, or the system identity.">
<select value={form.editorial_owner_mode} onChange={(event) => updateForm('editorial_owner_mode', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="creator">Current curator</option>
<option value="staff_account">Staff account</option>
<option value="system">System editorial identity</option>
</select>
<NovaSelect value={form.editorial_owner_mode} onChange={(val) => updateForm('editorial_owner_mode', val)} searchable={false} options={[{ value: 'creator', label: 'Current curator' }, { value: 'staff_account', label: 'Staff account' }, { value: 'system', label: 'System editorial identity' }]} />
</Field>
{form.editorial_owner_mode === 'staff_account' ? (
<Field label="Staff Account Username" help="Must be an admin or moderator username.">
@@ -2156,13 +2100,7 @@ export default function CollectionManage() {
<input type="text" value={form.campaign_label} onChange={(event) => updateForm('campaign_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={120} />
</Field>
<Field label="Spotlight Style" help="Controls the visual frame for the public campaign banner.">
<select value={form.spotlight_style} onChange={(event) => updateForm('spotlight_style', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="default">Default</option>
<option value="editorial">Editorial</option>
<option value="seasonal">Seasonal</option>
<option value="challenge">Challenge</option>
<option value="community">Community</option>
</select>
<NovaSelect value={form.spotlight_style} onChange={(val) => updateForm('spotlight_style', val)} searchable={false} options={[{ value: 'default', label: 'Default' }, { value: 'editorial', label: 'Editorial' }, { value: 'seasonal', label: 'Seasonal' }, { value: 'challenge', label: 'Challenge' }, { value: 'community', label: 'Community' }]} />
</Field>
<Field label="Banner Text" help="Short line shown in the collection spotlight banner.">
<input type="text" value={form.banner_text} onChange={(event) => updateForm('banner_text', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={200} />
@@ -2195,14 +2133,7 @@ export default function CollectionManage() {
<AdvancedSection title="Scheduling & Lifecycle" icon="fa-calendar-days" defaultOpen={mode === 'edit'}>
<Field label="Lifecycle State" help="Draft keeps it hidden. Published makes it live. Archived retires it from active surfaces.">
<select value={form.lifecycle_state} onChange={(event) => updateForm('lifecycle_state', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="draft">Draft</option>
<option value="scheduled">Scheduled</option>
<option value="published">Published</option>
<option value="featured">Featured</option>
<option value="archived">Archived</option>
<option value="expired">Expired</option>
</select>
<NovaSelect value={form.lifecycle_state} onChange={(val) => updateForm('lifecycle_state', val)} searchable={false} options={[{ value: 'draft', label: 'Draft' }, { value: 'scheduled', label: 'Scheduled' }, { value: 'published', label: 'Published' }, { value: 'featured', label: 'Featured' }, { value: 'archived', label: 'Archived' }, { value: 'expired', label: 'Expired' }]} />
</Field>
<div className="grid gap-5 md:grid-cols-2">
@@ -2244,10 +2175,9 @@ export default function CollectionManage() {
<Field label="Brand Safe Status">
<input type="text" value={form.brand_safe_status} onChange={(event) => updateForm('brand_safe_status', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={40} />
</Field>
<label className="flex items-center gap-3 self-end rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<input type="checkbox" checked={form.analytics_enabled} onChange={(event) => updateForm('analytics_enabled', event.target.checked)} />
Analytics enabled
</label>
<div className="flex items-center gap-3 self-end rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<Checkbox checked={form.analytics_enabled} onChange={(event) => updateForm('analytics_enabled', event.target.checked)} label="Analytics enabled" />
</div>
</div>
<Field label="Editorial Notes" help="Internal editorial context for campaign planning, curation rationale, and staff handoff.">
@@ -2800,19 +2730,9 @@ export default function CollectionManage() {
</div>
<form onSubmit={handleInviteMember} className="mt-5 flex flex-col gap-3 xl:flex-row xl:items-start">
<input value={inviteUsername} onChange={(event) => setInviteUsername(event.target.value)} placeholder="username" className="min-w-0 flex-1 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
<select value={inviteRole} onChange={(event) => setInviteRole(event.target.value)} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="editor">Editor</option>
<option value="contributor">Contributor</option>
<option value="viewer">Viewer</option>
</select>
<NovaSelect value={inviteRole} onChange={(val) => setInviteRole(val)} searchable={false} options={[{ value: 'editor', label: 'Editor' }, { value: 'contributor', label: 'Contributor' }, { value: 'viewer', label: 'Viewer' }]} />
<div className="flex min-w-0 flex-1 flex-col gap-2 md:min-w-[240px]">
<select value={inviteExpiryMode} onChange={(event) => setInviteExpiryMode(event.target.value)} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
<option value="default">Default ({inviteExpiryDays} days)</option>
{inviteExpiryOptions.map((days) => (
<option key={days} value={String(days)}>{days} day{days === 1 ? '' : 's'}</option>
))}
<option value="custom">Custom date</option>
</select>
<NovaSelect value={inviteExpiryMode} onChange={(val) => setInviteExpiryMode(val)} searchable={false} options={[{ value: 'default', label: `Default (${inviteExpiryDays} days)` }, ...inviteExpiryOptions.map((days) => ({ value: String(days), label: `${days} day${days === 1 ? '' : 's'}` })), { value: 'custom', label: 'Custom date' }]} />
{inviteExpiryMode === 'custom' ? (
<input type="datetime-local" value={inviteCustomExpiry} onChange={(event) => setInviteCustomExpiry(event.target.value)} className="rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" />
) : (
@@ -3116,16 +3036,12 @@ export default function CollectionManage() {
<div className="mt-6 flex flex-col gap-3 xl:flex-row xl:items-end">
<div className="min-w-0 flex-1">
<label className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Add manageable collection</label>
<select
<NovaSelect
value={selectedLinkedCollectionId}
onChange={(event) => setSelectedLinkedCollectionId(event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35"
>
<option value="">Select a collection</option>
{linkedCollectionOptions.map((item) => (
<option key={item.id} value={item.id}>{item.title}</option>
))}
</select>
onChange={(value) => setSelectedLinkedCollectionId(value)}
options={linkedCollectionOptions.map((item) => ({ value: String(item.id), label: item.title }))}
placeholder="Select a collection"
/>
</div>
<button
type="button"
@@ -3186,37 +3102,33 @@ export default function CollectionManage() {
<div className="mt-6 grid gap-3 xl:grid-cols-[180px_minmax(0,1fr)_240px_auto] xl:items-end">
<div>
<label className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Entity type</label>
<select
<NovaSelect
value={selectedEntityType}
onChange={(event) => {
const nextType = event.target.value
const nextOptions = Array.isArray(entityLinkOptions[nextType]) ? entityLinkOptions[nextType] : []
setSelectedEntityType(nextType)
setSelectedEntityId(nextOptions[0]?.id || '')
onChange={(value) => {
const nextOptions = Array.isArray(entityLinkOptions[value]) ? entityLinkOptions[value] : []
setSelectedEntityType(value)
setSelectedEntityId(String(nextOptions[0]?.id || ''))
}}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35"
>
<option value="creator">Creator</option>
<option value="artwork">Artwork</option>
<option value="story">Story</option>
<option value="category">Category</option>
<option value="campaign">Campaign</option>
<option value="event">Event</option>
<option value="tag">Tag or Theme</option>
</select>
options={[
{ value: 'creator', label: 'Creator' },
{ value: 'artwork', label: 'Artwork' },
{ value: 'story', label: 'Story' },
{ value: 'category', label: 'Category' },
{ value: 'campaign', label: 'Campaign' },
{ value: 'event', label: 'Event' },
{ value: 'tag', label: 'Tag or Theme' },
]}
searchable={false}
/>
</div>
<div className="min-w-0">
<label className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Choose entity</label>
<select
<NovaSelect
value={selectedEntityId}
onChange={(event) => setSelectedEntityId(event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35"
>
<option value="">Select an entity</option>
{(entityLinkOptions[selectedEntityType] || []).map((item) => (
<option key={`${selectedEntityType}-${item.id}`} value={item.id}>{item.label}</option>
))}
</select>
onChange={(value) => setSelectedEntityId(value)}
options={(entityLinkOptions[selectedEntityType] || []).map((item) => ({ value: String(item.id), label: item.label }))}
placeholder="Select an entity"
/>
</div>
<div className="min-w-0">
<label className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">Relationship label</label>
@@ -3319,31 +3231,32 @@ export default function CollectionManage() {
<div className="mt-6 grid gap-5 xl:grid-cols-2">
<div className="rounded-[24px] border border-white/10 bg-white/[0.04] p-5">
<Field label="Moderation Status">
<select
<NovaSelect
value={collectionState?.moderation_status || 'active'}
onChange={(event) => handleModerationStatusChange(event.target.value)}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35"
>
<option value="active">Active</option>
<option value="under_review">Under review</option>
<option value="restricted">Restricted</option>
<option value="hidden">Hidden</option>
</select>
onChange={(value) => handleModerationStatusChange(value)}
options={[
{ value: 'active', label: 'Active' },
{ value: 'under_review', label: 'Under review' },
{ value: 'restricted', label: 'Restricted' },
{ value: 'hidden', label: 'Hidden' },
]}
searchable={false}
/>
</Field>
<div className="mt-4 space-y-3">
<label className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<div className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<span>Allow comments</span>
<input type="checkbox" checked={form.allow_comments} onChange={(event) => handleModerationToggle('allow_comments', event.target.checked)} />
</label>
<label className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<Checkbox checked={form.allow_comments} onChange={(event) => handleModerationToggle('allow_comments', event.target.checked)} />
</div>
<div className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<span>Allow submissions</span>
<input type="checkbox" checked={form.allow_submissions} onChange={(event) => handleModerationToggle('allow_submissions', event.target.checked)} />
</label>
<label className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<Checkbox checked={form.allow_submissions} onChange={(event) => handleModerationToggle('allow_submissions', event.target.checked)} />
</div>
<div className="flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white">
<span>Allow saves</span>
<input type="checkbox" checked={form.allow_saves} onChange={(event) => handleModerationToggle('allow_saves', event.target.checked)} />
</label>
<Checkbox checked={form.allow_saves} onChange={(event) => handleModerationToggle('allow_saves', event.target.checked)} />
</div>
</div>
</div>

View File

@@ -3,6 +3,7 @@ import { usePage } from '@inertiajs/react'
import ArtworkGallery from '../../components/artwork/ArtworkGallery'
import CollectionCard from '../../components/profile/collections/CollectionCard'
import CollectionVisibilityBadge from '../../components/profile/collections/CollectionVisibilityBadge'
import NovaSelect from '../../components/ui/NovaSelect'
import SeoHead from '../../components/seo/SeoHead'
import CommentForm from '../../components/social/CommentForm'
import CommentList from '../../components/social/CommentList'
@@ -985,11 +986,7 @@ export default function CollectionShow() {
<PageSection icon="fa-paper-plane" eyebrow="Submissions" title="Submit to this collection">
{canSubmit && submissionArtworkOptions?.length ? (
<div className="space-y-3">
<select value={selectedArtworkId} onChange={(event) => setSelectedArtworkId(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none transition focus:border-sky-300/35">
{submissionArtworkOptions.map((artwork) => (
<option key={artwork.id} value={artwork.id}>{artwork.title}</option>
))}
</select>
<NovaSelect value={String(selectedArtworkId || '')} onChange={(val) => setSelectedArtworkId(val)} placeholder="Select artwork" options={submissionArtworkOptions.map((a) => ({ value: String(a.id), label: a.title }))} />
<button type="button" onClick={handleSubmitArtwork} className="inline-flex items-center gap-2 rounded-2xl border border-sky-300/25 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15"><i className="fa-solid fa-paper-plane fa-fw" />Submit artwork</button>
</div>
) : (

View File

@@ -2,6 +2,8 @@ import React from 'react'
import { Head, usePage } from '@inertiajs/react'
import CollectionCard from '../../components/profile/collections/CollectionCard'
import ShareToast from '../../components/ui/ShareToast'
import Checkbox from '../../components/ui/Checkbox'
import NovaSelect from '../../components/ui/NovaSelect'
function getCsrfToken() {
if (typeof document === 'undefined') return ''
@@ -618,9 +620,7 @@ export default function CollectionStaffProgramming() {
</div>
<div className="mt-6 grid gap-4 md:grid-cols-2">
<Field label="Collection">
<select value={assignmentForm.collection_id} onChange={(event) => setAssignmentForm((current) => ({ ...current, collection_id: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none">
{collectionOptions.map((option) => <option key={option.id} value={option.id}>{option.title}</option>)}
</select>
<NovaSelect value={String(assignmentForm.collection_id || '')} onChange={(val) => setAssignmentForm((current) => ({ ...current, collection_id: val }))} options={collectionOptions.map((o) => ({ value: String(o.id), label: o.title }))} />
</Field>
<Field label="Program Key" help="Use stable internal names like discover-spring or homepage-hero.">
<input list="program-key-options" value={assignmentForm.program_key} onChange={(event) => setAssignmentForm((current) => ({ ...current, program_key: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" maxLength={80} />
@@ -715,9 +715,7 @@ export default function CollectionStaffProgramming() {
</div>
<div className="mt-6 grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]">
<Field label="Target Collection" help="Leave a selection in place to inspect one collection. Change it any time before running a diagnostic.">
<select value={selectedCollectionId} onChange={(event) => setSelectedCollectionId(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none">
{collectionOptions.map((option) => <option key={option.id} value={option.id}>{option.title}</option>)}
</select>
<NovaSelect value={String(selectedCollectionId || '')} onChange={(val) => setSelectedCollectionId(val)} options={collectionOptions.map((o) => ({ value: String(o.id), label: o.title }))} />
</Field>
<div className="flex items-end gap-3">
<button type="button" onClick={() => runDiagnostic('eligibility')} disabled={busy !== ''} className="inline-flex items-center gap-2 rounded-2xl border border-lime-300/20 bg-lime-400/10 px-4 py-3 text-sm font-semibold text-lime-100 transition hover:bg-lime-400/15 disabled:opacity-60"><i className={`fa-solid ${busy === 'eligibility' ? 'fa-circle-notch fa-spin' : 'fa-shield-check'} fa-fw`} />Eligibility</button>
@@ -824,10 +822,9 @@ export default function CollectionStaffProgramming() {
) : <div className="rounded-[24px] border border-dashed border-white/12 bg-white/[0.03] px-4 py-4 text-sm text-slate-300 md:col-span-2 xl:col-span-3">Partner, sponsorship, ownership, and review metadata remain admin-only. Moderators can still manage experiment and promotion hooks here.</div>}
</div>
<label className="mt-4 flex items-center gap-3 rounded-[20px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-200">
<input type="checkbox" checked={hooksForm.placement_eligibility} onChange={(event) => setHooksForm((current) => ({ ...current, placement_eligibility: event.target.checked }))} className="h-4 w-4 rounded border-white/20 bg-white/[0.04] text-sky-400 focus:ring-sky-300/40" />
Placement eligible override
</label>
<div className="mt-4 flex items-center gap-3 rounded-[20px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-200">
<Checkbox checked={hooksForm.placement_eligibility} onChange={(event) => setHooksForm((current) => ({ ...current, placement_eligibility: event.target.checked }))} label="Placement eligible override" />
</div>
<div className="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
<div className="rounded-[22px] border border-white/10 bg-slate-950/40 px-4 py-3 text-sm text-slate-300">

View File

@@ -1,6 +1,8 @@
import React from 'react'
import { Head, usePage } from '@inertiajs/react'
import CollectionCard from '../../components/profile/collections/CollectionCard'
import Checkbox from '../../components/ui/Checkbox'
import NovaSelect from '../../components/ui/NovaSelect'
function getCsrfToken() {
if (typeof document === 'undefined') return ''
@@ -401,13 +403,13 @@ export default function CollectionStaffSurfaces() {
<div className="mt-6 grid gap-4 md:grid-cols-2">
<Field label="Surface Key" help={definitionForm.id ? 'Surface keys stay stable during edits so existing placements remain attached.' : null}><input value={definitionForm.surface_key} onChange={(event) => setDefinitionForm((current) => ({ ...current, surface_key: event.target.value }))} disabled={Boolean(definitionForm.id)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none disabled:cursor-not-allowed disabled:opacity-60" maxLength={120} /></Field>
<Field label="Title"><input value={definitionForm.title} onChange={(event) => setDefinitionForm((current) => ({ ...current, title: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" maxLength={160} /></Field>
<Field label="Mode"><select value={definitionForm.mode} onChange={(event) => setDefinitionForm((current) => ({ ...current, mode: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none"><option value="manual">Manual</option><option value="automatic">Automatic</option><option value="hybrid">Hybrid</option></select></Field>
<Field label="Ranking"><select value={definitionForm.ranking_mode} onChange={(event) => setDefinitionForm((current) => ({ ...current, ranking_mode: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none"><option value="ranking_score">Ranking score</option><option value="recent_activity">Recent activity</option><option value="quality_score">Quality score</option></select></Field>
<Field label="Mode"><NovaSelect value={definitionForm.mode} onChange={(val) => setDefinitionForm((current) => ({ ...current, mode: val }))} searchable={false} options={[{ value: 'manual', label: 'Manual' }, { value: 'automatic', label: 'Automatic' }, { value: 'hybrid', label: 'Hybrid' }]} /></Field>
<Field label="Ranking"><NovaSelect value={definitionForm.ranking_mode} onChange={(val) => setDefinitionForm((current) => ({ ...current, ranking_mode: val }))} searchable={false} options={[{ value: 'ranking_score', label: 'Ranking score' }, { value: 'recent_activity', label: 'Recent activity' }, { value: 'quality_score', label: 'Quality score' }]} /></Field>
<Field label="Max Items"><input type="number" min="1" max="24" value={definitionForm.max_items} onChange={(event) => setDefinitionForm((current) => ({ ...current, max_items: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /></Field>
<Field label="Starts At" help="Optional activation window for the full surface definition."><input type="datetime-local" value={definitionForm.starts_at} onChange={(event) => setDefinitionForm((current) => ({ ...current, starts_at: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /></Field>
<Field label="Ends At" help="Leave blank when the surface should stay live until staff changes it."><input type="datetime-local" value={definitionForm.ends_at} onChange={(event) => setDefinitionForm((current) => ({ ...current, ends_at: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /></Field>
<Field label="Fallback Surface Key" help="Optional fallback when this definition is inactive, scheduled out, or resolves no items."><input value={definitionForm.fallback_surface_key} onChange={(event) => setDefinitionForm((current) => ({ ...current, fallback_surface_key: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" maxLength={120} /></Field>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white"><input type="checkbox" checked={definitionForm.is_active} onChange={(event) => setDefinitionForm((current) => ({ ...current, is_active: event.target.checked }))} />Active</label>
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white"><Checkbox checked={definitionForm.is_active} onChange={(event) => setDefinitionForm((current) => ({ ...current, is_active: event.target.checked }))} label="Active" /></div>
</div>
<Field label="Description" help="Operational note for staff browsing this surface later."><textarea value={definitionForm.description} onChange={(event) => setDefinitionForm((current) => ({ ...current, description: event.target.value }))} className="mt-4 min-h-[96px] w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" maxLength={400} /></Field>
<Field label="Rules JSON" help="Supported filters include campaign, event, season, type, presentation_style, theme_token, collaboration_mode, owner_username or owner_usernames, commercial_eligible_only, analytics_enabled_only, min_quality_score, min_ranking_score, include_collection_ids, exclude_collection_ids, and featured_only."><textarea value={definitionForm.rules_json} onChange={(event) => setDefinitionForm((current) => ({ ...current, rules_json: event.target.value }))} className="mt-4 min-h-[160px] w-full rounded-2xl border border-white/10 bg-slate-950/50 px-4 py-3 font-mono text-sm text-white outline-none" spellCheck={false} /></Field>
@@ -423,14 +425,14 @@ export default function CollectionStaffSurfaces() {
<h2 className="mt-2 text-2xl font-semibold text-white">Manual and campaign slots</h2>
</div>
<div className="mt-6 grid gap-4 md:grid-cols-2">
<Field label="Surface"><select value={placementForm.surface_key} onChange={(event) => setPlacementForm((current) => ({ ...current, surface_key: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none">{surfaceKeyOptions.map((option) => <option key={option} value={option}>{option}</option>)}</select></Field>
<Field label="Collection"><select value={placementForm.collection_id} onChange={(event) => setPlacementForm((current) => ({ ...current, collection_id: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none">{collectionOptions.map((option) => <option key={option.id} value={option.id}>{option.title}</option>)}</select></Field>
<Field label="Placement Type"><select value={placementForm.placement_type} onChange={(event) => setPlacementForm((current) => ({ ...current, placement_type: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none"><option value="manual">Manual</option><option value="campaign">Campaign</option><option value="scheduled_override">Scheduled override</option></select></Field>
<Field label="Surface"><NovaSelect value={placementForm.surface_key} onChange={(val) => setPlacementForm((current) => ({ ...current, surface_key: val }))} options={surfaceKeyOptions.map((o) => ({ value: o, label: o }))} /></Field>
<Field label="Collection"><NovaSelect value={String(placementForm.collection_id || '')} onChange={(val) => setPlacementForm((current) => ({ ...current, collection_id: val }))} options={collectionOptions.map((o) => ({ value: String(o.id), label: o.title }))} /></Field>
<Field label="Placement Type"><NovaSelect value={placementForm.placement_type} onChange={(val) => setPlacementForm((current) => ({ ...current, placement_type: val }))} searchable={false} options={[{ value: 'manual', label: 'Manual' }, { value: 'campaign', label: 'Campaign' }, { value: 'scheduled_override', label: 'Scheduled override' }]} /></Field>
<Field label="Priority"><input type="number" min="-100" max="100" value={placementForm.priority} onChange={(event) => setPlacementForm((current) => ({ ...current, priority: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /></Field>
<Field label="Starts At"><input type="datetime-local" value={placementForm.starts_at} onChange={(event) => setPlacementForm((current) => ({ ...current, starts_at: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /></Field>
<Field label="Ends At"><input type="datetime-local" value={placementForm.ends_at} onChange={(event) => setPlacementForm((current) => ({ ...current, ends_at: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /></Field>
<Field label="Campaign Key" help="Optional campaign label for reporting and grouped overrides."><input value={placementForm.campaign_key} onChange={(event) => setPlacementForm((current) => ({ ...current, campaign_key: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" maxLength={80} /></Field>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white"><input type="checkbox" checked={placementForm.is_active} onChange={(event) => setPlacementForm((current) => ({ ...current, is_active: event.target.checked }))} />Active placement</label>
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white"><Checkbox checked={placementForm.is_active} onChange={(event) => setPlacementForm((current) => ({ ...current, is_active: event.target.checked }))} label="Active placement" /></div>
</div>
<Field label="Notes" help="Internal note for why this collection owns the slot."><textarea value={placementForm.notes} onChange={(event) => setPlacementForm((current) => ({ ...current, notes: event.target.value }))} className="mt-4 min-h-[110px] w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" maxLength={1000} /></Field>
<div className="mt-5 flex flex-wrap items-center gap-3">
@@ -459,7 +461,7 @@ export default function CollectionStaffSurfaces() {
const checked = batchForm.collection_ids.includes(option.id)
return (
<label key={option.id} className={`flex cursor-pointer items-start gap-3 rounded-[22px] border px-4 py-3 transition ${checked ? 'border-lime-300/30 bg-lime-400/10' : 'border-white/10 bg-white/[0.04] hover:bg-white/[0.07]'}`}>
<input type="checkbox" checked={checked} onChange={() => toggleBatchCollection(option.id)} className="mt-1" />
<Checkbox checked={checked} onChange={() => toggleBatchCollection(option.id)} />
<span className="min-w-0">
<span className="block truncate text-sm font-semibold text-white">{option.title}</span>
<span className="mt-1 block text-xs text-slate-400">{option.type || 'collection'} · {option.visibility || 'public'}</span>
@@ -487,10 +489,10 @@ export default function CollectionStaffSurfaces() {
<p className="text-sm font-semibold text-white">Optional placement plan</p>
<p className="mt-2 text-sm text-slate-300">If you set a surface, the preview shows which collections can safely be placed and which ones will be skipped.</p>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<Field label="Surface"><select value={batchForm.surface_key} onChange={(event) => setBatchForm((current) => ({ ...current, surface_key: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none"><option value="">No placement</option>{surfaceKeyOptions.map((option) => <option key={option} value={option}>{option}</option>)}</select></Field>
<Field label="Placement Type"><select value={batchForm.placement_type} onChange={(event) => setBatchForm((current) => ({ ...current, placement_type: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white outline-none"><option value="campaign">Campaign</option><option value="manual">Manual</option><option value="scheduled_override">Scheduled override</option></select></Field>
<Field label="Surface"><NovaSelect value={batchForm.surface_key} onChange={(val) => setBatchForm((current) => ({ ...current, surface_key: val }))} placeholder="No placement" options={surfaceKeyOptions.map((o) => ({ value: o, label: o }))} /></Field>
<Field label="Placement Type"><NovaSelect value={batchForm.placement_type} onChange={(val) => setBatchForm((current) => ({ ...current, placement_type: val }))} searchable={false} options={[{ value: 'campaign', label: 'Campaign' }, { value: 'manual', label: 'Manual' }, { value: 'scheduled_override', label: 'Scheduled override' }]} /></Field>
<Field label="Priority"><input type="number" min="-100" max="100" value={batchForm.priority} onChange={(event) => setBatchForm((current) => ({ ...current, priority: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /></Field>
<label className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white"><input type="checkbox" checked={batchForm.is_active} onChange={(event) => setBatchForm((current) => ({ ...current, is_active: event.target.checked }))} />Active placement</label>
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white"><Checkbox checked={batchForm.is_active} onChange={(event) => setBatchForm((current) => ({ ...current, is_active: event.target.checked }))} label="Active placement" /></div>
<Field label="Starts At"><input type="datetime-local" value={batchForm.starts_at} onChange={(event) => setBatchForm((current) => ({ ...current, starts_at: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /></Field>
<Field label="Ends At"><input type="datetime-local" value={batchForm.ends_at} onChange={(event) => setBatchForm((current) => ({ ...current, ends_at: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /></Field>
</div>

View File

@@ -1,5 +1,7 @@
import React from 'react'
import { Head, usePage } from '@inertiajs/react'
import Checkbox from '../../components/ui/Checkbox'
import NovaSelect from '../../components/ui/NovaSelect'
function getCsrfToken() {
if (typeof document === 'undefined') return ''
@@ -565,11 +567,9 @@ export default function FeaturedArtworksAdmin() {
<Field label="Active" help="Inactive rows stay visible in admin but cannot win the homepage hero.">
<label className="flex h-[52px] items-center gap-3 rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-slate-100">
<input
type="checkbox"
<Checkbox
checked={Boolean(form.is_active)}
onChange={(event) => setForm((current) => ({ ...current, is_active: event.target.checked }))}
className="h-4 w-4 rounded border-white/20 bg-transparent text-sky-400 focus:ring-sky-300/30"
/>
<span>{form.is_active ? 'Active on save' : 'Inactive on save'}</span>
</label>
@@ -625,22 +625,9 @@ export default function FeaturedArtworksAdmin() {
placeholder="Filter by title, artist, or artwork ID"
className="rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40"
/>
<select value={filter} onChange={(event) => setFilter(event.target.value)} className="rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="all">All rows</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="expired">Expired</option>
<option value="winner">Winner</option>
<option value="eligible">Eligible</option>
<option value="ineligible">Not eligible</option>
</select>
<NovaSelect value={filter} onChange={(val) => setFilter(val)} searchable={false} options={[{ value: 'all', label: 'All rows' }, { value: 'active', label: 'Active' }, { value: 'inactive', label: 'Inactive' }, { value: 'expired', label: 'Expired' }, { value: 'winner', label: 'Winner' }, { value: 'eligible', label: 'Eligible' }, { value: 'ineligible', label: 'Not eligible' }]} />
<div className="grid grid-cols-[1fr_auto] gap-3">
<select value={sortKey} onChange={(event) => setSortKey(event.target.value)} className="rounded-2xl border border-white/10 bg-[#08111d] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/40">
<option value="priority">Priority</option>
<option value="featured_at">Featured Since</option>
<option value="expires_at">Expires</option>
<option value="score_30d">Medal Score (30d)</option>
</select>
<NovaSelect value={sortKey} onChange={(val) => setSortKey(val)} searchable={false} options={[{ value: 'priority', label: 'Priority' }, { value: 'featured_at', label: 'Featured Since' }, { value: 'expires_at', label: 'Expires' }, { value: 'score_30d', label: 'Medal Score (30d)' }]} />
<button type="button" onClick={() => setSortDirection((current) => current === 'desc' ? 'asc' : 'desc')} className="rounded-2xl border border-white/10 px-4 py-3 text-sm font-semibold text-slate-100 transition hover:border-white/20 hover:bg-white/5">
{sortDirection === 'desc' ? 'Desc' : 'Asc'}
</button>

View File

@@ -1,6 +1,8 @@
import React from 'react'
import { Head, Link, usePage } from '@inertiajs/react'
import NovaCardCanvasPreview from '../../components/nova-cards/NovaCardCanvasPreview'
import Checkbox from '../../components/ui/Checkbox'
import NovaSelect from '../../components/ui/NovaSelect'
function requestJson(url, { method = 'GET', body } = {}) {
return fetch(url, {
@@ -396,16 +398,15 @@ export default function NovaCardsAdminIndex() {
</div>
) : null}
<div className="mt-3 max-w-xs">
<label className="text-sm text-amber-50">
<div className="text-sm text-amber-50">
<span className="mb-2 block text-[11px] font-semibold uppercase tracking-[0.16em] text-amber-100/75">Disposition</span>
<select
<NovaSelect
value={preferredDisposition(report.target.moderation_target.moderation_status, reportDispositions[report.id])}
onChange={(event) => setReportDispositions((current) => ({ ...current, [report.id]: event.target.value }))}
className="w-full rounded-2xl border border-amber-200/20 bg-[#0d1726] px-4 py-3 text-white"
>
{dispositionOptionsForStatus(report.target.moderation_target.moderation_status).map((option) => <option key={`${report.id}-disp-${option.value}`} value={option.value}>{option.label}</option>)}
</select>
</label>
onChange={(value) => setReportDispositions((current) => ({ ...current, [report.id]: value }))}
options={dispositionOptionsForStatus(report.target.moderation_target.moderation_status).map((option) => ({ value: option.value, label: option.label }))}
searchable={false}
/>
</div>
</div>
<div className="mt-3 flex flex-wrap gap-2">
{(report.target.moderation_target.available_actions || []).map((actionItem) => (
@@ -497,40 +498,34 @@ export default function NovaCardsAdminIndex() {
</div>
<p className="mt-3 line-clamp-3 text-sm leading-7 text-slate-300">{card.quote_text}</p>
<div className="mt-4 grid gap-3 md:grid-cols-3">
<label className="text-sm text-slate-300">
<div className="text-sm text-slate-300">
<span className="mb-2 block">Status</span>
<select value={card.status} onChange={(event) => updateCard(card.id, { status: event.target.value })} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
{['draft', 'processing', 'published', 'hidden', 'rejected'].map((item) => <option key={`${card.id}-${item}`} value={item}>{item}</option>)}
</select>
</label>
<label className="text-sm text-slate-300">
<NovaSelect value={card.status} onChange={(val) => updateCard(card.id, { status: val })} searchable={false} options={['draft', 'processing', 'published', 'hidden', 'rejected'].map((s) => ({ value: s, label: s }))} />
</div>
<div className="text-sm text-slate-300">
<span className="mb-2 block">Moderation</span>
<select value={card.moderation_status} onChange={(event) => updateCard(card.id, { moderation_status: event.target.value, disposition: preferredDisposition(event.target.value, cardDispositions[card.id]) })} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
{['pending', 'approved', 'flagged', 'rejected'].map((item) => <option key={`${card.id}-mod-${item}`} value={item}>{item}</option>)}
</select>
</label>
<label className="text-sm text-slate-300">
<NovaSelect value={card.moderation_status} onChange={(val) => updateCard(card.id, { moderation_status: val, disposition: preferredDisposition(val, cardDispositions[card.id]) })} searchable={false} options={['pending', 'approved', 'flagged', 'rejected'].map((s) => ({ value: s, label: s }))} />
</div>
<div className="text-sm text-slate-300">
<span className="mb-2 block">Disposition</span>
<select
<NovaSelect
value={preferredDisposition(card.moderation_status, cardDispositions[card.id])}
onChange={(event) => {
const disposition = event.target.value
setCardDispositions((current) => ({ ...current, [card.id]: disposition }))
updateCard(card.id, { moderation_status: card.moderation_status, disposition })
onChange={(val) => {
setCardDispositions((current) => ({ ...current, [card.id]: val }))
updateCard(card.id, { moderation_status: card.moderation_status, disposition: val })
}}
className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white"
>
{dispositionOptionsForStatus(card.moderation_status).map((option) => <option key={`${card.id}-disp-${option.value}`} value={option.value}>{option.label}</option>)}
</select>
</label>
<label className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
searchable={false}
options={dispositionOptionsForStatus(card.moderation_status)}
/>
</div>
<div className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<span>Featured</span>
<input type="checkbox" checked={Boolean(card.featured)} onChange={(event) => updateCard(card.id, { featured: event.target.checked })} className="h-4 w-4" />
</label>
<label className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<Checkbox checked={Boolean(card.featured)} onChange={(event) => updateCard(card.id, { featured: event.target.checked })} />
</div>
<div className="flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<span>Allow remix</span>
<input type="checkbox" checked={Boolean(card.allow_remix)} onChange={(event) => updateCard(card.id, { allow_remix: event.target.checked })} className="h-4 w-4" />
</label>
<Checkbox checked={Boolean(card.allow_remix)} onChange={(event) => updateCard(card.id, { allow_remix: event.target.checked })} />
</div>
</div>
<div className="mt-4 grid gap-3 md:grid-cols-4 text-xs text-slate-400">
<div className="rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3">{card.likes_count || 0} likes</div>
@@ -596,10 +591,10 @@ export default function NovaCardsAdminIndex() {
<div className="rounded-2xl border border-white/10 bg-[#08111f]/70 px-3 py-3">{creator.featured_cards_count || 0} featured</div>
<div className="rounded-2xl border border-white/10 bg-[#08111f]/70 px-3 py-3">{creator.total_views_count || 0} views</div>
</div>
<label className="mt-3 flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<div className="mt-3 flex items-center justify-between rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<span>Feature on editorial page</span>
<input type="checkbox" checked={Boolean(creator.nova_featured_creator)} onChange={(event) => updateCreator(creator.id, { nova_featured_creator: event.target.checked })} className="h-4 w-4" />
</label>
<Checkbox checked={Boolean(creator.nova_featured_creator)} onChange={(event) => updateCreator(creator.id, { nova_featured_creator: event.target.checked })} />
</div>
</div>
))}
</div>

View File

@@ -1,5 +1,7 @@
import React from 'react'
import { Head, Link, usePage } from '@inertiajs/react'
import Checkbox from '../../components/ui/Checkbox'
import NovaSelect from '../../components/ui/NovaSelect'
function requestJson(url, { method = 'GET', body } = {}) {
return fetch(url, {
@@ -101,13 +103,10 @@ export default function NovaCardsAssetPackAdmin() {
<input value={form.name} onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))} placeholder="Pack name" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
<input value={form.slug} onChange={(event) => setForm((current) => ({ ...current, slug: event.target.value }))} placeholder="Slug" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
<textarea value={form.description} onChange={(event) => setForm((current) => ({ ...current, description: event.target.value }))} placeholder="Description" rows={3} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white md:col-span-2" />
<label className="text-sm text-slate-300">
<div className="text-sm text-slate-300">
<span className="mb-2 block">Type</span>
<select value={form.type} onChange={(event) => setForm((current) => ({ ...current, type: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
<option value="asset">asset</option>
<option value="template">template</option>
</select>
</label>
<NovaSelect value={form.type} onChange={(val) => setForm((current) => ({ ...current, type: val }))} searchable={false} options={[{ value: 'asset', label: 'asset' }, { value: 'template', label: 'template' }]} />
</div>
<input value={form.preview_image} onChange={(event) => setForm((current) => ({ ...current, preview_image: event.target.value }))} placeholder="Preview image URL" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
<textarea value={JSON.stringify(form.manifest_json || {}, null, 2)} onChange={(event) => {
try {
@@ -118,8 +117,8 @@ export default function NovaCardsAssetPackAdmin() {
}} rows={10} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 font-mono text-sm text-white md:col-span-2" />
</div>
<div className="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<label className="flex items-center gap-2"><input type="checkbox" checked={Boolean(form.active)} onChange={(event) => setForm((current) => ({ ...current, active: event.target.checked }))} className="h-4 w-4" /> Active</label>
<label className="flex items-center gap-2"><input type="checkbox" checked={Boolean(form.official)} onChange={(event) => setForm((current) => ({ ...current, official: event.target.checked }))} className="h-4 w-4" /> Official</label>
<Checkbox checked={Boolean(form.active)} onChange={(event) => setForm((current) => ({ ...current, active: event.target.checked }))} label="Active" />
<Checkbox checked={Boolean(form.official)} onChange={(event) => setForm((current) => ({ ...current, official: event.target.checked }))} label="Official" />
</div>
<button type="button" onClick={savePack} className="mt-5 w-full rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">{selectedId ? 'Update pack' : 'Create pack'}</button>
</section>

View File

@@ -1,5 +1,7 @@
import React from 'react'
import { Head, Link, usePage } from '@inertiajs/react'
import Checkbox from '../../components/ui/Checkbox'
import NovaSelect from '../../components/ui/NovaSelect'
function requestJson(url, { method = 'GET', body } = {}) {
return fetch(url, {
@@ -105,19 +107,14 @@ export default function NovaCardsChallengeAdmin() {
<input value={form.slug} onChange={(event) => setForm((current) => ({ ...current, slug: event.target.value }))} placeholder="Slug" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
<textarea value={form.description} onChange={(event) => setForm((current) => ({ ...current, description: event.target.value }))} placeholder="Description" rows={3} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white md:col-span-2" />
<textarea value={form.prompt} onChange={(event) => setForm((current) => ({ ...current, prompt: event.target.value }))} placeholder="Prompt" rows={4} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white md:col-span-2" />
<label className="text-sm text-slate-300">
<div className="text-sm text-slate-300">
<span className="mb-2 block">Status</span>
<select value={form.status} onChange={(event) => setForm((current) => ({ ...current, status: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
{['draft', 'active', 'completed', 'archived'].map((status) => <option key={status} value={status}>{status}</option>)}
</select>
</label>
<label className="text-sm text-slate-300">
<NovaSelect value={form.status} onChange={(val) => setForm((current) => ({ ...current, status: val }))} searchable={false} options={['draft', 'active', 'completed', 'archived'].map((s) => ({ value: s, label: s }))} />
</div>
<div className="text-sm text-slate-300">
<span className="mb-2 block">Winner card</span>
<select value={form.winner_card_id} onChange={(event) => setForm((current) => ({ ...current, winner_card_id: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
<option value="">No winner</option>
{cards.map((card) => <option key={card.id} value={card.id}>{card.title}</option>)}
</select>
</label>
<NovaSelect value={String(form.winner_card_id || '')} onChange={(val) => setForm((current) => ({ ...current, winner_card_id: val }))} placeholder="No winner" options={cards.map((c) => ({ value: String(c.id), label: c.title }))} />
</div>
<label className="text-sm text-slate-300">
<span className="mb-2 block">Starts at</span>
<input type="datetime-local" value={form.starts_at} onChange={(event) => setForm((current) => ({ ...current, starts_at: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
@@ -135,8 +132,8 @@ export default function NovaCardsChallengeAdmin() {
}} rows={10} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 font-mono text-sm text-white md:col-span-2" />
</div>
<div className="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<label className="flex items-center gap-2"><input type="checkbox" checked={Boolean(form.official)} onChange={(event) => setForm((current) => ({ ...current, official: event.target.checked }))} className="h-4 w-4" /> Official</label>
<label className="flex items-center gap-2"><input type="checkbox" checked={Boolean(form.featured)} onChange={(event) => setForm((current) => ({ ...current, featured: event.target.checked }))} className="h-4 w-4" /> Featured</label>
<Checkbox checked={Boolean(form.official)} onChange={(event) => setForm((current) => ({ ...current, official: event.target.checked }))} label="Official" />
<Checkbox checked={Boolean(form.featured)} onChange={(event) => setForm((current) => ({ ...current, featured: event.target.checked }))} label="Featured" />
</div>
<button type="button" onClick={saveChallenge} className="mt-5 w-full rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">{selectedId ? 'Update challenge' : 'Create challenge'}</button>
</section>

View File

@@ -1,5 +1,7 @@
import React from 'react'
import { Head, Link, usePage } from '@inertiajs/react'
import Checkbox from '../../components/ui/Checkbox'
import NovaSelect from '../../components/ui/NovaSelect'
function requestJson(url, { method = 'GET', body } = {}) {
return fetch(url, {
@@ -133,27 +135,22 @@ export default function NovaCardsCollectionAdmin() {
<div className="rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_50px_rgba(2,6,23,0.18)]">
<div className="mb-4 text-sm font-semibold uppercase tracking-[0.2em] text-slate-400">Collection editor</div>
<div className="grid gap-4 md:grid-cols-2">
<label className="text-sm text-slate-300">
<div className="text-sm text-slate-300">
<span className="mb-2 block">Owner</span>
<select value={form.user_id} onChange={(event) => setForm((current) => ({ ...current, user_id: Number(event.target.value) }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
{admins.map((admin) => <option key={admin.id} value={admin.id}>{admin.name || admin.username}</option>)}
</select>
</label>
<label className="text-sm text-slate-300">
<NovaSelect value={form.user_id} onChange={(val) => setForm((current) => ({ ...current, user_id: Number(val) }))} options={admins.map((a) => ({ value: a.id, label: a.name || a.username }))} />
</div>
<div className="text-sm text-slate-300">
<span className="mb-2 block">Visibility</span>
<select value={form.visibility} onChange={(event) => setForm((current) => ({ ...current, visibility: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
<option value="public">public</option>
<option value="private">private</option>
</select>
</label>
<NovaSelect value={form.visibility} onChange={(val) => setForm((current) => ({ ...current, visibility: val }))} searchable={false} options={[{ value: 'public', label: 'public' }, { value: 'private', label: 'private' }]} />
</div>
<input value={form.name} onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))} placeholder="Collection name" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
<input value={form.slug} onChange={(event) => setForm((current) => ({ ...current, slug: event.target.value }))} placeholder="Slug" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
<textarea value={form.description} onChange={(event) => setForm((current) => ({ ...current, description: event.target.value }))} placeholder="Description" rows={4} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white md:col-span-2" />
</div>
<div className="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<div className="flex flex-wrap items-center gap-4">
<label className="flex items-center gap-2"><input type="checkbox" checked={Boolean(form.official)} onChange={(event) => setForm((current) => ({ ...current, official: event.target.checked }))} className="h-4 w-4" /> Official collection</label>
<label className="flex items-center gap-2"><input type="checkbox" checked={Boolean(form.featured)} onChange={(event) => setForm((current) => ({ ...current, featured: event.target.checked }))} className="h-4 w-4" /> Featured collection</label>
<Checkbox checked={Boolean(form.official)} onChange={(event) => setForm((current) => ({ ...current, official: event.target.checked }))} label="Official collection" />
<Checkbox checked={Boolean(form.featured)} onChange={(event) => setForm((current) => ({ ...current, featured: event.target.checked }))} label="Featured collection" />
</div>
{selected?.public_url ? <a href={selected.public_url} className="text-sky-100 transition hover:text-white" target="_blank" rel="noreferrer">Open public page</a> : null}
</div>
@@ -167,10 +164,7 @@ export default function NovaCardsCollectionAdmin() {
) : (
<>
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">
<select value={cardId} onChange={(event) => setCardId(event.target.value)} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
<option value="">Select a card</option>
{cards.map((card) => <option key={card.id} value={card.id}>{card.title}</option>)}
</select>
<NovaSelect value={String(cardId || '')} onChange={(val) => setCardId(val)} placeholder="Select a card" options={cards.map((c) => ({ value: String(c.id), label: c.title }))} />
<input value={cardNote} onChange={(event) => setCardNote(event.target.value)} placeholder="Optional curator note" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
<button type="button" onClick={attachCard} className="rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">Add</button>
</div>

View File

@@ -1,5 +1,7 @@
import React from 'react'
import { Head, Link, usePage } from '@inertiajs/react'
import Checkbox from '../../components/ui/Checkbox'
import NovaSelect from '../../components/ui/NovaSelect'
function requestJson(url, { method = 'GET', body } = {}) {
return fetch(url, {
@@ -152,36 +154,26 @@ export default function NovaCardsTemplateAdmin() {
<input value={form.name} onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))} placeholder="Template name" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
<input value={form.slug} onChange={(event) => setForm((current) => ({ ...current, slug: event.target.value }))} placeholder="Slug" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white" />
<textarea value={form.description} onChange={(event) => setForm((current) => ({ ...current, description: event.target.value }))} placeholder="Description" rows={3} className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white md:col-span-2" />
<label className="text-sm text-slate-300">
<div className="text-sm text-slate-300">
<span className="mb-2 block">Font preset</span>
<select value={form.config_json?.font_preset || 'modern-sans'} onChange={(event) => setForm((current) => ({ ...current, config_json: { ...current.config_json, font_preset: event.target.value } }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
{fonts.map((font) => <option key={font.key} value={font.key}>{font.label}</option>)}
</select>
</label>
<label className="text-sm text-slate-300">
<NovaSelect value={form.config_json?.font_preset || 'modern-sans'} onChange={(val) => setForm((current) => ({ ...current, config_json: { ...current.config_json, font_preset: val } }))} options={fonts.map((f) => ({ value: f.key, label: f.label }))} searchable={false} />
</div>
<div className="text-sm text-slate-300">
<span className="mb-2 block">Gradient preset</span>
<select value={form.config_json?.gradient_preset || 'midnight-nova'} onChange={(event) => setForm((current) => ({ ...current, config_json: { ...current.config_json, gradient_preset: event.target.value } }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
{gradients.map((gradient) => <option key={gradient.key} value={gradient.key}>{gradient.label}</option>)}
</select>
</label>
<label className="text-sm text-slate-300">
<NovaSelect value={form.config_json?.gradient_preset || 'midnight-nova'} onChange={(val) => setForm((current) => ({ ...current, config_json: { ...current.config_json, gradient_preset: val } }))} options={gradients.map((g) => ({ value: g.key, label: g.label }))} searchable={false} />
</div>
<div className="text-sm text-slate-300">
<span className="mb-2 block">Layout preset</span>
<select value={form.config_json?.layout || 'quote_heavy'} onChange={(event) => setForm((current) => ({ ...current, config_json: { ...current.config_json, layout: event.target.value } }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
{['quote_heavy', 'author_emphasis', 'centered', 'minimal'].map((value) => <option key={value} value={value}>{value}</option>)}
</select>
</label>
<label className="text-sm text-slate-300">
<NovaSelect value={form.config_json?.layout || 'quote_heavy'} onChange={(val) => setForm((current) => ({ ...current, config_json: { ...current.config_json, layout: val } }))} options={['quote_heavy', 'author_emphasis', 'centered', 'minimal'].map((v) => ({ value: v, label: v }))} searchable={false} />
</div>
<div className="text-sm text-slate-300">
<span className="mb-2 block">Text alignment</span>
<select value={form.config_json?.text_align || 'center'} onChange={(event) => setForm((current) => ({ ...current, config_json: { ...current.config_json, text_align: event.target.value } }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
{['left', 'center', 'right'].map((value) => <option key={value} value={value}>{value}</option>)}
</select>
</label>
<label className="text-sm text-slate-300">
<NovaSelect value={form.config_json?.text_align || 'center'} onChange={(val) => setForm((current) => ({ ...current, config_json: { ...current.config_json, text_align: val } }))} options={['left', 'center', 'right'].map((v) => ({ value: v, label: v }))} searchable={false} />
</div>
<div className="text-sm text-slate-300">
<span className="mb-2 block">Overlay style</span>
<select value={form.config_json?.overlay_style || 'dark-soft'} onChange={(event) => setForm((current) => ({ ...current, config_json: { ...current.config_json, overlay_style: event.target.value } }))} className="w-full rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-white">
{['none', 'dark-soft', 'dark-strong', 'light-soft'].map((value) => <option key={value} value={value}>{value}</option>)}
</select>
</label>
<NovaSelect value={form.config_json?.overlay_style || 'dark-soft'} onChange={(val) => setForm((current) => ({ ...current, config_json: { ...current.config_json, overlay_style: val } }))} options={['none', 'dark-soft', 'dark-strong', 'light-soft'].map((v) => ({ value: v, label: v }))} searchable={false} />
</div>
<label className="text-sm text-slate-300">
<span className="mb-2 block">Text color</span>
<input type="color" value={form.config_json?.text_color || '#ffffff'} onChange={(event) => setForm((current) => ({ ...current, config_json: { ...current.config_json, text_color: event.target.value } }))} className="h-12 w-full rounded-2xl border border-white/10 bg-[#0d1726] p-2" />
@@ -191,16 +183,15 @@ export default function NovaCardsTemplateAdmin() {
<div className="mb-3 text-sm font-semibold uppercase tracking-[0.18em] text-slate-400">Supported formats</div>
<div className="flex flex-wrap gap-3">
{formats.map((format) => (
<label key={format.key} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.03] px-3 py-2 text-sm text-slate-200">
<input type="checkbox" checked={form.supported_formats.includes(format.key)} onChange={() => toggleFormat(format.key)} className="h-4 w-4" />
{format.label}
</label>
<div key={format.key} className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.03] px-3 py-2 text-sm text-slate-200">
<Checkbox checked={form.supported_formats.includes(format.key)} onChange={() => toggleFormat(format.key)} label={format.label} />
</div>
))}
</div>
</div>
<div className="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<label className="flex items-center gap-2"><input type="checkbox" checked={Boolean(form.active)} onChange={(event) => setForm((current) => ({ ...current, active: event.target.checked }))} className="h-4 w-4" /> Active</label>
<label className="flex items-center gap-2"><input type="checkbox" checked={Boolean(form.official)} onChange={(event) => setForm((current) => ({ ...current, official: event.target.checked }))} className="h-4 w-4" /> Official</label>
<Checkbox checked={Boolean(form.active)} onChange={(event) => setForm((current) => ({ ...current, active: event.target.checked }))} label="Active" />
<Checkbox checked={Boolean(form.official)} onChange={(event) => setForm((current) => ({ ...current, official: event.target.checked }))} label="Official" />
</div>
<button type="button" onClick={saveTemplate} className="mt-5 w-full rounded-2xl border border-sky-300/20 bg-sky-400/10 px-4 py-3 text-sm font-semibold text-sky-100 transition hover:bg-sky-400/15">{selectedId ? 'Update template' : 'Create template'}</button>
</section>

View File

@@ -2,6 +2,7 @@ import React from 'react'
import { usePage } from '@inertiajs/react'
import CollectionCard from '../../components/profile/collections/CollectionCard'
import SeoHead from '../../components/seo/SeoHead'
import NovaSelect from '../../components/ui/NovaSelect'
function getCsrfToken() {
if (typeof document === 'undefined') return ''
@@ -475,9 +476,7 @@ export default function SavedCollections() {
</div>
{savedLists.length ? (
<div className="flex flex-wrap items-center gap-3 rounded-[24px] border border-white/10 bg-white/[0.04] px-4 py-3">
<select value={selectedLists[collection.id] || savedLists[0]?.id || ''} onChange={(event) => setSelectedLists((current) => ({ ...current, [collection.id]: event.target.value }))} className="min-w-[180px] rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-2.5 text-sm text-white outline-none">
{savedLists.map((list) => <option key={list.id} value={list.id}>{list.title}</option>)}
</select>
<NovaSelect value={String(selectedLists[collection.id] || savedLists[0]?.id || '')} onChange={(val) => setSelectedLists((current) => ({ ...current, [collection.id]: val }))} options={savedLists.map((l) => ({ value: String(l.id), label: l.title }))} />
<button type="button" onClick={() => handleAddToList(collection.id)} disabled={busy === `list-${collection.id}`} className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-white/[0.07] disabled:opacity-60"><i className={`fa-solid ${busy === `list-${collection.id}` ? 'fa-circle-notch fa-spin' : 'fa-folder-plus'} fa-fw`} />Add to list</button>
</div>
) : null}