Files
components/filter.tsx
1'use client'
2
3import * as React from 'react'
4import { ChevronDown } from 'lucide-react'
5
6import { Button } from '@/components/ui/button'
7import { Checkbox } from '@/components/ui/checkbox'
8import { Input } from '@/components/ui/input'
9import { Label } from '@/components/ui/label'
10import {
11    Popover,
12    PopoverContent,
13    PopoverTrigger,
14} from '@/components/ui/popover'
15import {
16    Tabs,
17    TabsContent,
18    TabsList,
19    TabsTrigger,
20} from '@/components/ui/tabs'
21
22type Filters = {
23    category: string[]
24    availableDate: string
25    instant: boolean
26    experience: string[]
27}
28
29const DEFAULT_FILTERS: Filters = {
30    category: [],
31    availableDate: '',
32    instant: false,
33    experience: [],
34}
35
36export function FilterWithTabs() {
37    const [filters, setFilters] = React.useState<Filters>(DEFAULT_FILTERS)
38    const [isOpen, setIsOpen] = React.useState(false)
39
40    const toggleArrayValue = (
41        key: 'category' | 'experience',
42        value: string,
43    ) => {
44        setFilters((prev) => ({
45            ...prev,
46            [key]: prev[key].includes(value)
47                ? prev[key].filter((v) => v !== value)
48                : [...prev[key], value],
49        }))
50    }
51
52    const handleApply = () => {
53        setIsOpen(false)
54    }
55
56    const handleReset = () => {
57        setFilters(DEFAULT_FILTERS)
58    }
59    return (
60        <Popover open={isOpen} onOpenChange={setIsOpen}>
61            <PopoverTrigger asChild>
62                <Button className="gap-2">
63                    Filter products
64                    <ChevronDown className="h-4 w-4" />
65                </Button>
66            </PopoverTrigger>
67
68            <PopoverContent className="w-80 rounded-lg bg-slate-800 p-4">
69                <Tabs defaultValue="category">
70                    <TabsList className="mb-2 grid grid-cols-3">
71                        <TabsTrigger value="category">Category</TabsTrigger>
72                        <TabsTrigger value="availability">
73                            Availability
74                        </TabsTrigger>
75                        <TabsTrigger value="experience">Experience</TabsTrigger>
76                    </TabsList>
77
78                    <TabsContent value="category">
79                        <div className="space-y-2">
80                            <Label className="text-sm">Choose Type</Label>
81
82                            <div className="grid grid-cols-2 gap-2">
83                                {[
84                                    'Electronics',
85                                    'Fashion',
86                                    'Home',
87                                    'Beauty',
88                                ].map((item) => (
89                                    <Button
90                                        key={item}
91                                        variant={
92                                            filters.category.includes(item)
93                                                ? 'primary'
94                                                : 'secondary'
95                                        }
96                                        className="justify-start"
97                                        onClick={() =>
98                                            toggleArrayValue('category', item)
99                                        }
100                                    >
101                                        {item}
102                                    </Button>
103                                ))}
104                            </div>
105                        </div>
106                    </TabsContent>
107
108                    <TabsContent value="availability" className="space-y-4">
109                        <div className="space-y-2">
110                            <Label>Available On</Label>
111                            <Input
112                                type="date"
113                                placeholder="Select date"
114                                value={filters.availableDate}
115                                onChange={(e) =>
116                                    setFilters((prev) => ({
117                                        ...prev,
118                                        availableDate: e.target.value,
119                                    }))
120                                }
121                            />
122                        </div>
123
124                        <div className="flex items-center gap-2">
125                            <Checkbox
126                                id="instant"
127                                checked={filters.instant}
128                                onCheckedChange={(v) =>
129                                    setFilters((prev) => ({
130                                        ...prev,
131                                        instant: Boolean(v),
132                                    }))
133                                }
134                            />
135                            <Label htmlFor="instant">Instant access only</Label>
136                        </div>
137                    </TabsContent>
138
139                    <TabsContent value="experience">
140                        <div className="space-y-2">
141                            <Label className="text-sm">Experience Level</Label>
142
143                            <div className="space-y-2">
144                                {[
145                                    'Beginner Friendly',
146                                    'Intermediate',
147                                    'Expert',
148                                ].map((level) => (
149                                    <div
150                                        key={level}
151                                        className="flex items-center gap-2"
152                                    >
153                                        <Checkbox
154                                            checked={filters.experience.includes(
155                                                level,
156                                            )}
157                                            onCheckedChange={() =>
158                                                toggleArrayValue(
159                                                    'experience',
160                                                    level,
161                                                )
162                                            }
163                                        />
164                                        <Label htmlFor={level}>{level}</Label>
165                                    </div>
166                                ))}
167                            </div>
168                        </div>
169                    </TabsContent>
170                </Tabs>
171                <div className="border-border mt-3 flex justify-end gap-2 border-t pt-2">
172                    <Button variant="ghost" onClick={handleReset}>
173                        Reset
174                    </Button>
175                    <Button onClick={handleApply}>Apply</Button>
176                </div>
177            </PopoverContent>
178        </Popover>
179    )
180}
181
Filter with tabs
filter-01
Files
components/filter.tsx
1'use client'
2
3import * as React from 'react'
4import { Search } from 'lucide-react'
5
6import {
7    Accordion,
8    AccordionContent,
9    AccordionItem,
10    AccordionTrigger,
11} from '@/components/ui/accordion'
12import { Badge } from '@/components/ui/badge'
13import { Button } from '@/components/ui/button'
14import { Checkbox } from '@/components/ui/checkbox'
15import { Input } from '@/components/ui/input'
16import { Label } from '@/components/ui/label'
17import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
18import { Slider } from '@/components/ui/slider'
19
20type Filters = {
21    keyword: string
22    status: string
23    range: number[]
24    options: string[]
25}
26
27const DEFAULT_FILTERS: Filters = {
28    keyword: '',
29    status: '',
30    range: [20, 80],
31    options: [],
32}
33
34export function Filter02() {
35    const [filters, setFilters] = React.useState<Filters>(DEFAULT_FILTERS)
36
37    const toggleOption = (value: string) => {
38        setFilters((prev) => ({
39            ...prev,
40            options: prev.options.includes(value)
41                ? prev.options.filter((v) => v !== value)
42                : [...prev.options, value],
43        }))
44    }
45
46    const handleApply = () => {
47        localStorage.setItem('filters_v3', JSON.stringify(filters))
48    }
49
50    const handleReset = () => {
51        setFilters(DEFAULT_FILTERS)
52        localStorage.removeItem('filters_v3')
53    }
54
55    return (
56        <div className="w-full p-4">
57            <div className="mb-3 flex items-center justify-between">
58                <Badge>Filter Product</Badge>
59            </div>
60
61            <div className="relative mb-3">
62                <Input
63                    placeholder="Search by name"
64                    className="pl-9"
65                    iconLeft={<Search className="h-4 w-4 text-slate-400" />}
66                    value={filters.keyword}
67                    onChange={(e) =>
68                        setFilters((prev) => ({
69                            ...prev,
70                            keyword: e.target.value,
71                        }))
72                    }
73                />
74            </div>
75
76            <Accordion
77                type="single"
78                collapsible
79                defaultValue="status"
80                className="space-y-1"
81            >
82                <AccordionItem value="status">
83                    <AccordionTrigger>Status</AccordionTrigger>
84                    <AccordionContent>
85                        <RadioGroup
86                            value={filters.status}
87                            onValueChange={(value) =>
88                                setFilters((prev) => ({
89                                    ...prev,
90                                    status: value,
91                                }))
92                            }
93                            className="space-y-1"
94                        >
95                            {['New', 'Used', 'Refurbished'].map((item) => (
96                                <div
97                                    key={item}
98                                    className="flex items-center gap-2"
99                                >
100                                    <RadioGroupItem value={item} />
101                                    <Label>{item}</Label>
102                                </div>
103                            ))}
104                        </RadioGroup>
105                    </AccordionContent>
106                </AccordionItem>
107
108                <AccordionItem value="range">
109                    <AccordionTrigger>Range</AccordionTrigger>
110                    <AccordionContent className="space-y-3">
111                        <Slider
112                            value={filters.range}
113                            onValueChange={(value) =>
114                                setFilters((prev) => ({
115                                    ...prev,
116                                    range: value,
117                                }))
118                            }
119                            min={0}
120                            max={100}
121                            step={1}
122                        />
123                        <div className="text-sm text-slate-400">
124                            {filters.range[0]} – {filters.range[1]}
125                        </div>
126                    </AccordionContent>
127                </AccordionItem>
128
129                <AccordionItem value="options">
130                    <AccordionTrigger>Options</AccordionTrigger>
131                    <AccordionContent className="grid grid-cols-3 gap-2">
132                        {[
133                            'Free delivery',
134                            'Premium support',
135                            'Extended warranty',
136                            'Instant activation',
137                            'Auto renewal',
138                        ].map((option) => (
139                            <div
140                                key={option}
141                                className="flex items-center gap-2"
142                            >
143                                <Checkbox
144                                    checked={filters.options.includes(option)}
145                                    onCheckedChange={() => toggleOption(option)}
146                                />
147                                <Label>{option}</Label>
148                            </div>
149                        ))}
150                    </AccordionContent>
151                </AccordionItem>
152            </Accordion>
153
154            <div className="flex justify-end gap-2 pt-3">
155                <Button variant="ghost" onClick={handleReset}>
156                    Reset
157                </Button>
158                <Button onClick={handleApply}>Apply</Button>
159            </div>
160        </div>
161    )
162}
163
Accordion-based filter featuring a slider, radio group, and checkboxes.
filter-02
Files
components/filter.tsx
1'use client'
2
3import * as React from 'react'
4import { ChevronDown } from 'lucide-react'
5
6import { cn } from '@/lib/utils'
7import { Button } from '@/components/ui/button'
8import { Label } from '@/components/ui/label'
9import {
10    Popover,
11    PopoverContent,
12    PopoverTrigger,
13} from '@/components/ui/popover'
14import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
15
16type PriorityKey = 'all' | 'critical' | 'high' | 'medium' | 'low'
17
18const PRIORITIES: {
19    key: PriorityKey
20    label: string
21    count: number
22    color: string
23}[] = [
24    { key: 'all', label: 'All Priorities', count: 128, color: 'bg-gray' },
25    {
26        key: 'critical',
27        label: 'Critical',
28        count: 14,
29        color: 'bg-danger',
30    },
31    { key: 'high', label: 'High', count: 36, color: 'bg-warning' },
32    { key: 'medium', label: 'Medium', count: 52, color: 'bg-blue' },
33    { key: 'low', label: 'Low', count: 26, color: 'bg-success' },
34]
35
36const PRIORITY_COLOR_MAP: Record<PriorityKey, string> = {
37    all: 'bg-gray',
38    critical: 'bg-danger',
39    high: 'bg-warning',
40    medium: 'bg-blue',
41    low: 'bg-success',
42}
43
44export function StatusFilterRadio() {
45    const [open, setOpen] = React.useState(false)
46    const [value, setValue] = React.useState<PriorityKey>('all')
47    const [appliedValue, setAppliedValue] = React.useState<PriorityKey>('all')
48
49    const handleApply = () => {
50        setAppliedValue(value)
51        setOpen(false)
52    }
53
54    const handleReset = () => {
55        setAppliedValue('all')
56        setValue('all')
57    }
58
59    return (
60        <Popover open={open} onOpenChange={setOpen}>
61            <PopoverTrigger asChild>
62                <Button className="gap-2" variant="outline">
63                    <div className="relative ml-auto flex h-2.5 w-2.5">
64                        <span
65                            className={cn(
66                                'absolute inline-flex h-full w-full animate-ping rounded-full opacity-75',
67                                PRIORITY_COLOR_MAP[appliedValue],
68                            )}
69                        ></span>
70                        <span
71                            className={cn(
72                                'relative inline-flex h-2.5 w-2.5 rounded-full',
73                                PRIORITY_COLOR_MAP[appliedValue],
74                            )}
75                        ></span>
76                    </div>
77                    Filter by priority
78                    <ChevronDown className="h-4 w-4" />
79                </Button>
80            </PopoverTrigger>
81
82            <PopoverContent className="rounded-lg">
83                <RadioGroup
84                    value={value}
85                    onValueChange={(v) => setValue(v as PriorityKey)}
86                >
87                    {PRIORITIES.map((priority) => (
88                        <div
89                            key={priority.key}
90                            className="flex items-center justify-between rounded-md hover:bg-slate-700"
91                        >
92                            <div className="flex items-center gap-2">
93                                <RadioGroupItem
94                                    value={priority.key}
95                                    id={priority.key}
96                                />
97                                <span
98                                    className={`h-2.5 w-2.5 rounded-full ${priority.color}`}
99                                />
100                                <Label
101                                    htmlFor={priority.key}
102                                    className="cursor-pointer text-sm"
103                                >
104                                    {priority.label}
105                                </Label>
106                            </div>
107                            <span className="text-xs text-slate-300">
108                                {priority.count}
109                            </span>
110                        </div>
111                    ))}
112                </RadioGroup>
113
114                <div className="border-border mt-3 flex justify-end gap-2 border-t pt-2">
115                    <Button variant="ghost" onClick={handleReset}>
116                        Reset
117                    </Button>
118                    <Button onClick={handleApply}>Apply</Button>
119                </div>
120            </PopoverContent>
121        </Popover>
122    )
123}
124
Filter with current status
filter-03
Files
components/filter.tsx
1'use client'
2
3import * as React from 'react'
4
5import { Button } from '@/components/ui/button'
6import { ButtonGroup } from '@/components/ui/button-group'
7import { Checkbox } from '@/components/ui/checkbox'
8import { Input } from '@/components/ui/input'
9import { Label } from '@/components/ui/label'
10import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
11
12type FilterTab = 'range' | 'category'
13
14type RangeKey =
15    | '100k+'
16    | '10k-100k'
17    | '1k-10k'
18    | '101-1k'
19    | '11-100'
20    | '1-10'
21    | 'custom'
22
23type CategoryKey =
24    | 'electronics'
25    | 'fashion'
26    | 'home'
27    | 'beauty'
28    | 'sports'
29    | 'books'
30
31type Filters = {
32    range: RangeKey | null
33    from: string
34    to: string
35    category: CategoryKey[]
36}
37
38const DEFAULT_FILTERS: Filters = {
39    range: null,
40    from: '',
41    to: '',
42    category: [],
43}
44
45const RANGES: { key: RangeKey; label: string }[] = [
46    { key: '100k+', label: '100,001+' },
47    { key: '10k-100k', label: '10,001 – 100,000' },
48    { key: '1k-10k', label: '1,001 – 10,000' },
49    { key: '101-1k', label: '101 – 1,000' },
50    { key: '11-100', label: '11 – 100' },
51    { key: '1-10', label: '1 – 10' },
52]
53
54const CATEGORIES: { key: CategoryKey; label: string }[] = [
55    { key: 'electronics', label: 'Electronics' },
56    { key: 'fashion', label: 'Fashion' },
57    { key: 'home', label: 'Home & Living' },
58    { key: 'beauty', label: 'Beauty' },
59    { key: 'sports', label: 'Sports & Fitness' },
60    { key: 'books', label: 'Books' },
61]
62
63export function FilterPanel() {
64    const [activeTab, setActiveTab] = React.useState<FilterTab>('range')
65
66    const [filters, setFilters] = React.useState<Filters>(DEFAULT_FILTERS)
67    const [draft, setDraft] = React.useState<Filters>(DEFAULT_FILTERS)
68
69    const toggleCategory = (key: CategoryKey) => {
70        setDraft((prev) => ({
71            ...prev,
72            category: prev.category.includes(key)
73                ? prev.category.filter((k) => k !== key)
74                : [...prev.category, key],
75        }))
76    }
77
78    const handleApply = () => {
79        setFilters(draft)
80    }
81
82    const handleReset = () => {
83        setDraft(DEFAULT_FILTERS)
84        setFilters(DEFAULT_FILTERS)
85    }
86
87    const handleCustomFocus = () => {
88        setDraft((prev) => ({ ...prev, range: 'custom' }))
89    }
90
91    return (
92        <div className="w-full space-y-4 rounded-lg">
93            <ButtonGroup>
94                {(['range', 'category'] as FilterTab[]).map((tab) => (
95                    <Button
96                        key={tab}
97                        variant={activeTab === tab ? 'primary' : 'outline'}
98                        onClick={() => setActiveTab(tab)}
99                    >
100                        {tab === 'range' ? 'Range' : 'Category'}
101                    </Button>
102                ))}
103            </ButtonGroup>
104
105            {activeTab === 'range' && (
106                <div>
107                    <RadioGroup
108                        value={draft.range ?? undefined}
109                        onValueChange={(v) =>
110                            setDraft((prev) => ({
111                                ...prev,
112                                range: v as RangeKey,
113                                from: '',
114                                to: '',
115                            }))
116                        }
117                    >
118                        {RANGES.map((range) => (
119                            <Label
120                                key={range.key}
121                                className="flex cursor-pointer items-center gap-2 rounded-md hover:bg-slate-800"
122                            >
123                                <RadioGroupItem value={range.key} />
124                                {range.label}
125                            </Label>
126                        ))}
127                    </RadioGroup>
128
129                    <div className="pt-2">
130                        <Label className="text-xs text-slate-400">
131                            Custom range
132                        </Label>
133                        <div className="mt-2 flex gap-2">
134                            <Input
135                                type="number"
136                                placeholder="From"
137                                value={draft.from}
138                                onFocus={handleCustomFocus}
139                                onChange={(e) =>
140                                    setDraft((prev) => ({
141                                        ...prev,
142                                        from: e.target.value,
143                                    }))
144                                }
145                            />
146                            <Input
147                                type="number"
148                                placeholder="To"
149                                value={draft.to}
150                                onFocus={handleCustomFocus}
151                                onChange={(e) =>
152                                    setDraft((prev) => ({
153                                        ...prev,
154                                        to: e.target.value,
155                                    }))
156                                }
157                            />
158                        </div>
159                    </div>
160                </div>
161            )}
162
163            {activeTab === 'category' && (
164                <div className="space-y-1">
165                    {CATEGORIES.map((cat) => (
166                        <Label
167                            key={cat.key}
168                            className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 hover:bg-slate-800"
169                        >
170                            <Checkbox
171                                checked={draft.category.includes(cat.key)}
172                                onCheckedChange={() => toggleCategory(cat.key)}
173                            />
174                            {cat.label}
175                        </Label>
176                    ))}
177                </div>
178            )}
179
180            <div className="border-border mt-3 flex justify-end gap-2 border-t pt-2">
181                <Button variant="ghost" onClick={handleReset}>
182                    Reset
183                </Button>
184                <Button onClick={handleApply}>Apply</Button>
185            </div>
186        </div>
187    )
188}
189
Filter with button group
filter-04
Files
components/filter.tsx
1'use client'
2
3import * as React from 'react'
4import { Check, ChevronDown, Trash2, X } from 'lucide-react'
5
6import { Button } from '@/components/ui/button'
7import {
8    DropdownMenu,
9    DropdownMenuContent,
10    DropdownMenuItem,
11    DropdownMenuTrigger,
12} from '@/components/ui/dropdown-menu'
13import { Input } from '@/components/ui/input'
14import {
15    Popover,
16    PopoverContent,
17    PopoverTrigger,
18} from '@/components/ui/popover'
19
20type FilterRow = {
21    scope: string
22    criteria: string
23    match: string
24    value: string
25}
26
27const DEFAULT_ROWS: FilterRow[] = [
28    {
29        scope: 'Location',
30        criteria: 'Germany',
31        match: 'Is',
32        value: '',
33    },
34    {
35        scope: 'Traffic Source',
36        criteria: 'Referring site',
37        match: 'Exactly matches',
38        value: '',
39    },
40]
41
42const SCOPE_OPTIONS = ['Location', 'Traffic Source', 'Device']
43const CRITERIA_OPTIONS = ['Germany', 'Canada', 'Referring site', 'Direct visit']
44const MATCH_OPTIONS = ['Is', 'Is not', 'Exactly matches']
45
46export function FacetFilter() {
47    const [rows, setRows] = React.useState<FilterRow[]>(DEFAULT_ROWS)
48    const [open, setOpen] = React.useState(false)
49
50    const updateRow = (index: number, key: keyof FilterRow, value: string) => {
51        setRows((prev) =>
52            prev.map((row, i) =>
53                i === index ? { ...row, [key]: value } : row,
54            ),
55        )
56    }
57
58    const addRow = () => {
59        setRows((prev) => [
60            ...prev,
61            { scope: '', criteria: '', match: '', value: '' },
62        ])
63    }
64
65    const removeRow = (index: number) => {
66        setRows((prev) => prev.filter((_, i) => i !== index))
67    }
68
69    const clearAll = () => setRows([])
70
71    return (
72        <Popover open={open} onOpenChange={setOpen}>
73            <PopoverTrigger asChild>
74                <Button className="gap-2">
75                    Filter results
76                    <ChevronDown className="h-4 w-4" />
77                </Button>
78            </PopoverTrigger>
79
80            <PopoverContent className="w-full space-y-2 rounded-lg">
81                <div className="space-y-3">
82                    {rows.map((row, index) => (
83                        <div
84                            key={index}
85                            className="grid grid-cols-[160px_200px_160px_1fr_40px] gap-2"
86                        >
87                            <FilterDropdown
88                                label={row.scope || 'Select scope'}
89                                options={SCOPE_OPTIONS}
90                                value={row.scope}
91                                onSelect={(v) => updateRow(index, 'scope', v)}
92                            />
93
94                            <FilterDropdown
95                                label={row.criteria || 'Select criteria'}
96                                options={CRITERIA_OPTIONS}
97                                value={row.criteria}
98                                onSelect={(v) =>
99                                    updateRow(index, 'criteria', v)
100                                }
101                            />
102
103                            <FilterDropdown
104                                label={row.match || 'Match type'}
105                                options={MATCH_OPTIONS}
106                                value={row.match}
107                                onSelect={(v) => updateRow(index, 'match', v)}
108                            />
109
110                            <Input
111                                placeholder="Enter value"
112                                value={row.value}
113                                onChange={(e) =>
114                                    updateRow(index, 'value', e.target.value)
115                                }
116                            />
117
118                            <Button
119                                onClick={() => removeRow(index)}
120                                variant="ghost"
121                                className="hover:text-danger"
122                            >
123                                <Trash2 size={16} />
124                            </Button>
125                        </div>
126                    ))}
127
128                    <Button onClick={addRow}>+ Add filter</Button>
129                </div>
130
131                <div className="border-border flex items-center justify-end gap-2 border-t pt-2">
132                    <Button onClick={clearAll} variant="outline">
133                        <X className="h-4 w-4" />
134                        Clear all
135                    </Button>
136                    <Button onClick={() => setOpen(false)}>Apply</Button>
137                </div>
138            </PopoverContent>
139        </Popover>
140    )
141}
142
143function FilterDropdown({
144    label,
145    options,
146    value,
147    onSelect,
148}: {
149    label: string
150    options: string[]
151    value: string
152    onSelect: (v: string) => void
153}) {
154    return (
155        <DropdownMenu modal={false}>
156            <DropdownMenuTrigger asChild>
157                <Button variant="secondary" className="w-full justify-between">
158                    {label}
159                    <ChevronDown className="h-4 w-4 opacity-60" />
160                </Button>
161            </DropdownMenuTrigger>
162
163            <DropdownMenuContent className="w-[200px]">
164                {options.map((option) => (
165                    <DropdownMenuItem
166                        key={option}
167                        onClick={() => onSelect(option)}
168                        className="flex items-center justify-between"
169                    >
170                        {option}
171                        {value === option && (
172                            <Check className="h-4 w-4 text-blue-500" />
173                        )}
174                    </DropdownMenuItem>
175                ))}
176            </DropdownMenuContent>
177        </DropdownMenu>
178    )
179}
180
Filter with contextual popover
filter-05
Files
components/filter.tsx
1'use client'
2
3import * as React from 'react'
4import { Check, ChevronDown } from 'lucide-react'
5
6import { Button } from '@/components/ui/button'
7import {
8    DropdownMenu,
9    DropdownMenuContent,
10    DropdownMenuItem,
11    DropdownMenuTrigger,
12} from '@/components/ui/dropdown-menu'
13import { Input } from '@/components/ui/input'
14import { Label } from '@/components/ui/label'
15import {
16    Popover,
17    PopoverContent,
18    PopoverTrigger,
19} from '@/components/ui/popover'
20import { Switch } from '@/components/ui/switch'
21
22const STATUS_OPTIONS = ['Active', 'Paused', 'Archived']
23const PERIOD_OPTIONS = ['Last 7 days', 'Last 30 days', 'Custom range']
24
25export function ActivityFilter() {
26    const [open, setOpen] = React.useState(false)
27
28    const [status, setStatus] = React.useState<string[]>([])
29    const [period, setPeriod] = React.useState('')
30    const [minCount, setMinCount] = React.useState('')
31    const [maxCount, setMaxCount] = React.useState('')
32
33    const toggleStatus = (value: string) => {
34        setStatus((prev) =>
35            prev.includes(value)
36                ? prev.filter((s) => s !== value)
37                : [...prev, value],
38        )
39    }
40
41    const resetAll = () => {
42        setStatus([])
43        setPeriod('')
44        setMinCount('')
45        setMaxCount('')
46    }
47
48    return (
49        <Popover open={open} onOpenChange={setOpen}>
50            <PopoverTrigger asChild>
51                <Button className="gap-2">
52                    Filter activity
53                    <ChevronDown className="h-4 w-4" />
54                </Button>
55            </PopoverTrigger>
56
57            <PopoverContent className="w-[250px] space-y-4 rounded-lg sm:w-full">
58                <section>
59                    <p className="text-sm font-medium">Status</p>
60
61                    <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3">
62                        {STATUS_OPTIONS.map((s) => {
63                            const checked = status.includes(s)
64
65                            return (
66                                <div
67                                    key={s}
68                                    className="flex items-center justify-between rounded-md px-3 py-2"
69                                >
70                                    <Label className="text-sm">{s}</Label>
71
72                                    <Switch
73                                        checked={checked}
74                                        onCheckedChange={() => toggleStatus(s)}
75                                    />
76                                </div>
77                            )
78                        })}
79                    </div>
80                </section>
81
82                <section className="space-y-2">
83                    <p className="text-sm font-medium">Time period</p>
84                    <DropdownMenu>
85                        <DropdownMenuTrigger asChild>
86                            <Button
87                                variant="secondary"
88                                className="w-full justify-between"
89                            >
90                                {period || 'Select period'}
91                                <ChevronDown className="h-4 w-4 opacity-60" />
92                            </Button>
93                        </DropdownMenuTrigger>
94
95                        <DropdownMenuContent className="w-full">
96                            {PERIOD_OPTIONS.map((p) => (
97                                <DropdownMenuItem
98                                    key={p}
99                                    onClick={() => setPeriod(p)}
100                                    className="flex justify-between"
101                                >
102                                    {p}
103                                    {period === p && (
104                                        <Check className="h-4 w-4 text-blue-500" />
105                                    )}
106                                </DropdownMenuItem>
107                            ))}
108                        </DropdownMenuContent>
109                    </DropdownMenu>
110                </section>
111
112                <section className="space-y-2">
113                    <p className="text-sm font-medium">Volume</p>
114                    <div className="flex gap-2">
115                        <Input
116                            placeholder="Min"
117                            value={minCount}
118                            onChange={(e) => setMinCount(e.target.value)}
119                        />
120                        <Input
121                            placeholder="Max"
122                            value={maxCount}
123                            onChange={(e) => setMaxCount(e.target.value)}
124                        />
125                    </div>
126                </section>
127
128                <div className="border-border flex items-center justify-end border-t pt-3">
129                    <Button variant="ghost" onClick={resetAll}>
130                        Reset
131                    </Button>
132
133                    <Button onClick={() => setOpen(false)}>Apply</Button>
134                </div>
135            </PopoverContent>
136        </Popover>
137    )
138}
139
Filter with contextual popover
filter-06