Velocity UI
Loading…
Menu

Pro Event Scheduler

A high-performance, glassmorphic event scheduler with drag-and-drop capabilities, multiple views (Month, Week, Day), and seamless export options.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add pro-scheduler

Source Code

pro-scheduler.tsx
"use client";

import React, { useState } from "react";
import {
  format,
  addMonths,
  subMonths,
  startOfMonth,
  endOfMonth,
  startOfWeek,
  endOfWeek,
  isSameMonth,
  isSameDay,
  addDays,
  isToday,
  addWeeks,
  subWeeks,
  setHours,
  setMinutes
} from "date-fns";
import { ChevronLeft, ChevronRight, Download, Calendar as CalendarIcon, Clock, MapPin, Plus, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
import { motion, AnimatePresence } from "framer-motion";

const USER_IMAGE = "https://i.postimg.cc/3xgQH76g/Whats-App-Image-2026-02-19-at-8-23-43-PM.jpg";

const GENERATE_MOCK_EVENTS = (baseDate: Date) => {
    const events: Event[] = [];
    const start = startOfMonth(baseDate);
    const end = endOfMonth(baseDate);
    let day = start;
    
    const TITLES = ["Team Sync", "Design Review", "Client Call", "Code Sprint", "Lunch with Sarah", "Gym", "Project Planning", "Deploy to Prod", "Marketing Sync"];
    
    while (day <= end) {
        if (Math.random() > 0.25) { // 75% chance of having events
            const numEvents = Math.floor(Math.random() * 3) + 1;
            for(let i=0; i<numEvents; i++) {
                 events.push({
                    id: Math.random().toString(36).substr(2, 9),
                    title: TITLES[Math.floor(Math.random() * TITLES.length)],
                    date: setMinutes(setHours(day, 9 + Math.floor(Math.random() * 8)), 0),
                    type: ["work", "meeting", "personal", "urgent"][Math.floor(Math.random() * 4)] as any,
                    description: "Discussing key milestones and deliverables.",
                    location: "Conference Room A"
                 });
            }
        }
        day = addDays(day, 1);
    }
    return events;
};

export type Event = {
  id: string;
  title: string;
  date: Date;
  type: "meeting" | "personal" | "work" | "urgent";
  description?: string;
  location?: string;
};

const EVENT_COLORS = {
  meeting: "bg-indigo-500/10 text-indigo-700 dark:text-indigo-300 border-indigo-200 dark:border-indigo-800/50",
  personal: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-800/50",
  work: "bg-neutral-500/10 text-neutral-700 dark:text-neutral-300 border-neutral-200 dark:border-neutral-800/50",
  urgent: "bg-rose-500/10 text-rose-700 dark:text-rose-300 border-rose-200 dark:border-rose-800/50",
};

export const ProScheduler = ({
  className,
  events: initialEvents = [],
  onAddEvent,
}: {
  className?: string;
  events?: Event[];
  onAddEvent?: (date: Date) => void;
}) => {
  const [currentDate, setCurrentDate] = useState(new Date());
  const [selectedDate, setSelectedDate] = useState(new Date());
  const [view, setView] = useState<"month" | "week" | "day">("month");
  
  // Generate more mock events
  const generateMockEvents = () => {
    const events: Event[] = [];
    const types: Event["type"][] = ["meeting", "personal", "work", "urgent"];
    const titles = ["Team Sync", "Client Call", "Project Review", "Lunch Break", "Code Review", "Design Sync", "Deploy", "Sprint Planning"];
    
    const today = new Date();
    const start = startOfMonth(today);
    const end = endOfMonth(today);
    
    // Add some random events for the current month
    for (let i = 0; i < 15; i++) {
        const date = addDays(start, Math.floor(Math.random() * 30));
        events.push({
            id: Math.random().toString(36).substr(2, 9),
            title: titles[Math.floor(Math.random() * titles.length)],
            date: new Date(date.setHours(9 + Math.floor(Math.random() * 8), 0)),
            type: types[Math.floor(Math.random() * types.length)],
            description: "Discuss project updates and next steps.",
            location: "Office / Zoom"
        });
    }
    return events;
  };

  const [events, setEvents] = useState<Event[]>([...initialEvents, ...generateMockEvents()]);
  const [showEventModal, setShowEventModal] = useState(false);
  
  const [showProfileCard, setShowProfileCard] = useState(false);

  // Form State
  const [newEventTitle, setNewEventTitle] = useState("");
  const [newEventType, setNewEventType] = useState<Event["type"]>("personal");
  const [newEventDesc, setNewEventDesc] = useState("");
  const [newEventLoc, setNewEventLoc] = useState("");

  const navigateDate = (direction: "prev" | "next") => {
    if (view === "month") {
      setCurrentDate(direction === "prev" ? subMonths(currentDate, 1) : addMonths(currentDate, 1));
    } else if (view === "week") {
      setCurrentDate(direction === "prev" ? subWeeks(currentDate, 1) : addWeeks(currentDate, 1));
    } else {
      setCurrentDate(direction === "prev" ? addDays(currentDate, -1) : addDays(currentDate, 1));
    }
  };

  const onDateClick = (day: Date) => {
    setSelectedDate(day);
    // Show profile card on specific date or double click (simulated here by checking date)
    // For demo purposes, let's say the 15th of the current month is the special profile date
    if (day.getDate() === 15) {
      setShowProfileCard(true);
    } else {
      setShowEventModal(true);
    }
  };

  const handleAddEvent = () => {
    if (newEventTitle) {
      const newEvent: Event = {
        id: Math.random().toString(36).substr(2, 9),
        title: newEventTitle,
        date: selectedDate,
        type: newEventType,
        description: newEventDesc,
        location: newEventLoc,
      };
      setEvents([...events, newEvent]);
      resetForm();
      setShowEventModal(false);
      if (onAddEvent) onAddEvent(selectedDate);
    }
  };

  const resetForm = () => {
    setNewEventTitle("");
    setNewEventType("personal");
    setNewEventDesc("");
    setNewEventLoc("");
  };

  const downloadCSV = () => {
    const headers = ["ID", "Title", "Date", "Type", "Description", "Location"];
    const csvContent = "data:text/csv;charset=utf-8," + [headers.join(",")]
      .concat(events.map(e => [e.id, `"${e.title}"`, format(e.date, "yyyy-MM-dd HH:mm"), e.type, `"${e.description || ""}"`, `"${e.location || ""}"`].join(",")))
      .join("
");
    const link = document.createElement("a");
    link.href = encodeURI(csvContent);
    link.download = "scheduler_events.csv";
    link.click();
  };

  // Helper to filter events for a specific day
  const getEventsForDay = (day: Date) => events.filter(e => isSameDay(e.date, day));

  return (
    <div className={cn("relative p-[1px] rounded-[24px] overflow-hidden bg-gradient-to-b from-white/20 to-white/5 dark:from-white/10 dark:to-white/5 shadow-2xl backdrop-blur-2xl group/shimmer", className)}>
      {/* Shimmer Effect */}
      <motion.div
        className="absolute inset-0 z-0 pointer-events-none"
        initial={{ x: "-100%" }}
        animate={{ x: "100%" }}
        transition={{
          repeat: Infinity,
          repeatType: "loop",
          duration: 3,
          ease: "linear",
          repeatDelay: 2
        }}
        style={{
          background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.05), transparent)",
        }}
      />
      
      <div className="relative z-10 bg-white/90 dark:bg-black/80 rounded-[23px] overflow-hidden h-full flex flex-col backdrop-blur-xl">
        {/* Header */}
        <div className="flex flex-col md:flex-row justify-between items-center p-3 border-b border-neutral-200/50 dark:border-white/5 gap-3">
          <div className="flex items-center gap-2">
            <div className="flex items-center bg-white/50 dark:bg-white/5 rounded-full border border-neutral-200 dark:border-white/10 p-0.5 shadow-sm backdrop-blur-md">
              <button onClick={() => navigateDate("prev")} className="p-1.5 hover:bg-neutral-100 dark:hover:bg-white/10 rounded-full transition-all active:scale-95 text-neutral-500">
                <ChevronLeft className="w-3.5 h-3.5" />
              </button>
              <div className="px-3 font-semibold min-w-[120px] text-center text-xs tracking-wide text-neutral-700 dark:text-neutral-200">
                {format(currentDate, view === "day" ? "MMMM d, yyyy" : "MMMM yyyy")}
              </div>
              <button onClick={() => navigateDate("next")} className="p-1.5 hover:bg-neutral-100 dark:hover:bg-white/10 rounded-full transition-all active:scale-95 text-neutral-500">
                <ChevronRight className="w-3.5 h-3.5" />
              </button>
            </div>
            <button 
              onClick={() => { setCurrentDate(new Date()); setView("month"); }}
              className="text-[10px] font-semibold uppercase tracking-wider text-neutral-500 hover:text-neutral-900 dark:hover:text-white transition-colors px-3 py-1.5 bg-neutral-100 dark:bg-white/5 rounded-full border border-neutral-200 dark:border-white/10"
            >
              Today
            </button>
          </div>

          <div className="flex items-center gap-2">
            <div className="flex bg-neutral-100/50 dark:bg-white/5 rounded-lg p-0.5 border border-neutral-200/50 dark:border-white/5">
              {(["month", "week", "day"] as const).map((v) => (
                <button
                  key={v}
                  onClick={() => setView(v)}
                  className={cn(
                    "px-3 py-1 text-[10px] font-medium uppercase tracking-wider rounded-md transition-all",
                    view === v
                      ? "bg-white dark:bg-neutral-800 shadow-sm text-neutral-900 dark:text-white"
                      : "text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300"
                  )}
                >
                  {v}
                </button>
              ))}
            </div>
            <button onClick={downloadCSV} className="p-1.5 bg-white dark:bg-white/5 border border-neutral-200 dark:border-white/10 rounded-lg hover:bg-neutral-50 dark:hover:bg-white/10 transition-colors text-neutral-600 dark:text-neutral-300">
              <Download className="w-3.5 h-3.5" />
            </button>
          </div>
        </div>

        {/* Content */}
        <div className="flex-1 overflow-auto bg-neutral-50/30 dark:bg-transparent custom-scrollbar">
           {view === "month" && (
             <div className="grid grid-cols-7 gap-[1px] bg-neutral-200/50 dark:bg-white/5 border-b border-neutral-200/50 dark:border-white/5">
                {["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map(day => (
                  <div key={day} className="bg-white/50 dark:bg-neutral-900/80 py-2 text-center text-[9px] font-bold uppercase tracking-wider text-neutral-400 dark:text-neutral-600">
                    {day}
                  </div>
                ))}
                {(() => {
                  const start = startOfWeek(startOfMonth(currentDate));
                  const end = endOfWeek(endOfMonth(currentDate));
                  const days = [];
                  let day = start;
                  while (day <= end) {
                    days.push(day);
                    day = addDays(day, 1);
                  }
                  return days.map((d, i) => {
                    const isSelected = isSameDay(d, selectedDate);
                    const isCurrentMonth = isSameMonth(d, currentDate);
                    const dayEvents = getEventsForDay(d);
                    const isTargetDate = d.getDate() === 15 && isSameMonth(d, currentDate); // Changed target date logic
                    
                    return (
                      <motion.div
                        key={d.toString()}
                        initial={{ opacity: 0 }}
                        animate={{ opacity: 1 }}
                        transition={{ delay: i * 0.002 }}
                        onClick={() => onDateClick(d)}
                        className={cn(
                          "min-h-[70px] bg-white dark:bg-[#0a0a0a] p-1 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-xl hover:z-20 relative group flex flex-col justify-between overflow-hidden border-[0.5px] border-neutral-100 dark:border-white/5 hover:border-neutral-300 dark:hover:border-white/20",
                          !isCurrentMonth && "bg-neutral-50/50 dark:bg-black/40 text-neutral-400 opacity-50 grayscale",
                          isSelected && !isTargetDate && "z-10 ring-2 ring-inset ring-indigo-500/50 dark:ring-indigo-400/50 bg-indigo-50/50 dark:bg-indigo-900/20",
                          isToday(d) && "bg-indigo-50/30 dark:bg-indigo-900/20 shadow-inner"
                        )}
                      >
                        {isTargetDate ? (
                           <div className="absolute inset-0 z-20 group-hover:scale-105 transition-transform duration-700">
                             <img 
                               src={USER_IMAGE} 
                               alt="Profile" 
                               className="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500"
                             />
                             <div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent flex flex-col justify-end p-2.5">
                               <span className="text-white font-bold text-[10px] tracking-wider uppercase mb-0.5">Special Day</span>
                               <span className="text-white/90 text-[9px] font-medium">View Profile</span>
                             </div>
                           </div>
                        ) : (
                          <>
                            <div className="flex justify-between items-start z-10">
                              <span className={cn(
                                "text-xs font-semibold transition-all leading-none w-6 h-6 flex items-center justify-center rounded-full",
                                isToday(d) 
                                  ? "bg-indigo-600 text-white shadow-md shadow-indigo-500/30" 
                                  : "text-neutral-700 dark:text-neutral-300 group-hover:bg-neutral-100 dark:group-hover:bg-white/10"
                              )}>
                                {format(d, "d")}
                              </span>
                              {dayEvents.length > 0 && (
                                <div className="flex -space-x-1">
                                  {dayEvents.slice(0, 3).map((e, idx) => (
                                    <div key={idx} className={cn("w-2 h-2 rounded-full border border-white dark:border-black", 
                                      e.type === 'urgent' ? 'bg-rose-500' :
                                      e.type === 'work' ? 'bg-blue-500' :
                                      'bg-indigo-500'
                                    )} />
                                  ))}
                                </div>
                              )}
                            </div>
                            <div className="space-y-1 mt-2 relative z-10">
                              {dayEvents.slice(0, 2).map(event => (
                                <div key={event.id} className={cn(
                                  "text-[9px] font-medium px-1.5 py-0.5 rounded-[4px] border truncate backdrop-blur-sm transition-all hover:scale-[1.02]", 
                                  EVENT_COLORS[event.type] || EVENT_COLORS.personal
                                )}>
                                  {event.title}
                                </div>
                              ))}
                              {dayEvents.length > 2 && (
                                <div className="text-[9px] text-neutral-400 font-medium pl-1">
                                  +{dayEvents.length - 2} more
                                </div>
                              )}
                            </div>
                          </>
                        )}
                        
                        {/* Hover Plus Icon */}
                        {!isTargetDate && (
                          <div className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity">
                              <div className="bg-neutral-100 dark:bg-white/10 text-neutral-600 dark:text-neutral-300 p-0.5 rounded-full">
                                  <Plus className="w-2.5 h-2.5" />
                              </div>
                          </div>
                        )}
                      </motion.div>
                    );
                  });
                })()}
             </div>
           )}

           {view === "week" && (
             <div className="grid grid-cols-7 gap-[1px] bg-neutral-200/50 dark:bg-white/5 h-full min-h-[400px]">
               {(() => {
                 const start = startOfWeek(currentDate);
                 const days = Array.from({ length: 7 }, (_, i) => addDays(start, i));
                 return days.map((d, i) => {
                   const isSelected = isSameDay(d, selectedDate);
                   const dayEvents = getEventsForDay(d);
                   const isTodayDate = isToday(d);

                   return (
                     <motion.div
                        key={d.toString()}
                        initial={{ opacity: 0, y: 10 }}
                        animate={{ opacity: 1, y: 0 }}
                        transition={{ delay: i * 0.05 }}
                        onClick={() => onDateClick(d)}
                        className={cn(
                          "bg-white dark:bg-[#0a0a0a] p-1.5 cursor-pointer transition-all hover:bg-neutral-50 dark:hover:bg-white/5 flex flex-col gap-2 border-r border-neutral-100 dark:border-white/5 last:border-0",
                          isSelected && "bg-neutral-50 dark:bg-white/5"
                        )}
                     >
                       <div className="text-center py-2 border-b border-neutral-100 dark:border-white/5">
                         <div className="text-[9px] font-bold uppercase tracking-wider text-neutral-400 mb-1">{format(d, "EEE")}</div>
                         <div className={cn(
                           "text-lg font-bold mx-auto w-7 h-7 flex items-center justify-center rounded-full transition-all",
                           isTodayDate 
                            ? "bg-neutral-900 text-white dark:bg-white dark:text-black shadow-lg scale-110" 
                            : "text-neutral-700 dark:text-neutral-200"
                         )}>
                           {format(d, "d")}
                         </div>
                       </div>
                       <div className="flex-1 space-y-1.5 overflow-y-auto custom-scrollbar">
                         {dayEvents.map(event => (
                           <div key={event.id} className={cn("p-1.5 rounded-md border text-[10px] shadow-sm transition-all hover:scale-[1.02]", EVENT_COLORS[event.type])}>
                             <div className="font-semibold truncate leading-tight">{event.title}</div>
                             <div className="flex items-center gap-1 mt-0.5 opacity-70 text-[9px]">
                               <Clock className="w-2.5 h-2.5" />
                               {format(event.date, "h:mm a")}
                             </div>
                           </div>
                         ))}
                         {dayEvents.length === 0 && (
                           <div className="h-full flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
                             <Plus className="w-5 h-5 text-neutral-200 dark:text-white/10" />
                           </div>
                         )}
                       </div>
                     </motion.div>
                   );
                 });
               })()}
             </div>
           )}

           {view === "day" && (
             <div className="p-3 h-full min-h-[400px]">
               <div className="max-w-3xl mx-auto bg-white dark:bg-[#0a0a0a] rounded-xl shadow-sm border border-neutral-200/50 dark:border-white/5 overflow-hidden flex flex-col h-full">
                  <div className="p-3 border-b border-neutral-200/50 dark:border-white/5 flex justify-between items-center bg-neutral-50/30 dark:bg-white/5">
                    <div>
                      <h2 className="text-xl font-bold text-neutral-800 dark:text-neutral-100">{format(currentDate, "EEEE")}</h2>
                      <p className="text-neutral-500 text-xs font-medium tracking-wide">{format(currentDate, "MMMM d, yyyy")}</p>
                    </div>
                    <button onClick={() => onDateClick(currentDate)} className="flex items-center gap-1.5 bg-neutral-900 hover:bg-neutral-800 dark:bg-white dark:hover:bg-neutral-200 text-white dark:text-black px-3 py-1.5 rounded-lg font-semibold text-xs transition-all shadow-md active:scale-95">
                      <Plus className="w-3.5 h-3.5" />
                      Add Event
                    </button>
                  </div>
                  <div className="flex-1 overflow-y-auto p-3 space-y-2 custom-scrollbar">
                    {getEventsForDay(currentDate).length > 0 ? (
                      getEventsForDay(currentDate).map((event, i) => (
                        <motion.div 
                          key={event.id}
                          initial={{ opacity: 0, x: -10 }}
                          animate={{ opacity: 1, x: 0 }}
                          transition={{ delay: i * 0.05 }}
                          className={cn("flex gap-3 p-3 rounded-lg border group hover:scale-[1.005] transition-all duration-300 relative overflow-hidden", EVENT_COLORS[event.type])}
                        >
                          <div className="absolute inset-0 bg-gradient-to-r from-white/50 to-transparent dark:from-white/5 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
                          <div className="flex flex-col items-center justify-center min-w-[50px] border-r border-current/10 pr-3">
                            <span className="text-base font-bold">{format(event.date, "h:mm")}</span>
                            <span className="text-[10px] uppercase opacity-70 font-medium">{format(event.date, "a")}</span>
                          </div>
                          <div className="flex-1 min-w-0">
                            <div className="flex justify-between items-start gap-2">
                              <h3 className="font-bold text-sm truncate">{event.title}</h3>
                              <span className="text-[9px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded-full bg-white/30 border border-white/20">{event.type}</span>
                            </div>
                            {event.description && <p className="text-xs opacity-80 mt-0.5 line-clamp-2">{event.description}</p>}
                            {event.location && (
                              <div className="flex items-center gap-1 mt-1.5 text-[10px] opacity-70">
                                <MapPin className="w-3 h-3" />
                                {event.location}
                              </div>
                            )}
                          </div>
                        </motion.div>
                      ))
                    ) : (
                      <div className="flex flex-col items-center justify-center h-full text-neutral-400 gap-3">
                        <div className="w-12 h-12 rounded-full bg-neutral-100 dark:bg-white/5 flex items-center justify-center">
                          <CalendarIcon className="w-6 h-6 opacity-40" />
                        </div>
                        <p className="text-xs font-medium">No events scheduled</p>
                      </div>
                    )}
                  </div>
               </div>
             </div>
           )}
        </div>
      </div>

      {/* Event Modal */}
      <AnimatePresence>
        {showEventModal && (
          <div className="absolute inset-0 z-50 flex items-center justify-center p-4 bg-white/10 dark:bg-black/40 backdrop-blur-sm">
            <motion.div
              initial={{ scale: 0.95, opacity: 0, y: 10 }}
              animate={{ scale: 1, opacity: 1, y: 0 }}
              exit={{ scale: 0.95, opacity: 0, y: 10 }}
              className="w-full max-w-[320px] bg-white dark:bg-[#111] rounded-2xl shadow-2xl border border-neutral-200/50 dark:border-white/10 overflow-hidden ring-1 ring-black/5"
            >
              <div className="p-4 space-y-3">
                <div className="flex justify-between items-center border-b border-neutral-100 dark:border-white/5 pb-3">
                    <h3 className="text-sm font-bold text-neutral-900 dark:text-white">New Event</h3>
                    <div className="flex items-center gap-2">
                        <span className="text-[10px] font-medium text-neutral-500 bg-neutral-100 dark:bg-white/5 px-2 py-1 rounded-full border border-neutral-200 dark:border-white/5">
                            {format(selectedDate, "MMM d")}
                        </span>
                        <button onClick={() => setShowEventModal(false)} className="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-200 transition-colors">
                            <Plus className="w-4 h-4 rotate-45" />
                        </button>
                    </div>
                </div>
                
                <div className="space-y-2.5">
                    <div>
                        <label className="text-[9px] uppercase font-bold text-neutral-400 tracking-wider mb-1 block">Title</label>
                        <input 
                            value={newEventTitle}
                            onChange={(e) => setNewEventTitle(e.target.value)}
                            placeholder="Event name"
                            className="w-full bg-neutral-50 dark:bg-white/5 border border-neutral-200 dark:border-white/10 rounded-lg px-2.5 py-1.5 text-xs focus:ring-1 focus:ring-neutral-900 dark:focus:ring-white outline-none transition-all placeholder:text-neutral-400"
                            autoFocus
                        />
                    </div>
                    <div className="grid grid-cols-2 gap-2">
                        <div>
                            <label className="text-[9px] uppercase font-bold text-neutral-400 tracking-wider mb-1 block">Type</label>
                            <select 
                                value={newEventType}
                                onChange={(e) => setNewEventType(e.target.value as any)}
                                className="w-full bg-neutral-50 dark:bg-white/5 border border-neutral-200 dark:border-white/10 rounded-lg px-2.5 py-1.5 text-xs outline-none focus:ring-1 focus:ring-neutral-900 dark:focus:ring-white appearance-none"
                            >
                                <option value="personal">Personal</option>
                                <option value="work">Work</option>
                                <option value="meeting">Meeting</option>
                                <option value="urgent">Urgent</option>
                            </select>
                        </div>
                        <div>
                            <label className="text-[9px] uppercase font-bold text-neutral-400 tracking-wider mb-1 block">Location</label>
                            <input 
                                value={newEventLoc}
                                onChange={(e) => setNewEventLoc(e.target.value)}
                                placeholder="Add location"
                                className="w-full bg-neutral-50 dark:bg-white/5 border border-neutral-200 dark:border-white/10 rounded-lg px-2.5 py-1.5 text-xs focus:ring-1 focus:ring-neutral-900 dark:focus:ring-white outline-none transition-all placeholder:text-neutral-400"
                            />
                        </div>
                    </div>
                    <div>
                        <label className="text-[9px] uppercase font-bold text-neutral-400 tracking-wider mb-1 block">Description</label>
                        <textarea 
                            value={newEventDesc}
                            onChange={(e) => setNewEventDesc(e.target.value)}
                            placeholder="Add details..."
                            rows={2}
                            className="w-full bg-neutral-50 dark:bg-white/5 border border-neutral-200 dark:border-white/10 rounded-lg px-2.5 py-1.5 text-xs focus:ring-1 focus:ring-neutral-900 dark:focus:ring-white outline-none transition-all placeholder:text-neutral-400 resize-none"
                        />
                    </div>
                </div>

                <div className="pt-2 flex gap-2">
                    <button 
                        onClick={() => setShowEventModal(false)}
                        className="flex-1 py-2 rounded-lg border border-neutral-200 dark:border-white/10 text-neutral-600 dark:text-neutral-300 text-xs font-semibold hover:bg-neutral-50 dark:hover:bg-white/5 transition-colors"
                    >
                        Cancel
                    </button>
                    <button 
                        onClick={handleAddEvent}
                        disabled={!newEventTitle}
                        className="flex-1 py-2 rounded-lg bg-neutral-900 dark:bg-white text-white dark:text-black text-xs font-semibold hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed shadow-md shadow-neutral-500/10"
                    >
                        Save Event
                    </button>
                </div>
              </div>
            </motion.div>
          </div>
        )}
      </AnimatePresence>

      <AnimatePresence>
        {showProfileCard && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md p-4"
            onClick={() => setShowProfileCard(false)}
          >
            <motion.div
              initial={{ scale: 0.9, y: 20 }}
              animate={{ scale: 1, y: 0 }}
              exit={{ scale: 0.9, y: 20 }}
              className="bg-white dark:bg-neutral-900 w-full max-w-sm rounded-3xl overflow-hidden shadow-2xl border border-white/20 relative"
              onClick={(e) => e.stopPropagation()}
            >
              <div className="h-48 relative">
                 <img 
                   src="/mac/user.png" 
                   alt="Profile" 
                   className="w-full h-full object-cover"
                 />
                 <div className="absolute inset-0 bg-gradient-to-t from-neutral-900/90 to-transparent" />
                 <div className="absolute bottom-4 left-4 text-white">
                    <h2 className="text-2xl font-bold tracking-tight">May 11, 2002</h2>
                    <p className="text-sm opacity-80 font-medium">A Special Day</p>
                 </div>
                 <button 
                   onClick={() => setShowProfileCard(false)}
                   className="absolute top-4 right-4 w-8 h-8 rounded-full bg-black/20 backdrop-blur-md flex items-center justify-center text-white hover:bg-black/40 transition-colors border border-white/10"
                 >
                   ×
                 </button>
              </div>
              <div className="p-6 space-y-4">
                 <div className="flex items-center gap-4">
                    <div className="w-12 h-12 rounded-full bg-indigo-100 dark:bg-indigo-500/20 flex items-center justify-center text-indigo-600 dark:text-indigo-400">
                       <CalendarIcon className="w-6 h-6" />
                    </div>
                    <div>
                       <h3 className="font-bold text-neutral-900 dark:text-white">Birthday Event</h3>
                       <p className="text-xs text-neutral-500">All day celebration</p>
                    </div>
                 </div>
                 <div className="space-y-2">
                    <div className="flex items-center gap-3 text-sm text-neutral-600 dark:text-neutral-400 p-3 rounded-xl bg-neutral-50 dark:bg-white/5">
                       <MapPin className="w-4 h-4" />
                       <span>San Francisco, CA</span>
                    </div>
                    <div className="flex items-center gap-3 text-sm text-neutral-600 dark:text-neutral-400 p-3 rounded-xl bg-neutral-50 dark:bg-white/5">
                       <Clock className="w-4 h-4" />
                       <span>12:00 AM - 11:59 PM</span>
                    </div>
                 </div>
                 <button 
                   className="w-full py-3 rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white font-semibold shadow-lg shadow-indigo-500/20 transition-all active:scale-95"
                   onClick={() => setShowProfileCard(false)}
                 >
                    Close Card
                 </button>
              </div>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
};

Dependencies

  • framer-motion: latest
  • lucide-react: latest
  • date-fns: latest
  • clsx: latest
  • tailwind-merge: latest

Props

Component property reference.

NameTypeDefaultDescription
eventsEvent[]-Array of event objects to display.
onAddEvent(date: Date) => void-Callback fired when a new event is added.
classNamestring-Additional CSS classes for the container.
Context Worth Keeping In Orbit

Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.

I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.

I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.