Task Progress

PreviousNext

Visual progress indicator for task completion goals with percentage and count display.

Docs
hextauiui

Preview

Loading preview…
registry/new-york/blocks/tasks/task-progress.tsx
"use client";

import { CheckCircle2, Target, TrendingUp } from "lucide-react";
import { useMemo } from "react";
import { cn } from "@/lib/utils";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/registry/new-york/ui/card";
import { Progress } from "@/registry/new-york/ui/progress";
import type { Task } from "./task-list";

export interface TaskProgressProps {
  tasks?: Task[];
  goal?: number;
  period?: "day" | "week" | "month" | "all";
  className?: string;
  showBreakdown?: boolean;
  showTrend?: boolean;
}

export default function TaskProgress({
  tasks = [],
  goal,
  period = "all",
  className,
  showBreakdown = true,
  showTrend = true,
}: TaskProgressProps) {
  const stats = useMemo(() => {
    const total = tasks.length;
    const completed = tasks.filter((t) => t.status === "done").length;
    const inProgress = tasks.filter((t) => t.status === "in_progress").length;
    const todo = tasks.filter((t) => t.status === "todo").length;
    const cancelled = tasks.filter((t) => t.status === "cancelled").length;
    const completionRate = total > 0 ? (completed / total) * 100 : 0;

    return {
      total,
      completed,
      inProgress,
      todo,
      cancelled,
      completionRate,
    };
  }, [tasks]);

  const progressPercentage = goal
    ? Math.min((stats.completed / goal) * 100, 100)
    : stats.completionRate;

  return (
    <Card className={cn("w-full shadow-xs", className)}>
      <CardHeader>
        <div className="flex flex-col gap-1">
          <CardTitle>Progress Overview</CardTitle>
          <CardDescription>
            {stats.completed} of {stats.total} tasks completed
            {goal && ` (Goal: ${goal})`}
          </CardDescription>
        </div>
      </CardHeader>
      <CardContent>
        <div className="flex flex-col gap-6">
          <div className="flex flex-col gap-2">
            <div className="flex items-center justify-between">
              <span className="text-muted-foreground text-sm">
                Overall Progress
              </span>
              <span className="font-semibold text-sm">
                {Math.round(progressPercentage)}%
              </span>
            </div>
            <Progress className="h-2" value={progressPercentage} />
            {goal && (
              <div className="flex items-center justify-between text-xs">
                <span className="text-muted-foreground">
                  {stats.completed} / {goal} completed
                </span>
                {stats.completed >= goal && (
                  <div className="flex items-center gap-1 text-green-600">
                    <Target className="size-3" />
                    Goal achieved!
                  </div>
                )}
              </div>
            )}
          </div>

          {showBreakdown && (
            <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
              <div className="flex flex-col gap-2 rounded-lg border bg-card p-3">
                <div className="flex items-center gap-2">
                  <CheckCircle2 className="size-4 text-green-600" />
                  <span className="text-muted-foreground text-xs">
                    Completed
                  </span>
                </div>
                <div className="flex items-baseline gap-1">
                  <span className="font-semibold text-2xl">
                    {stats.completed}
                  </span>
                  <span className="text-muted-foreground text-xs">
                    ({Math.round((stats.completed / stats.total) * 100) || 0}%)
                  </span>
                </div>
              </div>

              <div className="flex flex-col gap-2 rounded-lg border bg-card p-3">
                <div className="flex items-center gap-2">
                  <TrendingUp className="size-4 text-blue-600" />
                  <span className="text-muted-foreground text-xs">
                    In Progress
                  </span>
                </div>
                <div className="flex items-baseline gap-1">
                  <span className="font-semibold text-2xl">
                    {stats.inProgress}
                  </span>
                  <span className="text-muted-foreground text-xs">
                    ({Math.round((stats.inProgress / stats.total) * 100) || 0}%)
                  </span>
                </div>
              </div>

              <div className="flex flex-col gap-2 rounded-lg border bg-card p-3">
                <div className="flex items-center gap-2">
                  <Target className="size-4 text-muted-foreground" />
                  <span className="text-muted-foreground text-xs">To Do</span>
                </div>
                <div className="flex items-baseline gap-1">
                  <span className="font-semibold text-2xl">{stats.todo}</span>
                  <span className="text-muted-foreground text-xs">
                    ({Math.round((stats.todo / stats.total) * 100) || 0}%)
                  </span>
                </div>
              </div>

              <div className="flex flex-col gap-2 rounded-lg border bg-card p-3">
                <div className="flex items-center gap-2">
                  <Target className="size-4 text-muted-foreground" />
                  <span className="text-muted-foreground text-xs">Total</span>
                </div>
                <div className="flex items-baseline gap-1">
                  <span className="font-semibold text-2xl">{stats.total}</span>
                  <span className="text-muted-foreground text-xs">tasks</span>
                </div>
              </div>
            </div>
          )}
        </div>
      </CardContent>
    </Card>
  );
}

Installation

npx shadcn@latest add @hextaui/task-progress

Usage

import { TaskProgress } from "@/components/ui/task-progress"
<TaskProgress />