Retour au catalogue

Dashboard Widget Kanban

Tableau kanban interactif avec colonnes de statut, cartes draggables et indicateurs de priorite.

dashboard-widgetscomplex Both Responsive a11y
minimalcorporatesaassaasagencygrid
Theme
"use client";

import { useState, useRef } from "react";
import { motion, Reorder, AnimatePresence, useInView } from "framer-motion";
import {
  Plus,
  MoreHorizontal,
  Clock,
  AlertCircle,
  CheckCircle2,
  Circle,
  Tag,
} from "lucide-react";

interface KanbanCard {
  id: string;
  title: string;
  assignee: string;
  priority: "low" | "medium" | "high";
  tags: string[];
  dueDate?: string;
}

interface KanbanColumn {
  id: string;
  title: string;
  color: string;
  cards: KanbanCard[];
}

interface DashboardWidgetKanbanProps {
  columns?: KanbanColumn[];
  title?: string;
}

const priorityConfig = {
  low: { icon: Circle, color: "#10B981", label: "Basse" },
  medium: { icon: Clock, color: "#F59E0B", label: "Moyenne" },
  high: { icon: AlertCircle, color: "#EF4444", label: "Haute" },
};

const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];

export default function DashboardWidgetKanban({
  columns = [],
  title = "Tableau des taches",
}: DashboardWidgetKanbanProps) {
  const sectionRef = useRef<HTMLDivElement>(null);
  const isInView = useInView(sectionRef, { once: true, amount: 0.2 });
  const [boardColumns, setBoardColumns] = useState(columns);

  return (
    <section ref={sectionRef} className="py-12 px-6">
      <div className="max-w-6xl mx-auto">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={isInView ? { opacity: 1, y: 0 } : {}}
          transition={{ duration: 0.5, ease }}
          className="flex items-center justify-between mb-8"
        >
          <h2
            className="text-xl font-bold"
            style={{ color: "var(--color-foreground)" }}
          >
            {title}
          </h2>
          <div className="flex items-center gap-2">
            <span
              className="text-xs"
              style={{ color: "var(--color-foreground-muted)" }}
            >
              {boardColumns.reduce((sum, col) => sum + col.cards.length, 0)} taches
            </span>
            <button
              className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium"
              style={{
                background: "var(--color-accent)",
                color: "var(--color-background)",
              }}
            >
              <Plus size={14} />
              Ajouter
            </button>
          </div>
        </motion.div>

        <div className="flex gap-4 overflow-x-auto pb-4">
          {boardColumns.map((column, ci) => (
            <motion.div
              key={column.id}
              initial={{ opacity: 0, y: 20 }}
              animate={isInView ? { opacity: 1, y: 0 } : {}}
              transition={{ duration: 0.4, delay: ci * 0.1, ease }}
              className="w-72 shrink-0 rounded-2xl p-3 flex flex-col"
              style={{
                background: "var(--color-background-card)",
                border: "1px solid var(--color-border)",
              }}
            >
              {/* Column header */}
              <div className="flex items-center justify-between mb-3 px-1">
                <div className="flex items-center gap-2">
                  <div
                    className="w-3 h-3 rounded-full"
                    style={{ background: column.color }}
                  />
                  <span
                    className="text-sm font-semibold"
                    style={{ color: "var(--color-foreground)" }}
                  >
                    {column.title}
                  </span>
                  <span
                    className="text-[10px] px-1.5 py-0.5 rounded-full font-medium"
                    style={{
                      background: "var(--color-background-alt)",
                      color: "var(--color-foreground-muted)",
                    }}
                  >
                    {column.cards.length}
                  </span>
                </div>
                <button
                  style={{ color: "var(--color-foreground-light)" }}
                >
                  <MoreHorizontal size={16} />
                </button>
              </div>

              {/* Cards */}
              <div className="flex flex-col gap-2 flex-1 min-h-[200px]">
                {column.cards.map((card, cardIdx) => {
                  const pConfig = priorityConfig[card.priority];
                  const PriorityIcon = pConfig.icon;

                  return (
                    <motion.div
                      key={card.id}
                      layout
                      initial={{ opacity: 0, scale: 0.95 }}
                      animate={{ opacity: 1, scale: 1 }}
                      transition={{
                        duration: 0.3,
                        delay: ci * 0.1 + cardIdx * 0.05,
                      }}
                      className="p-3 rounded-xl cursor-grab active:cursor-grabbing"
                      style={{
                        background: "var(--color-background)",
                        border: "1px solid var(--color-border)",
                      }}
                      whileHover={{
                        y: -2,
                        boxShadow: "0 8px 20px -8px rgba(0,0,0,0.12)",
                      }}
                    >
                      {/* Priority */}
                      <div className="flex items-center justify-between mb-2">
                        <div
                          className="flex items-center gap-1 text-[10px] font-medium"
                          style={{ color: pConfig.color }}
                        >
                          <PriorityIcon size={12} />
                          {pConfig.label}
                        </div>
                        <MoreHorizontal
                          size={14}
                          style={{
                            color: "var(--color-foreground-light)",
                          }}
                        />
                      </div>

                      {/* Title */}
                      <h4
                        className="text-sm font-medium mb-2"
                        style={{ color: "var(--color-foreground)" }}
                      >
                        {card.title}
                      </h4>

                      {/* Tags */}
                      <div className="flex flex-wrap gap-1 mb-3">
                        {card.tags.map((tag) => (
                          <span
                            key={tag}
                            className="text-[10px] px-2 py-0.5 rounded-full"
                            style={{
                              background: "var(--color-accent-subtle)",
                              color: "var(--color-accent)",
                            }}
                          >
                            {tag}
                          </span>
                        ))}
                      </div>

                      {/* Footer */}
                      <div className="flex items-center justify-between">
                        <div
                          className="w-6 h-6 rounded-full flex items-center justify-center text-[9px] font-bold"
                          style={{
                            background: "var(--color-background-alt)",
                            color: "var(--color-foreground-muted)",
                            border: "1px solid var(--color-border)",
                          }}
                        >
                          {card.assignee.charAt(0)}
                        </div>
                        {card.dueDate && (
                          <div
                            className="flex items-center gap-1 text-[10px]"
                            style={{
                              color: "var(--color-foreground-light)",
                            }}
                          >
                            <Clock size={10} />
                            {card.dueDate}
                          </div>
                        )}
                      </div>
                    </motion.div>
                  );
                })}

                {/* Add card button */}
                <button
                  className="w-full py-2.5 rounded-xl text-xs font-medium flex items-center justify-center gap-1 mt-1 transition-colors"
                  style={{
                    border: "1px dashed var(--color-border)",
                    color: "var(--color-foreground-light)",
                  }}
                >
                  <Plus size={14} />
                  Ajouter une tache
                </button>
              </div>
            </motion.div>
          ))}
        </div>
      </div>
    </section>
  );
}

Avis

Dashboard Widget Kanban — React Dashboard-widgets Section — Incubator