Web InnoventixFreeCode

Calendar Agenda

Original · free

agenda list of upcoming days

byWeb InnoventixReact + Tailwind
calendaragendacalendars
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/calendar-agenda.json
calendar-agenda.tsx
"use client";

import { useId, useMemo, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type CategoryKey = "meeting" | "focus" | "personal" | "deadline" | "health";

type IconProps = { className?: string };

interface AgendaEvent {
  id: string;
  title: string;
  start: string;
  end?: string;
  category: CategoryKey;
  location: string;
  detail: string;
  people: string[];
}

interface DayGroup {
  id: string;
  weekdayShort: string;
  weekdayLong: string;
  dayNum: string;
  monthShort: string;
  relative?: string;
  isToday?: boolean;
  events: AgendaEvent[];
}

const CATEGORIES: Record<
  CategoryKey,
  { label: string; dot: string; spine: string; chip: string; chipOn: string }
> = {
  meeting: {
    label: "Meetings",
    dot: "bg-indigo-500",
    spine: "bg-indigo-400 dark:bg-indigo-500",
    chip: "bg-indigo-50 text-indigo-700 ring-indigo-600/15 dark:bg-indigo-400/10 dark:text-indigo-300 dark:ring-indigo-400/25",
    chipOn: "bg-indigo-600 text-white ring-indigo-600 dark:bg-indigo-500 dark:ring-indigo-500",
  },
  focus: {
    label: "Focus",
    dot: "bg-violet-500",
    spine: "bg-violet-400 dark:bg-violet-500",
    chip: "bg-violet-50 text-violet-700 ring-violet-600/15 dark:bg-violet-400/10 dark:text-violet-300 dark:ring-violet-400/25",
    chipOn: "bg-violet-600 text-white ring-violet-600 dark:bg-violet-500 dark:ring-violet-500",
  },
  personal: {
    label: "Personal",
    dot: "bg-emerald-500",
    spine: "bg-emerald-400 dark:bg-emerald-500",
    chip: "bg-emerald-50 text-emerald-700 ring-emerald-600/15 dark:bg-emerald-400/10 dark:text-emerald-300 dark:ring-emerald-400/25",
    chipOn: "bg-emerald-600 text-white ring-emerald-600 dark:bg-emerald-500 dark:ring-emerald-500",
  },
  deadline: {
    label: "Deadlines",
    dot: "bg-rose-500",
    spine: "bg-rose-400 dark:bg-rose-500",
    chip: "bg-rose-50 text-rose-700 ring-rose-600/15 dark:bg-rose-400/10 dark:text-rose-300 dark:ring-rose-400/25",
    chipOn: "bg-rose-600 text-white ring-rose-600 dark:bg-rose-500 dark:ring-rose-500",
  },
  health: {
    label: "Health",
    dot: "bg-sky-500",
    spine: "bg-sky-400 dark:bg-sky-500",
    chip: "bg-sky-50 text-sky-700 ring-sky-600/15 dark:bg-sky-400/10 dark:text-sky-300 dark:ring-sky-400/25",
    chipOn: "bg-sky-600 text-white ring-sky-600 dark:bg-sky-500 dark:ring-sky-500",
  },
};

const CATEGORY_ORDER: CategoryKey[] = ["meeting", "focus", "personal", "deadline", "health"];

const AVATAR_TONES = [
  "bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300",
  "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
  "bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300",
  "bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300",
  "bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300",
];

const DAYS: DayGroup[] = [
  {
    id: "d-0716",
    weekdayShort: "Thu",
    weekdayLong: "Thursday",
    dayNum: "16",
    monthShort: "Jul",
    relative: "Today",
    isToday: true,
    events: [
      {
        id: "e1",
        title: "Platform standup",
        start: "8:30 AM",
        end: "9:00 AM",
        category: "meeting",
        location: "Zoom · #platform",
        detail:
          "Quick sync on the migration blockers. Bring the failing CI logs from last night and the rollback plan for the auth service.",
        people: ["Marcus Vidal", "Lena Cho", "Dana Okafor"],
      },
      {
        id: "e2",
        title: "Deep work — Q3 pricing model",
        start: "9:30 AM",
        end: "11:00 AM",
        category: "focus",
        location: "Do not disturb",
        detail:
          "Rebuild the tiered pricing sheet with the new seat-based assumptions and sanity-check margins against the 2025 cohort.",
        people: [],
      },
      {
        id: "e3",
        title: "Lunch with Priya Nair",
        start: "12:30 PM",
        end: "1:15 PM",
        category: "personal",
        location: "Café Lomi, 4th St",
        detail:
          "Catching up before she heads to Lisbon. She's offered to intro us to two design leads she rates.",
        people: ["Priya Nair"],
      },
      {
        id: "e4",
        title: "Design review — onboarding flow",
        start: "2:00 PM",
        end: "2:45 PM",
        category: "meeting",
        location: "Room 4B",
        detail:
          "Walk through the 3-step onboarding redesign and the empty-state illustrations. Decision needed on the progress indicator.",
        people: ["Marcus Vidal", "Lena Cho"],
      },
      {
        id: "e5",
        title: "Send investor update draft",
        start: "5:00 PM",
        category: "deadline",
        location: "Notion → Board doc",
        detail:
          "Final July update: ARR, runway, the two new hires, and the enterprise pilot with Northwind Labs.",
        people: [],
      },
    ],
  },
  {
    id: "d-0717",
    weekdayShort: "Fri",
    weekdayLong: "Friday",
    dayNum: "17",
    monthShort: "Jul",
    relative: "Tomorrow",
    events: [
      {
        id: "e6",
        title: "Morning run — riverside loop",
        start: "7:00 AM",
        end: "7:45 AM",
        category: "health",
        location: "Willow Creek trail",
        detail: "Easy 6 km — third run this week. Keep the pace under 6:00 / km and don't chase it.",
        people: [],
      },
      {
        id: "e7",
        title: "Customer call — Northwind Labs",
        start: "10:00 AM",
        end: "11:30 AM",
        category: "meeting",
        location: "Google Meet",
        detail:
          "Pilot kickoff. Confirm the data-residency requirements and lock the timeline for their security questionnaire.",
        people: ["Aisha Rahman", "Tom Beckett"],
      },
      {
        id: "e8",
        title: "Focus — finalize API docs",
        start: "1:00 PM",
        end: "3:00 PM",
        category: "focus",
        location: "Do not disturb",
        detail: "Fill the gaps in the webhooks reference and add the rate-limit examples reviewers asked for.",
        people: [],
      },
      {
        id: "e9",
        title: "Ship v2.4 release notes",
        start: "4:30 PM",
        category: "deadline",
        location: "GitHub → Releases",
        detail: "Publish notes for v2.4: the agenda widget, dark-mode fixes, and the CSV export.",
        people: [],
      },
    ],
  },
  {
    id: "d-0718",
    weekdayShort: "Sat",
    weekdayLong: "Saturday",
    dayNum: "18",
    monthShort: "Jul",
    events: [
      {
        id: "e10",
        title: "Farmers market run",
        start: "9:00 AM",
        end: "10:00 AM",
        category: "personal",
        location: "Grand Ave market",
        detail: "Peaches, sourdough, and flowers for the table. Cash only at the cheese stall.",
        people: [],
      },
      {
        id: "e11",
        title: "Vinyasa yoga",
        start: "11:00 AM",
        end: "12:00 PM",
        category: "health",
        location: "Still Point Studio",
        detail: "Bring the good mat — last week's rental was falling apart at the seams.",
        people: [],
      },
    ],
  },
  {
    id: "d-0719",
    weekdayShort: "Sun",
    weekdayLong: "Sunday",
    dayNum: "19",
    monthShort: "Jul",
    events: [
      {
        id: "e12",
        title: "Dinner with the Alvarez family",
        start: "6:00 PM",
        end: "8:00 PM",
        category: "personal",
        location: "Their place, Maple Rd",
        detail:
          "Bringing dessert. Sofia's back from her semester in Kyoto — ask her about the ceramics course.",
        people: ["Sofia Alvarez", "Ramón Alvarez"],
      },
    ],
  },
  {
    id: "d-0720",
    weekdayShort: "Mon",
    weekdayLong: "Monday",
    dayNum: "20",
    monthShort: "Jul",
    events: [
      {
        id: "e13",
        title: "Weekly planning",
        start: "9:00 AM",
        end: "9:30 AM",
        category: "focus",
        location: "Do not disturb",
        detail: "Set the three priorities for the week and clear the inbox to zero before standup.",
        people: [],
      },
      {
        id: "e14",
        title: "1:1 with Dana Okafor",
        start: "11:00 AM",
        end: "12:00 PM",
        category: "meeting",
        location: "Room 2A",
        detail: "Career check-in. She wants more backend ownership — line up the payments refactor.",
        people: ["Dana Okafor"],
      },
      {
        id: "e15",
        title: "Tax documents to accountant",
        start: "3:00 PM",
        category: "deadline",
        location: "Email → Reyes & Co.",
        detail: "Send the Q2 receipts and the mileage log. Reyes needs everything before the 25th.",
        people: [],
      },
    ],
  },
  {
    id: "d-0721",
    weekdayShort: "Tue",
    weekdayLong: "Tuesday",
    dayNum: "21",
    monthShort: "Jul",
    events: [
      {
        id: "e16",
        title: "Dentist — cleaning",
        start: "8:00 AM",
        end: "8:30 AM",
        category: "health",
        location: "Bright Smile Dental",
        detail: "Routine cleaning. Mention the cold sensitivity on the lower left this time.",
        people: [],
      },
      {
        id: "e17",
        title: "Roadmap sync",
        start: "10:00 AM",
        end: "11:00 AM",
        category: "meeting",
        location: "Room 4B",
        detail: "Align eng and design on Q3. Cut scope or move the reporting module to Q4 — pick one.",
        people: ["Marcus Vidal", "Aisha Rahman"],
      },
      {
        id: "e18",
        title: "Focus — prototype agenda widget",
        start: "2:00 PM",
        end: "4:00 PM",
        category: "focus",
        location: "Do not disturb",
        detail:
          "Build the interactive agenda list — expandable events, category filters, and the completed state.",
        people: [],
      },
    ],
  },
  {
    id: "d-0722",
    weekdayShort: "Wed",
    weekdayLong: "Wednesday",
    dayNum: "22",
    monthShort: "Jul",
    events: [
      {
        id: "e19",
        title: "All-hands",
        start: "9:30 AM",
        end: "10:30 AM",
        category: "meeting",
        location: "Main hall + stream",
        detail: "Q2 results, the new hires, and the roadmap preview. Our demo slot is 4 minutes — keep it tight.",
        people: [],
      },
      {
        id: "e20",
        title: "Conference talk proposal",
        start: "1:00 PM",
        category: "deadline",
        location: "Sessionize",
        detail: "Submit the 'Designing calm software' talk — 300-word abstract plus a two-line bio.",
        people: [],
      },
    ],
  },
];

const TOTAL_EVENTS = DAYS.reduce((sum, day) => sum + day.events.length, 0);

function initials(name: string): string {
  return name
    .split(" ")
    .map((part) => part.charAt(0))
    .join("")
    .slice(0, 2)
    .toUpperCase();
}

function IconCalendar({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <rect x="3" y="4.5" width="18" height="16" rx="2.5" />
      <path d="M3 9h18M8 3v3M16 3v3" />
      <path d="M7.5 13h3v3h-3z" fill="currentColor" stroke="none" />
    </svg>
  );
}

function IconChevron({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="m6 9 6 6 6-6" />
    </svg>
  );
}

function IconCheck({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="m5 12.5 4 4 10-10" />
    </svg>
  );
}

function IconPin({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="M12 21s-6.5-5.2-6.5-10a6.5 6.5 0 0 1 13 0c0 4.8-6.5 10-6.5 10Z" />
      <circle cx="12" cy="11" r="2.2" />
    </svg>
  );
}

function IconUsers({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <circle cx="9" cy="8" r="3" />
      <path d="M3.5 20a5.5 5.5 0 0 1 11 0M16 5.2a3 3 0 0 1 0 5.6M17 20a5.5 5.5 0 0 0-2-4.3" />
    </svg>
  );
}

function IconBell({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6Z" />
      <path d="M10 19a2 2 0 0 0 4 0" />
    </svg>
  );
}

function IconFlag({ className }: IconProps) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" className={className} aria-hidden="true">
      <path d="M5 21V4M5 4h11l-2 3.5L16 11H5" />
    </svg>
  );
}

export default function CalendarAgenda() {
  const uid = useId();
  const reduce = useReducedMotion();

  const [activeCats, setActiveCats] = useState<Set<CategoryKey>>(() => new Set());
  const [completed, setCompleted] = useState<Set<string>>(() => new Set(["e1"]));
  const [reminders, setReminders] = useState<Set<string>>(() => new Set());
  const [expanded, setExpanded] = useState<Set<string>>(() => new Set(["e2"]));
  const [hideCompleted, setHideCompleted] = useState(false);

  const toggleCat = (key: CategoryKey) =>
    setActiveCats((prev) => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });

  const toggleSet =
    (setter: React.Dispatch<React.SetStateAction<Set<string>>>) => (id: string) =>
      setter((prev) => {
        const next = new Set(prev);
        if (next.has(id)) next.delete(id);
        else next.add(id);
        return next;
      });

  const toggleDone = toggleSet(setCompleted);
  const toggleReminder = toggleSet(setReminders);
  const toggleExpand = toggleSet(setExpanded);

  const filteredDays = useMemo(() => {
    return DAYS.map((day) => ({
      ...day,
      events: day.events.filter(
        (ev) =>
          (activeCats.size === 0 || activeCats.has(ev.category)) &&
          !(hideCompleted && completed.has(ev.id)),
      ),
    })).filter((day) => day.events.length > 0);
  }, [activeCats, hideCompleted, completed]);

  const visibleCount = filteredDays.reduce((sum, day) => sum + day.events.length, 0);
  const isFiltered = activeCats.size > 0 || hideCompleted;

  const headingId = `${uid}-heading`;

  return (
    <section
      aria-labelledby={headingId}
      className="relative w-full overflow-hidden bg-gradient-to-b from-white to-slate-50 px-4 py-14 text-neutral-900 dark:from-neutral-950 dark:to-neutral-900 dark:text-neutral-100 sm:py-20"
    >
      <style>{`
        @keyframes calag-rise {
          from { opacity: 0; transform: translateY(12px); }
          to { opacity: 1; transform: translateY(0); }
        }
        @keyframes calag-ping {
          0% { transform: scale(1); opacity: 0.55; }
          70% { transform: scale(2.4); opacity: 0; }
          100% { transform: scale(2.4); opacity: 0; }
        }
        .calag-rise { animation: calag-rise 0.5s cubic-bezier(0.22, 1, 0.36, 1) both; }
        .calag-ping { animation: calag-ping 2.4s ease-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .calag-rise, .calag-ping { animation: none !important; }
        }
      `}</style>

      <div
        aria-hidden="true"
        className="pointer-events-none absolute -top-24 left-1/2 h-64 w-[42rem] max-w-full -translate-x-1/2 rounded-full bg-indigo-300/25 blur-3xl dark:bg-indigo-500/10"
      />

      <div className="relative mx-auto max-w-2xl">
        {/* Header */}
        <header className="mb-7">
          <div className="flex items-start gap-4">
            <span className="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-lg shadow-indigo-500/25">
              <IconCalendar className="h-6 w-6" />
            </span>
            <div className="min-w-0">
              <h2 id={headingId} className="text-2xl font-semibold tracking-tight sm:text-3xl">
                Your agenda
              </h2>
              <p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
                The week ahead — Thursday, July 16 to Wednesday, July 22
              </p>
            </div>
          </div>

          <p
            aria-live="polite"
            className="mt-4 text-sm text-neutral-600 dark:text-neutral-300"
          >
            <span className="font-semibold text-neutral-900 dark:text-neutral-100">
              {visibleCount}
            </span>{" "}
            {visibleCount === 1 ? "event" : "events"}
            {isFiltered ? (
              <span className="text-neutral-400 dark:text-neutral-500"> of {TOTAL_EVENTS}</span>
            ) : null}{" "}
            across{" "}
            <span className="font-semibold text-neutral-900 dark:text-neutral-100">
              {filteredDays.length}
            </span>{" "}
            {filteredDays.length === 1 ? "day" : "days"}
          </p>
        </header>

        {/* Controls */}
        <div className="mb-8 flex flex-col gap-4 border-y border-neutral-200/80 py-4 dark:border-neutral-800 sm:flex-row sm:items-center sm:justify-between">
          <div
            role="group"
            aria-label="Filter agenda by category"
            className="flex flex-wrap items-center gap-2"
          >
            <button
              type="button"
              aria-pressed={activeCats.size === 0}
              onClick={() => setActiveCats(new Set())}
              className={`inline-flex items-center rounded-full px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-950 ${
                activeCats.size === 0
                  ? "bg-neutral-900 text-white ring-neutral-900 dark:bg-white dark:text-neutral-900 dark:ring-white"
                  : "bg-white text-neutral-600 ring-neutral-300 hover:bg-neutral-50 dark:bg-neutral-900 dark:text-neutral-300 dark:ring-neutral-700 dark:hover:bg-neutral-800"
              }`}
            >
              All
            </button>
            {CATEGORY_ORDER.map((key) => {
              const cfg = CATEGORIES[key];
              const on = activeCats.has(key);
              return (
                <button
                  key={key}
                  type="button"
                  aria-pressed={on}
                  onClick={() => toggleCat(key)}
                  className={`inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-950 ${
                    on
                      ? cfg.chipOn
                      : "bg-white text-neutral-600 ring-neutral-300 hover:bg-neutral-50 dark:bg-neutral-900 dark:text-neutral-300 dark:ring-neutral-700 dark:hover:bg-neutral-800"
                  }`}
                >
                  <span
                    className={`h-2 w-2 rounded-full ${on ? "bg-current opacity-90" : cfg.dot}`}
                  />
                  {cfg.label}
                </button>
              );
            })}
          </div>

          <button
            type="button"
            role="switch"
            aria-checked={hideCompleted}
            onClick={() => setHideCompleted((v) => !v)}
            className="group inline-flex shrink-0 items-center gap-2.5 rounded-full text-xs font-medium text-neutral-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-neutral-300 dark:focus-visible:ring-offset-neutral-950"
          >
            <span
              className={`relative h-5 w-9 rounded-full transition-colors ${
                hideCompleted
                  ? "bg-indigo-600 dark:bg-indigo-500"
                  : "bg-neutral-300 dark:bg-neutral-700"
              }`}
            >
              <span
                className={`absolute top-0.5 left-0.5 h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
                  hideCompleted ? "translate-x-4" : "translate-x-0"
                }`}
              />
            </span>
            Hide completed
          </button>
        </div>

        {/* Agenda list */}
        {filteredDays.length === 0 ? (
          <div className="rounded-2xl border border-dashed border-neutral-300 py-16 text-center dark:border-neutral-700">
            <p className="text-sm font-medium text-neutral-500 dark:text-neutral-400">
              Nothing scheduled for this filter.
            </p>
            <button
              type="button"
              onClick={() => {
                setActiveCats(new Set());
                setHideCompleted(false);
              }}
              className="mt-3 text-sm font-semibold text-indigo-600 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-400 dark:focus-visible:ring-offset-neutral-950"
            >
              Reset filters
            </button>
          </div>
        ) : (
          <div className="space-y-8">
            {filteredDays.map((day, dayIndex) => (
              <section
                key={day.id}
                aria-label={`${day.weekdayLong}, ${day.monthShort} ${day.dayNum}`}
                className="calag-rise"
                style={reduce ? undefined : { animationDelay: `${dayIndex * 60}ms` }}
              >
                {/* Day header */}
                <div className="mb-3 flex items-center gap-3">
                  <div
                    className={`flex h-12 w-12 shrink-0 flex-col items-center justify-center rounded-xl border ${
                      day.isToday
                        ? "border-indigo-400 bg-indigo-50 dark:border-indigo-500/60 dark:bg-indigo-500/10"
                        : "border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900"
                    }`}
                  >
                    <span
                      className={`text-[10px] font-semibold uppercase tracking-wider ${
                        day.isToday
                          ? "text-indigo-600 dark:text-indigo-300"
                          : "text-neutral-400 dark:text-neutral-500"
                      }`}
                    >
                      {day.weekdayShort}
                    </span>
                    <span className="text-lg font-semibold leading-none tabular-nums">
                      {day.dayNum}
                    </span>
                  </div>

                  <div className="min-w-0">
                    <div className="flex items-center gap-2">
                      <h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
                        {day.relative ?? day.weekdayLong}
                      </h3>
                      {day.isToday ? (
                        <span className="inline-flex items-center gap-1.5 rounded-full bg-indigo-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-indigo-600 ring-1 ring-inset ring-indigo-600/20 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/25">
                          <span className="relative flex h-1.5 w-1.5">
                            <span className="calag-ping absolute inline-flex h-full w-full rounded-full bg-indigo-500" />
                            <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-indigo-500" />
                          </span>
                          Now
                        </span>
                      ) : null}
                    </div>
                    <p className="text-xs text-neutral-500 dark:text-neutral-400">
                      {day.weekdayLong}, {day.monthShort} {day.dayNum} ·{" "}
                      {day.events.length} {day.events.length === 1 ? "item" : "items"}
                    </p>
                  </div>
                </div>

                {/* Events */}
                <ul className="space-y-2">
                  {day.events.map((ev) => {
                    const cfg = CATEGORIES[ev.category];
                    const isOpen = expanded.has(ev.id);
                    const isDone = completed.has(ev.id);
                    const hasReminder = reminders.has(ev.id);
                    const isDeadline = ev.category === "deadline";
                    const panelId = `${uid}-panel-${ev.id}`;
                    const triggerId = `${uid}-trigger-${ev.id}`;

                    return (
                      <li key={ev.id}>
                        <div className="flex gap-3">
                          {/* Time column */}
                          <div className="w-16 shrink-0 pt-3 text-right sm:w-20">
                            {isDeadline ? (
                              <span className="block text-[10px] font-semibold uppercase tracking-wide text-rose-500 dark:text-rose-400">
                                Due
                              </span>
                            ) : null}
                            <span className="block text-xs font-semibold tabular-nums text-neutral-700 dark:text-neutral-200">
                              {ev.start}
                            </span>
                            {ev.end ? (
                              <span className="block text-[11px] tabular-nums text-neutral-400 dark:text-neutral-500">
                                {ev.end}
                              </span>
                            ) : null}
                          </div>

                          {/* Card */}
                          <div
                            className={`relative flex-1 overflow-hidden rounded-xl border bg-white transition-colors dark:bg-neutral-900 ${
                              isDone
                                ? "border-neutral-200/70 dark:border-neutral-800/70"
                                : "border-neutral-200 shadow-sm shadow-neutral-900/5 hover:border-neutral-300 dark:border-neutral-800 dark:hover:border-neutral-700"
                            }`}
                          >
                            <span
                              className={`absolute inset-y-2.5 left-0 w-1 rounded-r-full ${cfg.spine} ${
                                isDone ? "opacity-40" : ""
                              }`}
                            />

                            <div className="flex items-start gap-1.5 pl-3.5 pr-2 py-2.5">
                              <button
                                id={triggerId}
                                type="button"
                                aria-expanded={isOpen}
                                aria-controls={panelId}
                                onClick={() => toggleExpand(ev.id)}
                                className="flex flex-1 items-start gap-2 rounded-lg text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-900"
                              >
                                <span className="min-w-0 flex-1">
                                  <span className="flex items-center gap-1.5">
                                    {isDeadline ? (
                                      <IconFlag className="h-3.5 w-3.5 shrink-0 text-rose-500 dark:text-rose-400" />
                                    ) : null}
                                    <span
                                      className={`text-sm font-semibold ${
                                        isDone
                                          ? "text-neutral-400 line-through decoration-neutral-300 dark:text-neutral-500 dark:decoration-neutral-600"
                                          : "text-neutral-900 dark:text-neutral-100"
                                      }`}
                                    >
                                      {ev.title}
                                    </span>
                                  </span>
                                  <span className="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1">
                                    <span
                                      className={`inline-flex items-center rounded-md px-1.5 py-0.5 text-[11px] font-medium ring-1 ring-inset ${cfg.chip}`}
                                    >
                                      {CATEGORIES[ev.category].label}
                                    </span>
                                    <span className="inline-flex items-center gap-1 text-xs text-neutral-500 dark:text-neutral-400">
                                      <IconPin className="h-3.5 w-3.5" />
                                      {ev.location}
                                    </span>
                                  </span>
                                </span>
                                <IconChevron
                                  className={`mt-0.5 h-4 w-4 shrink-0 text-neutral-400 transition-transform duration-200 ${
                                    isOpen ? "rotate-180" : ""
                                  }`}
                                />
                              </button>

                              <button
                                type="button"
                                aria-pressed={isDone}
                                onClick={() => toggleDone(ev.id)}
                                aria-label={
                                  isDone
                                    ? `Mark "${ev.title}" as not done`
                                    : `Mark "${ev.title}" as done`
                                }
                                className={`mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-full border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-900 ${
                                  isDone
                                    ? "border-emerald-500 bg-emerald-500 text-white dark:border-emerald-500 dark:bg-emerald-500"
                                    : "border-neutral-300 text-transparent hover:border-emerald-400 hover:text-emerald-300 dark:border-neutral-600 dark:hover:border-emerald-500"
                                }`}
                              >
                                <IconCheck className="h-3.5 w-3.5" />
                              </button>
                            </div>

                            {/* Expandable detail */}
                            <AnimatePresence initial={false}>
                              {isOpen ? (
                                <motion.div
                                  key="detail"
                                  id={panelId}
                                  role="region"
                                  aria-labelledby={triggerId}
                                  initial={{ height: 0, opacity: 0 }}
                                  animate={{ height: "auto", opacity: 1 }}
                                  exit={{ height: 0, opacity: 0 }}
                                  transition={
                                    reduce
                                      ? { duration: 0 }
                                      : { duration: 0.28, ease: [0.22, 1, 0.36, 1] }
                                  }
                                  className="overflow-hidden"
                                >
                                  <div className="border-t border-neutral-100 px-3.5 pb-3.5 pt-3 dark:border-neutral-800">
                                    <p className="text-sm leading-relaxed text-neutral-600 dark:text-neutral-300">
                                      {ev.detail}
                                    </p>

                                    {ev.people.length > 0 ? (
                                      <div className="mt-3 flex items-center gap-2.5">
                                        <IconUsers className="h-4 w-4 shrink-0 text-neutral-400 dark:text-neutral-500" />
                                        <ul className="flex flex-wrap items-center gap-2">
                                          {ev.people.map((person, i) => (
                                            <li
                                              key={person}
                                              className="inline-flex items-center gap-1.5 rounded-full bg-neutral-100 py-0.5 pl-0.5 pr-2.5 dark:bg-neutral-800"
                                            >
                                              <span
                                                className={`grid h-6 w-6 place-items-center rounded-full text-[10px] font-bold ${
                                                  AVATAR_TONES[i % AVATAR_TONES.length]
                                                }`}
                                              >
                                                {initials(person)}
                                              </span>
                                              <span className="text-xs font-medium text-neutral-700 dark:text-neutral-200">
                                                {person}
                                              </span>
                                            </li>
                                          ))}
                                        </ul>
                                      </div>
                                    ) : null}

                                    <div className="mt-3.5 flex flex-wrap items-center gap-2">
                                      <button
                                        type="button"
                                        aria-pressed={hasReminder}
                                        onClick={() => toggleReminder(ev.id)}
                                        className={`inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-semibold ring-1 ring-inset transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-neutral-900 ${
                                          hasReminder
                                            ? "bg-amber-50 text-amber-700 ring-amber-500/30 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/25"
                                            : "bg-white text-neutral-600 ring-neutral-300 hover:bg-neutral-50 dark:bg-neutral-900 dark:text-neutral-300 dark:ring-neutral-700 dark:hover:bg-neutral-800"
                                        }`}
                                      >
                                        <IconBell className="h-3.5 w-3.5" />
                                        {hasReminder ? "Reminder on" : "Set reminder"}
                                      </button>
                                      <button
                                        type="button"
                                        onClick={() => toggleDone(ev.id)}
                                        className="inline-flex items-center gap-1.5 rounded-lg bg-white px-2.5 py-1.5 text-xs font-semibold text-neutral-600 ring-1 ring-inset ring-neutral-300 transition-colors hover:bg-neutral-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-neutral-900 dark:text-neutral-300 dark:ring-neutral-700 dark:hover:bg-neutral-800 dark:focus-visible:ring-offset-neutral-900"
                                      >
                                        <IconCheck className="h-3.5 w-3.5" />
                                        {isDone ? "Mark as not done" : "Mark done"}
                                      </button>
                                    </div>
                                  </div>
                                </motion.div>
                              ) : null}
                            </AnimatePresence>
                          </div>
                        </div>
                      </li>
                    );
                  })}
                </ul>
              </section>
            ))}
          </div>
        )}
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →