# Issue List 1 - Searchable backlog list

> A flat backlog with search and status chips, sorted from backlog through done.

A shadcn/ui block from the [@shadcnblocks](https://shadcnblocks.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcnblocks/issue-list1).

- Page, with a live preview: https://shadcn.exchange/r/shadcnblocks/issue-list1
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcnblocks/issue-list1-light-802a5327c463a7fa.png
- Registry item: https://shadcnblocks.com/r/issue-list1.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcnblocks.com/r/issue-list1.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `cn`, `lucide-react`
- Registry items: `avatar`, `button`, `input`

## Files

### `block/issue-list1/issue-list1.tsx`

```tsx
import { Search } from "lucide-react";
import { useMemo, useState } from "react";

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "cn";

const person1 = {
  name: "Jack Walsh",
  firstName: "Jack",
  lastName: "Walsh",
  initials: "JW",
  email: "jack.walsh@example.com",
  username: "jackwalsh",
  role: "Product Designer",
  company: "Cartwright",
  location: "Brisbane, Australia",
  avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar1.webp",
};
const person2 = {
  name: "Ilze Pērkone",
  firstName: "Ilze",
  lastName: "Pērkone",
  initials: "MK",
  email: "iperkone@example.com",
  username: "iperkone",
  role: "Brand Director",
  company: "Kivit",
  location: "Rīga, Latvia",
  avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar2.webp",
};
const person3 = {
  name: "Vernon Desilets",
  firstName: "Vernon",
  lastName: "Desilets",
  initials: "VD",
  email: "vernod@example.com",
  username: "vernod",
  role: "Creative Director",
  company: "Soho Logix",
  location: "Paris, France",
  avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar3.webp",
};
const person4 = {
  name: "Lín Wàngshū",
  firstName: "Lín",
  lastName: "Wangshu",
  initials: "LW",
  email: "linwang4@example.com",
  username: "liwang",
  role: "Graphic Designer",
  company: "Tenxcloud",
  location: "Shanghai, China",
  avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar4.webp",
};
const person5 = {
  name: "Rajesh Bhatt",
  firstName: "Rajesh",
  lastName: "Bhatt",
  initials: "RB",
  email: "rajesh.bhatt@example.com",
  username: "rajeshbhatt",
  role: "UX Researcher",
  company: "Mindtree",
  location: "Mumbai, India",
  avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar5.webp",
};
const person6 = {
  name: "Jessica Cannon",
  firstName: "Jessica",
  lastName: "Cannon",
  initials: "JC",
  email: "jessica.cannon@example.com",
  username: "jessicacannon",
  role: "Data Analyst",
  company: "Fidelity Investments",
  location: "Boston, USA",
  avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar6.webp",
};
const person7 = {
  name: "Allison Goldstein",
  firstName: "Allison",
  lastName: "Goldstein",
  initials: "AG",
  email: "allison.goldstein@example.com",
  username: "allisongoldstein",
  role: "VP of Product",
  company: "General Mills",
  location: "San Diego, CA",
  avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar7.webp",
};
const person8 = {
  name: "Kim Jihoon",
  firstName: "Kim",
  lastName: "Jihoon",
  initials: "KJ",
  email: "kim.jihoon@example.com",
  username: "kimjihoon",
  role: "Software Engineer",
  company: "Saehan Electronics",
  location: "Seoul, South Korea",
  avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar8.webp",
};

type IssueStatus = "Backlog" | "Planned" | "In Progress" | "Review" | "Done";

type IssuePriority = "low" | "medium" | "high" | null;

type ProjectIssue = {
  id: string;
  code: string;
  title: string;
  description?: string;
  status: IssueStatus;
  startDate?: string;
  targetDate?: string;
};

const INITIAL_PROJECT_ISSUES: ProjectIssue[] = [
  {
    id: "issue-001",
    code: "RFC-101",
    title: "Finalize issue timeline architecture",
    status: "In Progress",
    startDate: "2026-04-02",
    targetDate: "2026-04-10",
  },
  {
    id: "issue-002",
    code: "RFC-102",
    title: "Review gantt interaction states",
    status: "Planned",
    startDate: "2026-04-04",
    targetDate: "2026-04-06",
  },
  {
    id: "issue-003",
    code: "RFC-103",
    title: "Sync issue filters with project views",
    status: "Review",
    startDate: "2026-04-08",
    targetDate: "2026-04-16",
  },
  {
    id: "issue-004",
    code: "RFC-104",
    title: "Document drag and resize behavior",
    status: "Backlog",
  },
  {
    id: "issue-005",
    code: "RFC-105",
    title: "Create empty and loading timeline states",
    status: "Planned",
    startDate: "2026-04-14",
    targetDate: "2026-04-21",
  },
  {
    id: "issue-006",
    code: "RFC-106",
    title: "Add mock dependencies and handoff notes",
    status: "Done",
    startDate: "2026-04-03",
    targetDate: "2026-04-07",
  },
  {
    id: "issue-007",
    code: "RFC-107",
    title: "Polish the quarter view density",
    status: "Review",
    startDate: "2026-04-06",
    targetDate: "2026-04-08",
  },
  {
    id: "issue-008",
    code: "RFC-108",
    title: "Prepare issue calendar follow-up screen",
    status: "Backlog",
  },
];

function getDisplayStatusLabel(status: IssueStatus) {
  return status === "Planned" ? "Todo" : status;
}

const backlogMeta: Record<
  string,
  { priority: IssuePriority; assignee: string; avatar: string }
> = {
  "issue-001": {
    priority: "high",
    assignee: person5.name,
    avatar: person5.avatar,
  },
  "issue-002": {
    priority: "medium",
    assignee: person2.name,
    avatar: person2.avatar,
  },
  "issue-003": {
    priority: "high",
    assignee: person8.name,
    avatar: person8.avatar,
  },
  "issue-004": {
    priority: null,
    assignee: person4.name,
    avatar: person4.avatar,
  },
  "issue-005": {
    priority: "medium",
    assignee: person6.name,
    avatar: person6.avatar,
  },
  "issue-006": {
    priority: "low",
    assignee: person3.name,
    avatar: person3.avatar,
  },
  "issue-007": {
    priority: "medium",
    assignee: person1.name,
    avatar: person1.avatar,
  },
  "issue-008": {
    priority: null,
    assignee: person7.name,
    avatar: person7.avatar,
  },
};

const statusOrder: IssueStatus[] = [
  "Backlog",
  "Planned",
  "In Progress",
  "Review",
  "Done",
];

function IssueList1Screen() {
  const [query, setQuery] = useState("");
  const [status, setStatus] = useState<IssueStatus | "all">("all");
  const issues = useMemo(() => {
    const normalized = query.trim().toLowerCase();
    return [...INITIAL_PROJECT_ISSUES]
      .filter((issue) => status === "all" || issue.status === status)
      .filter((issue) => {
        if (!normalized) return true;
        return [issue.code, issue.title, issue.status]
          .join(" ")
          .toLowerCase()
          .includes(normalized);
      })
      .sort(
        (a, b) => statusOrder.indexOf(a.status) - statusOrder.indexOf(b.status),
      );
  }, [query, status]);

  return (
    <main className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
      <header className="flex h-14 items-center gap-3 border-b px-4">
        <h1 className="truncate text-sm font-medium">
          Renewal Forecast Console
        </h1>
        <span className="rounded-md bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
          {issues.length}
        </span>
        <div className="relative ml-auto w-40 sm:w-56">
          <Search className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground" />
          <Input
            value={query}
            onChange={(event) => setQuery(event.target.value)}
            placeholder="Search backlog"
            className="h-8 pl-8"
          />
        </div>
      </header>
      <div className="flex gap-2 overflow-x-auto border-b px-4 py-2">
        {(["all", ...statusOrder] as const).map((option) => (
          <Button
            key={option}
            size="sm"
            variant={status === option ? "default" : "outline"}
            className="h-7"
            onClick={() => setStatus(option)}
          >
            {option === "all" ? "All" : getDisplayStatusLabel(option)}
          </Button>
        ))}
      </div>
      <ul className="min-h-0 flex-1 divide-y overflow-auto">
        {issues.map((issue) => {
          const meta = backlogMeta[issue.id];
          return (
            <li
              key={issue.id}
              className="grid grid-cols-[7rem_minmax(0,1fr)_auto] items-center gap-3 px-4 py-3 hover:bg-muted/40"
            >
              <span className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
                {issue.code}
              </span>
              <span className="min-w-0">
                <span className="block truncate text-sm font-medium">
                  {issue.title}
                </span>
                <span className="text-xs text-muted-foreground">
                  {getDisplayStatusLabel(issue.status)}
                  {meta?.priority ? ` · ${meta.priority}` : ""}
                </span>
              </span>
              <span className="flex items-center gap-2">
                <Avatar className="size-6">
                  <AvatarImage src={meta.avatar} alt={meta.assignee} />
                  <AvatarFallback className="text-[10px]">
                    {meta.assignee
                      .split(" ")
                      .map((part) => part[0])
                      .join("")}
                  </AvatarFallback>
                </Avatar>
                <span className="hidden text-xs text-muted-foreground sm:inline">
                  {meta.assignee}
                </span>
              </span>
            </li>
          );
        })}
      </ul>
    </main>
  );
}

export function IssueList1({ className }: { className?: string }) {
  return (
    <div
      className={cn(
        "flex min-h-svh min-w-0 flex-col overflow-hidden bg-background",
        className,
      )}
    >
      <IssueList1Screen />
    </div>
  );
}
```
