Skip to content
Closed
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 { TasksPageClient } from "@/components/tasks-page-client"
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 || []} />
<TasksPageClient initialTasks={tasks || []} />
</Suspense>
</div>
)
Expand Down
58 changes: 58 additions & 0 deletions components/tasks-page-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client"

import { useState, useMemo } from "react"
import { TaskList } from "@/components/task-list"
import { TasksSearchFilter, type FilterOptions } from "@/components/tasks-search-filter"
import type { Task as PrismaTask, User } from "@/app/generated/prisma/client"

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

type TasksPageClientProps = {
initialTasks: TaskWithProfile[]
}

export function TasksPageClient({ initialTasks }: TasksPageClientProps) {
const [searchQuery, setSearchQuery] = useState("")
const [filters, setFilters] = useState<FilterOptions>({
statuses: ["todo", "in_progress", "review", "done"],
priorities: ["high", "medium", "low"],
})
Comment on lines +18 to +21
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The initial filter values are hardcoded here and duplicated from STATUS_OPTIONS and PRIORITY_OPTIONS in tasks-search-filter.tsx. Consider deriving these values from the constants to ensure consistency:

import { TasksSearchFilter, type FilterOptions } from "@/components/tasks-search-filter"

// At the top of the file after imports, export the constants from tasks-search-filter
// or define them in a shared location

const [filters, setFilters] = useState<FilterOptions>({
  statuses: STATUS_OPTIONS.map(o => o.value),
  priorities: PRIORITY_OPTIONS.map(o => o.value),
})

This ensures that if the filter options change, the defaults automatically stay in sync.

Copilot uses AI. Check for mistakes.

const filteredTasks = useMemo(() => {
return initialTasks.filter((task) => {
// Apply search filter
const matchesSearch =
searchQuery.trim() === "" ||
task.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(task.description?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false)

// Apply status filter
const matchesStatus = filters.statuses.includes(task.status)

// Apply priority filter
const matchesPriority = filters.priorities.includes(task.priority)

return matchesSearch && matchesStatus && matchesPriority
})
}, [initialTasks, searchQuery, filters])

return (
<div className="space-y-4">
<TasksSearchFilter
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
filters={filters}
onFilterChange={setFilters}
/>
{filteredTasks.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
No tasks match the current search and filter criteria
</div>
) : (
<TaskList initialTasks={filteredTasks} />
)}
</div>
)
}
138 changes: 138 additions & 0 deletions components/tasks-search-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"use client"

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

export type FilterOptions = {
statuses: string[]
priorities: string[]
}

type TasksSearchFilterProps = {
searchQuery: string
onSearchChange: (query: string) => void
filters: FilterOptions
onFilterChange: (filters: FilterOptions) => void
}

const STATUS_OPTIONS = [
{ value: "todo", label: "Todo" },
{ value: "in_progress", label: "In Progress" },
{ value: "review", label: "Review" },
{ value: "done", label: "Done" },
]

const PRIORITY_OPTIONS = [
{ value: "high", label: "High" },
{ value: "medium", label: "Medium" },
{ value: "low", label: "Low" },
]

export function TasksSearchFilter({
searchQuery,
onSearchChange,
filters,
onFilterChange,
}: TasksSearchFilterProps) {
const [isOpen, setIsOpen] = useState(false)

const handleStatusToggle = (status: string) => {
const newStatuses = filters.statuses.includes(status)
? filters.statuses.filter((s) => s !== status)
: [...filters.statuses, status]
onFilterChange({ ...filters, statuses: newStatuses })
}

const handlePriorityToggle = (priority: string) => {
const newPriorities = filters.priorities.includes(priority)
? filters.priorities.filter((p) => p !== priority)
: [...filters.priorities, priority]
onFilterChange({ ...filters, priorities: newPriorities })
}

const handleClearSearch = () => {
onSearchChange("")
}

return (
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
type="text"
placeholder="Search tasks..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-9 pr-9"
/>
{searchQuery && (
<button
type="button"
onClick={handleClearSearch}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label="Clear search"
>
<X className="h-4 w-4" />
</button>
)}
</div>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" aria-label="Filter tasks">
<Filter className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Status</DropdownMenuLabel>
<div className="px-2 py-2 space-y-2">
{STATUS_OPTIONS.map((option) => (
<div key={option.value} className="flex items-center space-x-2">
<Checkbox
id={`status-${option.value}`}
checked={filters.statuses.includes(option.value)}
onCheckedChange={() => handleStatusToggle(option.value)}
/>
<Label
htmlFor={`status-${option.value}`}
className="text-sm font-normal cursor-pointer"
>
{option.label}
</Label>
</div>
))}
</div>
<DropdownMenuSeparator />
<DropdownMenuLabel>Priority</DropdownMenuLabel>
<div className="px-2 py-2 space-y-2">
{PRIORITY_OPTIONS.map((option) => (
<div key={option.value} className="flex items-center space-x-2">
<Checkbox
id={`priority-${option.value}`}
checked={filters.priorities.includes(option.value)}
onCheckedChange={() => handlePriorityToggle(option.value)}
/>
<Label
htmlFor={`priority-${option.value}`}
className="text-sm font-normal cursor-pointer"
>
{option.label}
</Label>
</div>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
Loading