Add a Search Bar

With 50 projects in the database, scrolling and clicking “Load More” to find a specific one gets tedious. Let’s add a search bar.

Build the Search Component

Create src/components/project-search.tsx:

import { Search, X } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

type ProjectSearchProps = {
  value: string;
  onChange: (value: string) => void;
};

function ProjectSearch({ value, onChange }: ProjectSearchProps) {
  return (
    <div className="relative w-64">
      <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
      <Input
        placeholder="Search projects..."
        value={value}
        onChange={(e) => onChange(e.target.value)}
        className="pl-9 pr-9"
      />
      {value && (
        <Button
          variant="ghost"
          size="icon"
          className="absolute right-0.5 top-0.5 h-8 w-8"
          onClick={() => onChange("")}
        >
          <X className="h-4 w-4" />
        </Button>
      )}
    </div>
  );
}

export default ProjectSearch;

This is a controlled input with a search icon and a clear button that shows up when there is text.

Add Search to the Project List Page

Update src/routes/index.tsx to add the search bar and filter the loaded results on the client:

  1. Import the new component and useState:

    import { useState } from "react";
    import ProjectSearch from "@/components/project-search";
    
  2. Add state for the search query:

    function ProjectListPage() {
      const [searchQuery, setSearchQuery] = useState("");
      // ...
    }
    
  3. Render the ProjectSearch component and pass the state:

    <div>
      <div className="mb-6 flex items-center justify-between">
        <h2 className="text-xl font-semibold">Projects</h2>
        <div className="flex items-center gap-2">
          <ProjectSearch value={searchQuery} onChange={setSearchQuery} />
          <CreateProjectDialog />
        </div>
      </div>
      {/* ... */}
    </div>
    
  4. Filter the results based on the search query: (add this inside the ProjectListPage component, before the return)

    const filtered = searchQuery.trim()
      ? results.filter((p) =>
          p.name.toLowerCase().includes(searchQuery.toLowerCase()),
        )
      : results;
    
  5. Update the rendering logic to use filtered instead of results:

    {
      status === "LoadingFirstPage" ? (
        <p className="text-muted-foreground">Loading projects...</p>
      ) : filtered.length === 0 ? (
        <p className="py-12 text-center text-muted-foreground">
          {searchQuery.trim()
            ? "No matching projects found"
            : "No projects yet. Create one to get started!"}
        </p>
      ) : (
        <>
          <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
            {filtered.map((project) => (
              <ProjectCard key={project._id} project={project} />
            ))}
          </div>
          {/* ... keep the rest of the component ... */}
        </>
      );
    }
    

Try it out. Type a search term. The grid filters down to the matching projects, so the search looks like it works.

The Problem

But it does not really work. The results array only contains the projects you have loaded so far. If you have loaded 6 projects and the one you are looking for is on page 5, you will not find it. You would have to click “Load More” repeatedly until the right project happens to be loaded, and then the filter would match it.

Client-side filtering only searches what is in memory. To search the entire dataset, the search needs to happen on the backend. In the next section, we will move the search to the backend using a Convex search index.

Checkpoint: Commit your progress.

git add .
git commit -m "planner-16: Add search bar with client-side filtering"
git push