Skip to content

Commit c4f2bfa

Browse files
Copilothotlong
andcommitted
feat: add FilterDrawer and FilterButton components for list view filtering
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 32386cf commit c4f2bfa

2 files changed

Lines changed: 186 additions & 0 deletions

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import React, { useCallback, useMemo } from "react";
2+
import { View, Text, Pressable, Modal } from "react-native";
3+
import { Filter, X } from "lucide-react-native";
4+
import { cn } from "~/lib/utils";
5+
import { QueryBuilder } from "~/components/query";
6+
import { useQueryBuilder } from "~/hooks/useQueryBuilder";
7+
import type { FieldDefinition } from "~/components/renderers/types";
8+
9+
/* ------------------------------------------------------------------ */
10+
/* FilterDrawer Props */
11+
/* ------------------------------------------------------------------ */
12+
13+
export interface FilterDrawerProps {
14+
/** Available field definitions for filtering */
15+
fields: FieldDefinition[];
16+
/** Whether the drawer is visible */
17+
visible: boolean;
18+
/** Called when the drawer should close */
19+
onClose: () => void;
20+
/** Called with the serialised filter when the user taps Apply */
21+
onApply: (filter: unknown) => void;
22+
}
23+
24+
/* ------------------------------------------------------------------ */
25+
/* FilterDrawer */
26+
/* ------------------------------------------------------------------ */
27+
28+
/**
29+
* Modal overlay that wraps the QueryBuilder, providing Apply / Clear
30+
* actions and a header with a close button.
31+
*/
32+
export function FilterDrawer({
33+
fields,
34+
visible,
35+
onClose,
36+
onApply,
37+
}: FilterDrawerProps) {
38+
const {
39+
root,
40+
addFilter,
41+
updateFilter,
42+
removeFilter,
43+
toggleRootLogic,
44+
clearFilters,
45+
serialize,
46+
hasFilters,
47+
} = useQueryBuilder();
48+
49+
/* ---- Active filter count ---- */
50+
const filterCount = root.filters.length;
51+
52+
/* ---- Handlers ---- */
53+
const handleApply = useCallback(() => {
54+
onApply(serialize());
55+
onClose();
56+
}, [onApply, onClose, serialize]);
57+
58+
const handleClear = useCallback(() => {
59+
clearFilters();
60+
onApply(null);
61+
onClose();
62+
}, [clearFilters, onApply, onClose]);
63+
64+
return (
65+
<Modal
66+
visible={visible}
67+
animationType="slide"
68+
transparent
69+
onRequestClose={onClose}
70+
>
71+
<View className="flex-1 justify-end bg-black/40">
72+
<View className="max-h-[85%] rounded-t-2xl bg-background">
73+
{/* ---- Header ---- */}
74+
<View className="flex-row items-center justify-between border-b border-border px-4 py-3">
75+
<View className="flex-row items-center">
76+
<Filter size={18} color="#1e40af" />
77+
<Text className="ml-2 text-lg font-semibold text-foreground">
78+
Filters
79+
</Text>
80+
{filterCount > 0 && (
81+
<View className="ml-2 min-w-[20px] items-center rounded-full bg-primary px-1.5 py-0.5">
82+
<Text className="text-[10px] font-bold text-primary-foreground">
83+
{filterCount}
84+
</Text>
85+
</View>
86+
)}
87+
</View>
88+
89+
<Pressable
90+
onPress={onClose}
91+
className="rounded-lg p-1.5 active:bg-muted"
92+
>
93+
<X size={20} color="#64748b" />
94+
</Pressable>
95+
</View>
96+
97+
{/* ---- Query builder ---- */}
98+
<View className="flex-1 px-4 py-4">
99+
<QueryBuilder
100+
root={root}
101+
fields={fields}
102+
onAddFilter={addFilter}
103+
onUpdateFilter={updateFilter}
104+
onRemoveFilter={removeFilter}
105+
onToggleLogic={toggleRootLogic}
106+
onClear={clearFilters}
107+
/>
108+
</View>
109+
110+
{/* ---- Footer actions ---- */}
111+
<View className="flex-row gap-3 border-t border-border px-4 py-4">
112+
<Pressable
113+
onPress={handleClear}
114+
className="flex-1 items-center rounded-xl border border-border bg-card py-3 active:bg-muted/50"
115+
>
116+
<Text className="text-sm font-semibold text-muted-foreground">
117+
Clear
118+
</Text>
119+
</Pressable>
120+
121+
<Pressable
122+
onPress={handleApply}
123+
className="flex-1 items-center rounded-xl bg-primary py-3 active:bg-primary/80"
124+
>
125+
<Text className="text-sm font-semibold text-primary-foreground">
126+
Apply
127+
</Text>
128+
</Pressable>
129+
</View>
130+
</View>
131+
</View>
132+
</Modal>
133+
);
134+
}
135+
136+
/* ------------------------------------------------------------------ */
137+
/* FilterButton */
138+
/* ------------------------------------------------------------------ */
139+
140+
export interface FilterButtonProps {
141+
/** Number of currently active filters */
142+
count?: number;
143+
/** Press handler (typically toggles the FilterDrawer) */
144+
onPress: () => void;
145+
className?: string;
146+
}
147+
148+
/**
149+
* Compact filter icon button with an optional active-filter badge.
150+
*/
151+
export function FilterButton({ count = 0, onPress, className }: FilterButtonProps) {
152+
const isActive = count > 0;
153+
154+
return (
155+
<Pressable
156+
onPress={onPress}
157+
className={cn(
158+
"relative flex-row items-center rounded-lg border px-3 py-2",
159+
isActive ? "border-primary bg-primary/10" : "border-border bg-card",
160+
className,
161+
)}
162+
>
163+
<Filter size={16} color={isActive ? "#1e40af" : "#64748b"} />
164+
<Text
165+
className={cn(
166+
"ml-1.5 text-xs font-medium",
167+
isActive ? "text-primary" : "text-muted-foreground",
168+
)}
169+
>
170+
Filter
171+
</Text>
172+
173+
{isActive && (
174+
<View className="absolute -right-1.5 -top-1.5 min-w-[18px] items-center rounded-full bg-primary px-1 py-0.5">
175+
<Text className="text-[10px] font-bold text-primary-foreground">
176+
{count}
177+
</Text>
178+
</View>
179+
)}
180+
</Pressable>
181+
);
182+
}

components/renderers/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export type { DetailViewRendererProps } from "./DetailViewRenderer";
1515
export { DashboardViewRenderer } from "./DashboardViewRenderer";
1616
export type { DashboardViewRendererProps, WidgetDataPayload } from "./DashboardViewRenderer";
1717

18+
// Filter drawer
19+
export { FilterDrawer, FilterButton } from "./FilterDrawer";
20+
export type { FilterDrawerProps, FilterButtonProps } from "./FilterDrawer";
21+
1822
// Field renderer
1923
export { FieldRenderer, formatDisplayValue } from "./fields/FieldRenderer";
2024
export type { FieldRendererProps } from "./fields/FieldRenderer";

0 commit comments

Comments
 (0)