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,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>