Retour au catalogue

Dashboard Widget Calendar

Widget calendrier avec vue mensuelle, evenements colores et detail au clic.

dashboard-widgetsmedium Both Responsive a11y
minimalelegantsaasmedicaleducationgrid
Theme
"use client";

import { useState, useRef } from "react";
import { motion, AnimatePresence, useInView } from "framer-motion";
import {
  ChevronLeft,
  ChevronRight,
  Clock,
  MapPin,
  Plus,
  X,
} from "lucide-react";

interface CalendarEvent {
  day: number;
  title: string;
  time: string;
  color: string;
  location?: string;
}

interface DashboardWidgetCalendarProps {
  events?: CalendarEvent[];
  title?: string;
  month?: string;
  year?: number;
  daysInMonth?: number;
  startDay?: number;
}

const DAYS = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"];

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

export default function DashboardWidgetCalendar({
  events = [],
  title = "Calendrier",
  month = "Mars",
  year = 2026,
  daysInMonth = 31,
  startDay = 6,
}: DashboardWidgetCalendarProps) {
  const [selectedDay, setSelectedDay] = useState<number | null>(null);
  const sectionRef = useRef<HTMLDivElement>(null);
  const isInView = useInView(sectionRef, { once: true, amount: 0.3 });

  const today = 13;

  const eventsForDay = (day: number) =>
    events.filter((e) => e.day === day);

  const selectedEvents = selectedDay ? eventsForDay(selectedDay) : [];

  // Build calendar grid
  const blanks = Array.from({ length: startDay }, () => null);
  const days = Array.from({ length: daysInMonth }, (_, i) => i + 1);
  const grid = [...blanks, ...days];

  return (
    <section ref={sectionRef} className="py-12 px-6">
      <div className="max-w-md mx-auto">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={isInView ? { opacity: 1, y: 0 } : {}}
          transition={{ duration: 0.5, ease }}
          className="rounded-3xl overflow-hidden"
          style={{
            background: "var(--color-background-card)",
            border: "1px solid var(--color-border)",
          }}
        >
          {/* Header */}
          <div className="px-6 py-5 flex items-center justify-between">
            <div>
              <h2
                className="text-lg font-bold"
                style={{ color: "var(--color-foreground)" }}
              >
                {month} {year}
              </h2>
              <p
                className="text-xs mt-0.5"
                style={{ color: "var(--color-foreground-muted)" }}
              >
                {events.length} evenements ce mois
              </p>
            </div>
            <div className="flex items-center gap-1">
              <button
                className="w-8 h-8 rounded-lg flex items-center justify-center"
                style={{
                  background: "var(--color-background-alt)",
                  color: "var(--color-foreground-muted)",
                }}
              >
                <ChevronLeft size={16} />
              </button>
              <button
                className="w-8 h-8 rounded-lg flex items-center justify-center"
                style={{
                  background: "var(--color-background-alt)",
                  color: "var(--color-foreground-muted)",
                }}
              >
                <ChevronRight size={16} />
              </button>
            </div>
          </div>

          {/* Day headers */}
          <div className="grid grid-cols-7 px-4">
            {DAYS.map((day) => (
              <div
                key={day}
                className="text-center text-[10px] font-semibold uppercase tracking-wider py-2"
                style={{ color: "var(--color-foreground-light)" }}
              >
                {day}
              </div>
            ))}
          </div>

          {/* Calendar grid */}
          <div className="grid grid-cols-7 gap-1 px-4 pb-4">
            {grid.map((day, i) => {
              if (day === null) {
                return <div key={`blank-${i}`} />;
              }

              const dayEvents = eventsForDay(day);
              const isToday = day === today;
              const isSelected = day === selectedDay;

              return (
                <motion.button
                  key={day}
                  onClick={() =>
                    setSelectedDay(isSelected ? null : day)
                  }
                  className="relative aspect-square rounded-xl flex flex-col items-center justify-center transition-all"
                  style={{
                    background: isSelected
                      ? "var(--color-accent)"
                      : isToday
                        ? "var(--color-accent-subtle)"
                        : "transparent",
                    color: isSelected
                      ? "var(--color-background)"
                      : isToday
                        ? "var(--color-accent)"
                        : "var(--color-foreground)",
                  }}
                  whileHover={{ scale: 1.1 }}
                  whileTap={{ scale: 0.95 }}
                >
                  <span className="text-sm font-medium">{day}</span>
                  {dayEvents.length > 0 && (
                    <div className="flex gap-0.5 mt-0.5">
                      {dayEvents.slice(0, 3).map((ev, ei) => (
                        <div
                          key={ei}
                          className="w-1 h-1 rounded-full"
                          style={{
                            background: isSelected
                              ? "var(--color-background)"
                              : ev.color,
                          }}
                        />
                      ))}
                    </div>
                  )}
                </motion.button>
              );
            })}
          </div>

          {/* Event detail panel */}
          <AnimatePresence>
            {selectedDay && (
              <motion.div
                initial={{ height: 0, opacity: 0 }}
                animate={{ height: "auto", opacity: 1 }}
                exit={{ height: 0, opacity: 0 }}
                transition={{ duration: 0.25, ease }}
                className="overflow-hidden"
                style={{ borderTop: "1px solid var(--color-border)" }}
              >
                <div className="px-6 py-4">
                  <div className="flex items-center justify-between mb-3">
                    <span
                      className="text-sm font-semibold"
                      style={{ color: "var(--color-foreground)" }}
                    >
                      {selectedDay} {month}
                    </span>
                    <button
                      onClick={() => setSelectedDay(null)}
                      style={{ color: "var(--color-foreground-light)" }}
                    >
                      <X size={16} />
                    </button>
                  </div>

                  {selectedEvents.length > 0 ? (
                    <div className="flex flex-col gap-2">
                      {selectedEvents.map((ev, i) => (
                        <motion.div
                          key={ev.title}
                          initial={{ opacity: 0, x: -10 }}
                          animate={{ opacity: 1, x: 0 }}
                          transition={{ delay: i * 0.05 }}
                          className="flex items-start gap-3 p-3 rounded-xl"
                          style={{
                            background: "var(--color-background-alt)",
                          }}
                        >
                          <div
                            className="w-1 h-full rounded-full self-stretch shrink-0"
                            style={{ background: ev.color }}
                          />
                          <div className="flex-1">
                            <div
                              className="text-sm font-medium"
                              style={{
                                color: "var(--color-foreground)",
                              }}
                            >
                              {ev.title}
                            </div>
                            <div
                              className="flex items-center gap-3 mt-1 text-xs"
                              style={{
                                color: "var(--color-foreground-muted)",
                              }}
                            >
                              <span className="flex items-center gap-1">
                                <Clock size={10} /> {ev.time}
                              </span>
                              {ev.location && (
                                <span className="flex items-center gap-1">
                                  <MapPin size={10} /> {ev.location}
                                </span>
                              )}
                            </div>
                          </div>
                        </motion.div>
                      ))}
                    </div>
                  ) : (
                    <div
                      className="text-center py-4 text-xs"
                      style={{
                        color: "var(--color-foreground-muted)",
                      }}
                    >
                      Aucun evenement ce jour
                    </div>
                  )}
                </div>
              </motion.div>
            )}
          </AnimatePresence>
        </motion.div>
      </div>
    </section>
  );
}

Avis

Dashboard Widget Calendar — React Dashboard-widgets Section — Incubator