Add project filter, Shared/Assigned views, and task search
- Backend: GET /api/tasks accepts ?search=, ILIKE on title/notes - Generalized CategoryFilter into a reusable FilterChips component, added ProjectFilter alongside it (both now on Board/List/Today) - New Shared-with-me / Assigned-to-me nav views (FilteredListView), surfacing the view=shared/view=assigned params that already existed backend-side but had no UI - Global debounced (300ms) search bar in the layout header, wired into Board/List/Today/Dispatch's task fetches via SearchContext
This commit is contained in:
parent
017f6dfdc1
commit
4eab3c29ed
12 changed files with 227 additions and 53 deletions
|
|
@ -1,8 +1,9 @@
|
|||
import { useState } from 'react'
|
||||
import { Check, ChevronDown, Tag } from 'lucide-react'
|
||||
import type { Priority, Category } from '../types'
|
||||
import { Check, ChevronDown, Tag, FolderKanban, type LucideIcon } from 'lucide-react'
|
||||
import type { Priority } from '../types'
|
||||
import { PRIORITY_LABELS } from '../types'
|
||||
import { useCategories } from '../context/CategoriesContext'
|
||||
import { useProjects } from '../context/ProjectsContext'
|
||||
|
||||
export function PriorityBadge({ priority }: { priority: Priority }) {
|
||||
if (priority === 'medium') return null
|
||||
|
|
@ -19,26 +20,31 @@ export function Check_({ isDone, onClick }: { isDone: boolean; onClick: (e: Reac
|
|||
)
|
||||
}
|
||||
|
||||
// Collapsible multi-select category filter chip-bar — used on Board/List views.
|
||||
// `selected` empty means "no filter" (show everything).
|
||||
export function CategoryFilter({ selected, onChange }: { selected: number[]; onChange: (ids: number[]) => void }) {
|
||||
const { categories } = useCategories()
|
||||
interface FilterableEntity { id: number; name: string; colour: string }
|
||||
|
||||
// Collapsible multi-select filter chip-bar — generic over anything with an
|
||||
// id/name/colour (categories, projects, ...). `selected` empty means "no
|
||||
// filter" (show everything).
|
||||
function FilterChips<T extends FilterableEntity>({ items, selected, onChange, label, icon: Icon }: {
|
||||
items: T[]; selected: number[]; onChange: (ids: number[]) => void
|
||||
label: string; icon: LucideIcon
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
if (categories.length === 0) return null
|
||||
if (items.length === 0) return null
|
||||
|
||||
function toggle(id: number) {
|
||||
onChange(selected.includes(id) ? selected.filter(i => i !== id) : [...selected, id])
|
||||
}
|
||||
|
||||
const summary = selected.length === 0
|
||||
? 'All categories'
|
||||
: categories.filter(c => selected.includes(c.id)).map(c => c.name).join(', ')
|
||||
? `All ${label.toLowerCase()}s`
|
||||
: items.filter(x => selected.includes(x.id)).map(x => x.name).join(', ')
|
||||
|
||||
return (
|
||||
<div className="filter-section">
|
||||
<div className="filter-section-header" onClick={() => setOpen(o => !o)}>
|
||||
<Tag size={13} strokeWidth={1.75} />
|
||||
<span className="filter-section-label">Category</span>
|
||||
<Icon size={13} strokeWidth={1.75} />
|
||||
<span className="filter-section-label">{label}</span>
|
||||
<span className={`filter-section-summary${selected.length === 0 ? ' muted' : ''}`}>{summary}</span>
|
||||
<ChevronDown size={14} strokeWidth={1.75} className={`filter-section-chevron${open ? ' open' : ''}`} />
|
||||
</div>
|
||||
|
|
@ -47,10 +53,10 @@ export function CategoryFilter({ selected, onChange }: { selected: number[]; onC
|
|||
{selected.length > 0 && (
|
||||
<button className="chip" onClick={() => onChange([])}>Clear</button>
|
||||
)}
|
||||
{categories.map((c: Category) => (
|
||||
<button key={c.id} className={`chip${selected.includes(c.id) ? ' active' : ''}`} onClick={() => toggle(c.id)}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: c.colour, display: 'inline-block' }} />
|
||||
{c.name}
|
||||
{items.map(x => (
|
||||
<button key={x.id} className={`chip${selected.includes(x.id) ? ' active' : ''}`} onClick={() => toggle(x.id)}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: x.colour, display: 'inline-block' }} />
|
||||
{x.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -59,6 +65,16 @@ export function CategoryFilter({ selected, onChange }: { selected: number[]; onC
|
|||
)
|
||||
}
|
||||
|
||||
export function CategoryFilter({ selected, onChange }: { selected: number[]; onChange: (ids: number[]) => void }) {
|
||||
const { categories } = useCategories()
|
||||
return <FilterChips items={categories} selected={selected} onChange={onChange} label="Category" icon={Tag} />
|
||||
}
|
||||
|
||||
export function ProjectFilter({ selected, onChange }: { selected: number[]; onChange: (ids: number[]) => void }) {
|
||||
const { projects } = useProjects()
|
||||
return <FilterChips items={projects} selected={selected} onChange={onChange} label="Project" icon={FolderKanban} />
|
||||
}
|
||||
|
||||
export function formatDate(iso: string | null | undefined): string {
|
||||
if (!iso) return '—'
|
||||
return new Date(iso).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue