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
13 changes: 11 additions & 2 deletions app/(dashboard)/tasks/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,24 @@ export async function createTask(formData: FormData) {
}

// Get all tasks with assignee and creator info
export async function getAllTasks() {
export async function getAllTasks(searchQuery?: string) {
try {
const tasks = await prisma.task.findMany({
let tasks = await prisma.task.findMany({
include: {
assignee: { select: { id: true, name: true, email: true, password: true } },
creator: { select: { id: true, name: true, email: true, password: true } },
},
orderBy: { createdAt: "desc" },
});

// Filter tasks by search query (case-insensitive)
if (searchQuery) {
const lowerQuery = searchQuery.toLowerCase();
tasks = tasks.filter(task =>
task.name.toLowerCase().includes(lowerQuery)
);
}

return { tasks, error: null };
} catch (e) {
return { tasks: [], error: "Failed to fetch tasks." };
Expand Down
14 changes: 10 additions & 4 deletions app/(dashboard)/tasks/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
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 { TaskSearch } from "@/components/task-search"
import { poppins } from "@/lib/fonts"

import { getAllTasks } from "@/app/(dashboard)/tasks/actions"

export const revalidate = 0

type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>

export default async function TasksPage() {
const { tasks, error } = await getAllTasks();
export default async function TasksPage(props: { searchParams: SearchParams }) {
const searchParams = await props.searchParams
const searchQuery = typeof searchParams.search === "string" ? searchParams.search : undefined

const { tasks, error } = await getAllTasks(searchQuery);
if (error) {
console.error("Error fetching data:", error)
return <p className="p-8">Could not load data. Please try again later.</p>
Expand All @@ -30,6 +34,8 @@ export default async function TasksPage() {
</Link>
</div>

<TaskSearch />

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

import { useState, useEffect, useCallback } from "react"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Search, X, Filter } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"

export function TaskSearch() {
const router = useRouter()
const searchParams = useSearchParams()
const [searchQuery, setSearchQuery] = useState(searchParams.get("search") || "")

// Debounced search with 100ms delay
const updateSearchParam = useCallback((value: string) => {
const params = new URLSearchParams(searchParams.toString())
if (value) {
params.set("search", value)
} else {
params.delete("search")
}
router.push(`/tasks?${params.toString()}`, { scroll: false })
}, [router, searchParams])

useEffect(() => {
const timer = setTimeout(() => {
updateSearchParam(searchQuery)
}, 100)

return () => clearTimeout(timer)
}, [searchQuery, updateSearchParam])

const handleClear = () => {
setSearchQuery("")
}

return (
<div className="flex gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search tasks..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-10"
/>
{searchQuery && (
<Button
variant="ghost"
size="icon"
onClick={handleClear}
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-8 w-8"
>
<X className="h-4 w-4" />
</Button>
)}
</div>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
</div>
)
}