Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions app/(dashboard)/tasks/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Suspense } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Plus, Search } from "lucide-react"
import { Plus } from "lucide-react"
import Link from "next/link"
import { TaskList } from "@/components/task-list"
import { TaskListWithSearch } from "@/components/task-list-with-search"
import { poppins } from "@/lib/fonts"

import { getAllTasks } from "@/app/(dashboard)/tasks/actions"
Expand Down Expand Up @@ -31,7 +30,7 @@ export default async function TasksPage() {
</div>

<Suspense fallback={<div>Loading tasks...</div>}>
<TaskList initialTasks={tasks || []} />
<TaskListWithSearch initialTasks={tasks || []} />
</Suspense>
</div>
)
Expand Down
128 changes: 128 additions & 0 deletions components/search-and-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"use client"

import { useState } from "react"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Search, X, Filter } from "lucide-react"

export type FilterOptions = {
status: string[]
priority: string[]
}

type SearchAndFilterProps = {
searchText: string
onSearchChange: (text: string) => void
filterOptions: FilterOptions
onFilterChange: (filters: FilterOptions) => void
}

const STATUS_OPTIONS = ["todo", "in_progress", "review", "done"]
const PRIORITY_OPTIONS = ["high", "medium", "low"]

const STATUS_LABELS = {
todo: "Todo",
in_progress: "In progress",
review: "Review",
done: "Done"
}

const PRIORITY_LABELS = {
high: "High",
medium: "Medium",
low: "Low"
}

export function SearchAndFilter({
searchText,
onSearchChange,
filterOptions,
onFilterChange
}: SearchAndFilterProps) {
const handleClearSearch = () => {
onSearchChange("")
}

const handleStatusToggle = (status: string, checked: boolean) => {
const newStatus = checked
? [...filterOptions.status, status]
: filterOptions.status.filter(s => s !== status)

onFilterChange({
...filterOptions,
status: newStatus
})
}

const handlePriorityToggle = (priority: string, checked: boolean) => {
const newPriority = checked
? [...filterOptions.priority, priority]
: filterOptions.priority.filter(p => p !== priority)

onFilterChange({
...filterOptions,
priority: newPriority
})
}

return (
<div className="flex items-center space-x-2 mb-6">
{/* Search Input */}
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search tasks..."
value={searchText}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10 pr-10"
/>
{searchText && (
<Button
variant="ghost"
size="sm"
onClick={handleClearSearch}
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-8 w-8 p-0 hover:bg-transparent"
>
<X className="h-4 w-4" />
</Button>
)}
</div>

{/* Filter Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<Filter className="h-4 w-4 mr-2" />
Filter
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Status</DropdownMenuLabel>
{STATUS_OPTIONS.map((status) => (
<DropdownMenuCheckboxItem
key={status}
checked={filterOptions.status.includes(status)}
onCheckedChange={(checked) => handleStatusToggle(status, checked)}
>
{STATUS_LABELS[status as keyof typeof STATUS_LABELS]}
</DropdownMenuCheckboxItem>
))}

<DropdownMenuSeparator />

<DropdownMenuLabel>Priority</DropdownMenuLabel>
{PRIORITY_OPTIONS.map((priority) => (
<DropdownMenuCheckboxItem
key={priority}
checked={filterOptions.priority.includes(priority)}
onCheckedChange={(checked) => handlePriorityToggle(priority, checked)}
>
{PRIORITY_LABELS[priority as keyof typeof PRIORITY_LABELS]}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
70 changes: 70 additions & 0 deletions components/task-list-with-search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client"

import { useState, useMemo } from "react"
import { TaskList } from "./task-list"
import { SearchAndFilter, FilterOptions } from "./search-and-filter"
import { Search } from "lucide-react"

import type { Task as PrismaTask, User } from "@/app/generated/prisma/client"

type TaskWithProfile = PrismaTask & {
assignee?: Pick<User, "name"> | null;
}

type TaskListWithSearchProps = {
initialTasks: TaskWithProfile[]
}

export function TaskListWithSearch({ initialTasks }: TaskListWithSearchProps) {
const [searchText, setSearchText] = useState("")
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
status: ["todo", "in_progress", "review", "done"], // All selected by default
priority: ["high", "medium", "low"] // All selected by default
})

const filteredTasks = useMemo(() => {
return initialTasks.filter((task) => {
// Check if task matches search text
const matchesSearch = searchText === "" ||
task.name.toLowerCase().includes(searchText.toLowerCase()) ||
task.description.toLowerCase().includes(searchText.toLowerCase())

// Check if task matches status filter
const matchesStatus = filterOptions.status.includes(task.status)

// Check if task matches priority filter
const matchesPriority = filterOptions.priority.includes(task.priority)

return matchesSearch && matchesStatus && matchesPriority
})
}, [initialTasks, searchText, filterOptions])

const hasFilters = searchText !== "" ||
filterOptions.status.length < 4 ||
filterOptions.priority.length < 3

return (
<div>
<SearchAndFilter
searchText={searchText}
onSearchChange={setSearchText}
filterOptions={filterOptions}
onFilterChange={setFilterOptions}
/>

{filteredTasks.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="mb-4">
<Search className="h-12 w-12 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold mb-2">No tasks found</h3>
<p className="text-muted-foreground max-w-md">
No tasks match your current search and filter criteria. Try adjusting your search terms or filter settings.
</p>
</div>
) : (
<TaskList initialTasks={filteredTasks} />
)}
</div>
)
}