Skip to content

Commit 1f7e748

Browse files
committed
chore: improve filters
1 parent 85fc3cd commit 1f7e748

File tree

1 file changed

+278
-41
lines changed

1 file changed

+278
-41
lines changed

apps/client/pages/issues/index.tsx

Lines changed: 278 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import useTranslation from "next-translate/useTranslation";
22
import { useRouter } from "next/router";
33
import Loader from "react-spinners/ClipLoader";
4-
import { useState } from "react";
4+
import { useState, useMemo } from "react";
55

66
import { ContextMenu } from "@radix-ui/themes";
77
import { getCookie } from "cookies-next";
@@ -10,7 +10,7 @@ import Link from "next/link";
1010
import { useQuery } from "react-query";
1111
import { useUser } from "../../store/session";
1212
import { Popover, PopoverContent, PopoverTrigger } from "@/shadcn/ui/popover";
13-
import { CheckIcon, PlusCircle } from "lucide-react";
13+
import { CheckIcon, Filter, PlusCircle, X } from "lucide-react";
1414
import { Button } from "@/shadcn/ui/button";
1515
import {
1616
Command,
@@ -32,6 +32,28 @@ async function getUserTickets(token: any) {
3232
return res.json();
3333
}
3434

35+
// Add this new component for the filter badge
36+
const FilterBadge = ({
37+
text,
38+
onRemove,
39+
}: {
40+
text: string;
41+
onRemove: () => void;
42+
}) => (
43+
<div className="flex items-center gap-1 bg-accent rounded-md px-2 py-1 text-xs">
44+
<span>{text}</span>
45+
<button
46+
onClick={(e) => {
47+
e.preventDefault();
48+
onRemove();
49+
}}
50+
className="hover:bg-muted rounded-full p-0.5"
51+
>
52+
<X className="h-3 w-3" />
53+
</button>
54+
</div>
55+
);
56+
3557
export default function Tickets() {
3658
const router = useRouter();
3759
const { t } = useTranslation("peppermint");
@@ -47,7 +69,10 @@ export default function Tickets() {
4769
const low = "bg-blue-100 text-blue-800";
4870
const normal = "bg-green-100 text-green-800";
4971

72+
const [filterSelected, setFilterSelected] = useState();
5073
const [selectedPriorities, setSelectedPriorities] = useState<string[]>([]);
74+
const [selectedStatuses, setSelectedStatuses] = useState<string[]>([]);
75+
const [selectedAssignees, setSelectedAssignees] = useState<string[]>([]);
5176

5277
const handlePriorityToggle = (priority: string) => {
5378
setSelectedPriorities((prev) =>
@@ -57,14 +82,65 @@ export default function Tickets() {
5782
);
5883
};
5984

85+
const handleStatusToggle = (status: string) => {
86+
setSelectedStatuses((prev) =>
87+
prev.includes(status)
88+
? prev.filter((s) => s !== status)
89+
: [...prev, status]
90+
);
91+
};
92+
93+
const handleAssigneeToggle = (assignee: string) => {
94+
setSelectedAssignees((prev) =>
95+
prev.includes(assignee)
96+
? prev.filter((a) => a !== assignee)
97+
: [...prev, assignee]
98+
);
99+
};
100+
60101
const filteredTickets = data
61-
? data.tickets.filter((ticket) =>
62-
selectedPriorities.length > 0
63-
? selectedPriorities.includes(ticket.priority)
64-
: true
65-
)
102+
? data.tickets.filter((ticket) => {
103+
const priorityMatch =
104+
selectedPriorities.length === 0 ||
105+
selectedPriorities.includes(ticket.priority);
106+
const statusMatch =
107+
selectedStatuses.length === 0 ||
108+
selectedStatuses.includes(ticket.isComplete ? "closed" : "open");
109+
const assigneeMatch =
110+
selectedAssignees.length === 0 ||
111+
selectedAssignees.includes(ticket.assignedTo?.name || "Unassigned");
112+
113+
return priorityMatch && statusMatch && assigneeMatch;
114+
})
66115
: [];
67116

117+
type FilterType = "priority" | "status" | "assignee" | null;
118+
const [activeFilter, setActiveFilter] = useState<FilterType>(null);
119+
const [filterSearch, setFilterSearch] = useState("");
120+
121+
const filteredPriorities = useMemo(() => {
122+
const priorities = ["low", "medium", "high"];
123+
return priorities.filter((priority) =>
124+
priority.toLowerCase().includes(filterSearch.toLowerCase())
125+
);
126+
}, [filterSearch]);
127+
128+
const filteredStatuses = useMemo(() => {
129+
const statuses = ["open", "closed"];
130+
return statuses.filter((status) =>
131+
status.toLowerCase().includes(filterSearch.toLowerCase())
132+
);
133+
}, [filterSearch]);
134+
135+
const filteredAssignees = useMemo(() => {
136+
const assignees = data?.tickets
137+
.map((t) => t.assignedTo?.name || "Unassigned")
138+
.filter((name, index, self) => self.indexOf(name) === index);
139+
return assignees?.filter((assignee) =>
140+
assignee.toLowerCase().includes(filterSearch.toLowerCase())
141+
);
142+
}, [data?.tickets, filterSearch]);
143+
68144
return (
69145
<div>
70146
{status === "loading" && (
@@ -76,60 +152,221 @@ export default function Tickets() {
76152
{status === "success" && (
77153
<div>
78154
<div className="flex flex-col">
79-
<div className="py-2 px-6 bg-gray-200 dark:bg-[#0A090C] border-b-[1px] flex flex-row items-center justify-between">
80-
<div className="flex flex-row items-center space-x-4">
81-
<span className="text-sm font-bold">All Tickets</span>
155+
<div className="py-2 px-3 bg-background border-b-[1px] flex flex-row items-center justify-between">
156+
<div className="flex flex-row items-center gap-2">
82157
<Popover>
83158
<PopoverTrigger asChild>
84159
<Button
85-
variant="outline"
160+
variant="ghost"
86161
size="sm"
87-
className="h-6 bg-transparent border-dashed"
162+
className="h-6 bg-transparent"
88163
>
89-
<PlusCircle className="mr-2 h-4 w-4" />
90-
Priority
164+
<Filter className="mr-2 h-4 w-4" />
165+
<span className="hidden sm:block">Filters</span>
91166
</Button>
92167
</PopoverTrigger>
93-
<PopoverContent className="w-[200px] p-0" align="start">
94-
<Command>
95-
<CommandInput placeholder="Search priority..." />
96-
<CommandList>
97-
<CommandEmpty>No results found.</CommandEmpty>
98-
<CommandGroup>
99-
{["low", "medium", "high"].map((priority) => (
168+
<PopoverContent className="w-[300px] p-0" align="start">
169+
{!activeFilter ? (
170+
<Command>
171+
<CommandInput placeholder="Search filters..." />
172+
<CommandList>
173+
<CommandEmpty>No results found.</CommandEmpty>
174+
<CommandGroup>
175+
<CommandItem
176+
onSelect={() => setActiveFilter("priority")}
177+
>
178+
Priority
179+
</CommandItem>
180+
<CommandItem
181+
onSelect={() => setActiveFilter("status")}
182+
>
183+
Status
184+
</CommandItem>
185+
<CommandItem
186+
onSelect={() => setActiveFilter("assignee")}
187+
>
188+
Assigned To
189+
</CommandItem>
190+
</CommandGroup>
191+
</CommandList>
192+
</Command>
193+
) : activeFilter === "priority" ? (
194+
<Command>
195+
<CommandInput
196+
placeholder="Search priority..."
197+
value={filterSearch}
198+
onValueChange={setFilterSearch}
199+
/>
200+
<CommandList>
201+
<CommandEmpty>No priorities found.</CommandEmpty>
202+
<CommandGroup heading="Priority">
203+
{filteredPriorities.map((priority) => (
204+
<CommandItem
205+
key={priority}
206+
onSelect={() => handlePriorityToggle(priority)}
207+
>
208+
<div
209+
className={cn(
210+
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
211+
selectedPriorities.includes(priority)
212+
? "bg-primary text-primary-foreground"
213+
: "opacity-50 [&_svg]:invisible"
214+
)}
215+
>
216+
<CheckIcon className={cn("h-4 w-4")} />
217+
</div>
218+
<span className="capitalize">{priority}</span>
219+
</CommandItem>
220+
))}
221+
</CommandGroup>
222+
<CommandSeparator />
223+
<CommandGroup>
100224
<CommandItem
101-
key={priority}
102-
onSelect={() => handlePriorityToggle(priority)}
225+
onSelect={() => {
226+
setActiveFilter(null);
227+
setFilterSearch("");
228+
}}
229+
className="justify-center text-center"
103230
>
104-
<div
105-
className={cn(
106-
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
107-
selectedPriorities.includes(priority)
108-
? "bg-primary text-primary-foreground"
109-
: "opacity-50 [&_svg]:invisible"
110-
)}
231+
Back to filters
232+
</CommandItem>
233+
</CommandGroup>
234+
</CommandList>
235+
</Command>
236+
) : activeFilter === "status" ? (
237+
<Command>
238+
<CommandInput
239+
placeholder="Search status..."
240+
value={filterSearch}
241+
onValueChange={setFilterSearch}
242+
/>
243+
<CommandList>
244+
<CommandEmpty>No statuses found.</CommandEmpty>
245+
<CommandGroup heading="Status">
246+
{filteredStatuses.map((status) => (
247+
<CommandItem
248+
key={status}
249+
onSelect={() => handleStatusToggle(status)}
111250
>
112-
<CheckIcon className={cn("h-4 w-4")} />
113-
</div>
114-
<span className="capitalize">{priority}</span>
251+
<div
252+
className={cn(
253+
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
254+
selectedStatuses.includes(status)
255+
? "bg-primary text-primary-foreground"
256+
: "opacity-50 [&_svg]:invisible"
257+
)}
258+
>
259+
<CheckIcon className={cn("h-4 w-4")} />
260+
</div>
261+
<span className="capitalize">{status}</span>
262+
</CommandItem>
263+
))}
264+
</CommandGroup>
265+
<CommandSeparator />
266+
<CommandGroup>
267+
<CommandItem
268+
onSelect={() => {
269+
setActiveFilter(null);
270+
setFilterSearch("");
271+
}}
272+
className="justify-center text-center"
273+
>
274+
Back to filters
115275
</CommandItem>
116-
))}
117-
</CommandGroup>
118-
<>
276+
</CommandGroup>
277+
</CommandList>
278+
</Command>
279+
) : activeFilter === "assignee" ? (
280+
<Command>
281+
<CommandInput
282+
placeholder="Search assignee..."
283+
value={filterSearch}
284+
onValueChange={setFilterSearch}
285+
/>
286+
<CommandList>
287+
<CommandEmpty>No assignees found.</CommandEmpty>
288+
<CommandGroup heading="Assigned To">
289+
{filteredAssignees?.map((name) => (
290+
<CommandItem
291+
key={name}
292+
onSelect={() => handleAssigneeToggle(name)}
293+
>
294+
<div
295+
className={cn(
296+
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
297+
selectedAssignees.includes(name)
298+
? "bg-primary text-primary-foreground"
299+
: "opacity-50 [&_svg]:invisible"
300+
)}
301+
>
302+
<CheckIcon className={cn("h-4 w-4")} />
303+
</div>
304+
<span>{name}</span>
305+
</CommandItem>
306+
))}
307+
</CommandGroup>
119308
<CommandSeparator />
120309
<CommandGroup>
121310
<CommandItem
122-
onSelect={() => setSelectedPriorities([])}
311+
onSelect={() => {
312+
setActiveFilter(null);
313+
setFilterSearch("");
314+
}}
123315
className="justify-center text-center"
124316
>
125-
Clear filters
317+
Back to filters
126318
</CommandItem>
127319
</CommandGroup>
128-
</>
129-
</CommandList>
130-
</Command>
320+
</CommandList>
321+
</Command>
322+
) : null}
131323
</PopoverContent>
132324
</Popover>
325+
326+
{/* Display selected filters */}
327+
<div className="flex flex-wrap gap-2">
328+
{selectedPriorities.map((priority) => (
329+
<FilterBadge
330+
key={`priority-${priority}`}
331+
text={`Priority: ${priority}`}
332+
onRemove={() => handlePriorityToggle(priority)}
333+
/>
334+
))}
335+
336+
{selectedStatuses.map((status) => (
337+
<FilterBadge
338+
key={`status-${status}`}
339+
text={`Status: ${status}`}
340+
onRemove={() => handleStatusToggle(status)}
341+
/>
342+
))}
343+
344+
{selectedAssignees.map((assignee) => (
345+
<FilterBadge
346+
key={`assignee-${assignee}`}
347+
text={`Assignee: ${assignee}`}
348+
onRemove={() => handleAssigneeToggle(assignee)}
349+
/>
350+
))}
351+
352+
{/* Clear all filters button - only show if there are filters */}
353+
{(selectedPriorities.length > 0 ||
354+
selectedStatuses.length > 0 ||
355+
selectedAssignees.length > 0) && (
356+
<Button
357+
variant="ghost"
358+
size="sm"
359+
className="h-6 px-2 text-xs"
360+
onClick={() => {
361+
setSelectedPriorities([]);
362+
setSelectedStatuses([]);
363+
setSelectedAssignees([]);
364+
}}
365+
>
366+
Clear all
367+
</Button>
368+
)}
369+
</div>
133370
</div>
134371
<div></div>
135372
</div>

0 commit comments

Comments
 (0)