eu-scrapper-ui / src /widgets /FiltersBar.jsx
brestok's picture
init
90723a6
import React, {useMemo, useRef, useState} from 'react'
import Select from 'react-select'
import {DayPicker} from 'react-day-picker'
import 'react-day-picker/dist/style.css'
import {searchJobOptions} from '../api/jobs.js'
import { formatDate } from '../utils/date.js'
export function FiltersBar({value, onChange, domain, onRefresh, lastUpdatedAt, nextPlannedAt, onSubmit, onClear, isLoading}) {
function set(partial) {
onChange({...value, ...partial})
}
const hasAny = useMemo(() => {
return Boolean(
value.titles.length || value.companies.length || value.locations.length ||
value.salaryMin !== '' || value.salaryMax !== '' || value.top || value.dateFrom || value.dateTo
)
}, [value])
const [openCalendar, setOpenCalendar] = useState(false)
const selectStyles = {
control: (base) => ({
...base,
minHeight: 44,
backgroundColor: 'rgba(255,255,255,0.06)',
borderColor: 'rgba(255,255,255,0.2)',
color: 'white',
boxShadow: 'none',
}),
menu: (base) => ({
...base,
backgroundColor: '#171d28',
color: 'white',
border: '1px solid rgba(255,255,255,0.12)'
}),
menuPortal: (base) => ({...base, zIndex: 60}),
singleValue: (base) => ({...base, color: 'white'}),
input: (base) => ({...base, color: 'white'}),
multiValue: (base) => ({...base, backgroundColor: 'rgba(183,116,255,0.25)'}),
multiValueLabel: (base) => ({...base, color: 'white'}),
multiValueRemove: (base) => ({
...base,
color: 'white',
':hover': {backgroundColor: 'rgba(183,116,255,0.4)', color: '#0b0c10'}
}),
option: (base, s) => ({
...base,
background: s.isSelected ? 'rgba(183,116,255,0.25)' : s.isFocused ? 'rgba(255,255,255,0.08)' : 'transparent',
color: 'white'
}),
}
const [titleOptions, setTitleOptions] = useState([])
const [companyOptions, setCompanyOptions] = useState([])
const [locationOptions, setLocationOptions] = useState([])
const debounceIdRef = useRef(null)
async function loadOptions(field, query, setter) {
try {
const api = await searchJobOptions(field, query)
const arr = api?.data?.data || []
setter(arr.map(v => ({value: v, label: v})))
} catch (e) {
setter([])
}
}
function handleMenuOpen(field, setter) {
loadOptions(field, '', setter)
}
function handleInputChange(field, setter) {
return (inputValue, meta) => {
if (meta.action === 'input-change') {
if (debounceIdRef.current) clearTimeout(debounceIdRef.current)
debounceIdRef.current = setTimeout(() => {
loadOptions(field, inputValue || '', setter)
}, 500)
}
return inputValue
}
}
return (
<div className="glass p-4 relative z-20">
<div className="flex flex-wrap items-center gap-4 justify-between text-xs text-white/60">
<div>Last update: <span className="text-white">{isLoading ? <span className="skeleton h-3 w-28 inline-block align-middle"></span> : lastUpdatedAt}</span></div>
<div>Next planned: <span className="text-white">{isLoading ? <span className="skeleton h-3 w-28 inline-block align-middle"></span> : nextPlannedAt}</span></div>
<div className="flex-1"></div>
{hasAny && (
<div className="flex items-center gap-2 order-2 sm:order-none">
<button onClick={onSubmit} disabled={isLoading}
className="px-4 py-2 rounded-lg bg-gradient-to-r from-[var(--color-primary-600)] to-[var(--color-primary)] text-black font-semibold disabled:opacity-60">Submit
</button>
<button onClick={onClear} disabled={isLoading}
className="px-4 py-2 rounded-lg bg-white/5 border border-white/10 text-white/80 disabled:opacity-60">Clear
All
</button>
</div>
)}
</div>
<div className="mt-4 grid grid-cols-12 gap-4">
<div className="col-span-12 md:col-span-4">
<label className="text-xs text-white/60">Title</label>
<div className="mt-1">
<Select isMulti isDisabled={isLoading} isLoading={isLoading} options={titleOptions} value={value.titles.map(v => ({value: v, label: v}))}
onChange={vals => set({titles: vals.map(v => v.value)})}
onMenuOpen={() => handleMenuOpen('title', setTitleOptions)}
onInputChange={handleInputChange('title', setTitleOptions)} styles={selectStyles}
classNamePrefix="rs" menuPortalTarget={document.body}/>
</div>
</div>
<div className="col-span-12 md:col-span-4">
<label className="text-xs text-white/60">Company</label>
<div className="mt-1">
<Select isMulti isDisabled={isLoading} isLoading={isLoading} options={companyOptions}
value={value.companies.map(v => ({value: v, label: v}))}
onChange={vals => set({companies: vals.map(v => v.value)})}
onMenuOpen={() => handleMenuOpen('company', setCompanyOptions)}
onInputChange={handleInputChange('company', setCompanyOptions)} styles={selectStyles}
classNamePrefix="rs" menuPortalTarget={document.body}/>
</div>
</div>
<div className="col-span-12 md:col-span-4">
<label className="text-xs text-white/60">Location</label>
<div className="mt-1">
<Select isMulti isDisabled={isLoading} isLoading={isLoading} options={locationOptions}
value={value.locations.map(v => ({value: v, label: v}))}
onChange={vals => set({locations: vals.map(v => v.value)})}
onMenuOpen={() => handleMenuOpen('location', setLocationOptions)}
onInputChange={handleInputChange('location', setLocationOptions)} styles={selectStyles}
classNamePrefix="rs" menuPortalTarget={document.body}/>
</div>
</div>
<div className="col-span-6 md:col-span-3">
<div>
<label className="text-xs text-white/60">Salary from</label>
<input type="number" value={value.salaryMin} onChange={e => set({salaryMin: e.target.value})} disabled={isLoading}
className="mt-1 h-[44px] w-full px-3 rounded-lg bg-white/5 border border-white/10 outline-none focus:border-[var(--color-primary)] disabled:opacity-60"/>
</div>
</div>
<div className="col-span-6 md:col-span-3">
<div>
<label className="text-xs text-white/60">Salary to</label>
<input type="number" value={value.salaryMax} onChange={e => set({salaryMax: e.target.value})} disabled={isLoading}
className="mt-1 h-[44px] w-full px-3 rounded-lg bg-white/5 border border-white/10 outline-none focus:border-[var(--color-primary)] disabled:opacity-60"/>
</div>
</div>
<div className="col-span-12 md:col-span-2">
<label className="text-xs text-white/60">Top 5</label>
<div
className="mt-1 h-[44px] w-full rounded-lg bg-white/5 border border-white/10 flex items-center px-3 gap-2">
<input type="checkbox" checked={value.top} onChange={e => set({top: e.target.checked})} disabled={isLoading}
className="size-5 accent-[var(--color-primary-600)]"/>
<span className="text-sm">Only top 5</span>
</div>
</div>
<div className="col-span-12 md:col-span-4">
<label className="text-xs text-white/60">Date inserted (range)</label>
<button type="button" onClick={() => setOpenCalendar(v => !v)} disabled={isLoading}
className="mt-1 h-[44px] w-full text-left px-3 rounded-lg bg-white/5 border border-white/10 flex items-center justify-between disabled:opacity-60">
<span
className="text-white/80 text-sm">{value.dateFrom ? formatDate(value.dateFrom) : 'Start'} — {value.dateTo ? formatDate(value.dateTo) : 'End'}</span>
<span className="text-white/50"></span>
</button>
{openCalendar && (
<div
className="absolute z-50 mt-2 bg-[#0f1117] border border-white/10 rounded-xl shadow-2xl p-2">
<DayPicker
mode="range"
selected={{
from: value.dateFrom ? new Date(value.dateFrom) : undefined,
to: value.dateTo ? new Date(value.dateTo) : undefined
}}
onSelect={(range) => {
let from = range?.from ?? null
let to = range?.to ?? null
const isSameDay = from && to && from.toDateString() === to.toDateString()
if (isSameDay) {
to = null
}
set({dateFrom: from, dateTo: to})
if (from && to && !isSameDay) setOpenCalendar(false)
}}
styles={{
caption: {color: 'white'},
day: {color: 'white'},
day_selected: {backgroundColor: '#8f8cf8', color: '#0b0c10'},
day_range_start: {backgroundColor: '#7a76f2', color: '#0b0c10'},
day_range_end: {backgroundColor: '#7a76f2', color: '#0b0c10'},
day_range_middle: {backgroundColor: 'rgba(143,140,248,0.4)', color: 'white'},
}}
/>
</div>
)}
</div>
</div>
</div>
)
}