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

@@ -0,0 +1,55 @@
import React, { useEffect, useState } from 'react'
import NovaSelect from '../../../ui/NovaSelect'
function Button({ tone = 'default', children, ...props }) {
const tones = {
default: 'border-white/10 bg-white/[0.04] text-slate-200',
sky: 'border-sky-300/20 bg-sky-400/10 text-sky-100',
emerald: 'border-emerald-300/20 bg-emerald-400/10 text-emerald-100',
amber: 'border-amber-300/20 bg-amber-400/10 text-amber-100',
rose: 'border-rose-300/20 bg-rose-400/10 text-rose-100',
}
return <button type="button" {...props} className={`rounded-2xl border px-3 py-2 text-sm font-semibold transition disabled:cursor-not-allowed disabled:opacity-50 ${tones[tone] || tones.default} ${props.className || ''}`.trim()}>{children}</button>
}
export default function WorldSuggestionActions({ item, busyKey = '', onAddFeatured, onAddSection, onPin, onDismiss, onNotRelevant, onRestore }) {
const targets = Array.isArray(item?.section_targets) ? item.section_targets : []
const [selectedSection, setSelectedSection] = useState(item?.default_section_key || targets[0]?.value || '')
const isBusy = Boolean(busyKey)
useEffect(() => {
setSelectedSection(item?.state?.section_key || item?.default_section_key || targets[0]?.value || '')
}, [item?.default_section_key, item?.state?.section_key, targets])
if (!item) {
return null
}
if (item.state?.status === 'dismissed' || item.state?.status === 'not_relevant') {
return (
<div className="mt-4 flex flex-wrap gap-2">
<Button tone="sky" disabled={isBusy} onClick={() => onRestore(item)}>Restore</Button>
</div>
)
}
return (
<div className="mt-4 grid gap-3 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end">
<div className="grid gap-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">Section target</span>
<div className="flex flex-col gap-2 sm:flex-row">
<NovaSelect value={selectedSection} onChange={(val) => setSelectedSection(val)} options={targets} searchable={false} className="min-w-0 flex-1" />
<Button tone="emerald" disabled={isBusy || !selectedSection} onClick={() => onAddSection(item, selectedSection, false)}>Add to section</Button>
<Button tone="amber" disabled={isBusy || !selectedSection} onClick={() => onAddFeatured(item, selectedSection)}>Add as featured</Button>
</div>
</div>
<div className="flex flex-wrap justify-start gap-2 lg:justify-end">
<Button tone={item.state?.status === 'pinned' ? 'sky' : 'default'} disabled={isBusy || !selectedSection} onClick={() => item.state?.status === 'pinned' ? onRestore(item) : onPin(item, selectedSection)}>{item.state?.status === 'pinned' ? 'Unpin' : 'Pin for later'}</Button>
<Button tone="default" disabled={isBusy} onClick={() => onDismiss(item)}>Dismiss</Button>
<Button tone="rose" disabled={isBusy} onClick={() => onNotRelevant(item)}>Not relevant</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,40 @@
import React from 'react'
import Checkbox from '../../../ui/Checkbox'
import NovaSelect from '../../../ui/NovaSelect'
function FilterSelect({ label, value, onChange, options = [] }) {
const novaOptions = [{ value: '', label: 'All' }, ...options.map((o) => ({ value: o.value, label: typeof o.count === 'number' ? `${o.label} (${o.count})` : o.label }))]
return (
<div className="grid gap-2 text-sm text-slate-300">
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">{label}</span>
<NovaSelect value={value || ''} onChange={onChange} options={novaOptions} searchable={false} />
</div>
)
}
export default function WorldSuggestionFilters({ filters, value, onChange }) {
return (
<div className="grid gap-4 rounded-[24px] border border-white/10 bg-black/20 p-4 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,0.9fr)]">
<FilterSelect label="Category" value={value.category} onChange={(nextValue) => onChange({ ...value, category: nextValue })} options={filters?.category_options || []} />
<FilterSelect label="Entity type" value={value.type} onChange={(nextValue) => onChange({ ...value, type: nextValue })} options={filters?.type_options || []} />
<FilterSelect label="Section target" value={value.section} onChange={(nextValue) => onChange({ ...value, section: nextValue })} options={filters?.section_options || []} />
<FilterSelect label="Sort" value={value.sort} onChange={(nextValue) => onChange({ ...value, sort: nextValue })} options={filters?.sort_options || []} />
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<Checkbox checked={value.challengeOnly} onChange={(event) => onChange({ ...value, challengeOnly: event.target.checked })} label="Challenge-linked only" />
</div>
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<Checkbox checked={value.communityOnly} onChange={(event) => onChange({ ...value, communityOnly: event.target.checked })} label="Community submissions only" />
</div>
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<Checkbox checked={value.recurringOnly} onChange={(event) => onChange({ ...value, recurringOnly: event.target.checked })} label="Recurring-history informed" />
</div>
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<Checkbox checked={value.analyticsOnly} onChange={(event) => onChange({ ...value, analyticsOnly: event.target.checked })} label="Analytics-informed only" />
</div>
<div className="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-3 text-sm text-slate-200">
<Checkbox checked={value.showSuppressed} onChange={(event) => onChange({ ...value, showSuppressed: event.target.checked })} label="Show suppressed" />
</div>
</div>
)
}