Pro Event Scheduler
A high-performance, glassmorphic event scheduler with drag-and-drop capabilities, multiple views (Month, Week, Day), and seamless export options.
Preview
Live interactive preview.
Installation
Add this component to your project using the CLI:
npx -y vui-registry-cli-v1@latest add pro-schedulerconstants.ts
import { addDays, setHours, setMinutes, startOfMonth } from "date-fns"
import type { Event } from "./types"
export const USER_IMAGE =
"https://i.postimg.cc/3xgQH76g/Whats-App-Image-2026-02-19-at-8-23-43-PM.jpg"
export const COVER_IMAGE =
"https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800&auto=format&fit=crop"
export const PROFILE_DAY = 15
export 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 STATIC_MOCK_EVENTS = (): Event[] => {
const today = new Date()
const start = startOfMonth(today)
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 offsets = [2, 4, 7, 9, 12, 14, 18, 21, 24, 26]
return offsets.map((offset, i) => {
const date = addDays(start, offset)
return {
id: `mock-${i}`,
title: titles[i % titles.length],
date: setMinutes(setHours(date, 9 + (i % 8)), 0),
type: types[i % types.length],
description: "Discuss project updates and next steps.",
location: "Office / Zoom",
}
})
}
Source Code
"use client"
import { format } from "date-fns"
import { Clock, MapPin, Plus } from "lucide-react"
import { motion } from "framer-motion"
import { cn } from "@/lib/utils"
import { EVENT_COLORS } from "./constants"
import type { Event } from "./types"
type DayViewProps = {
selectedDate: Date
getEventsForDay: (day: Date) => Event[]
onAddEvent: () => void
onEventClick: (event: Event) => void
}
export function DayView({ selectedDate, getEventsForDay, onAddEvent, onEventClick }: DayViewProps) {
const dayEvents = getEventsForDay(selectedDate)
return (
<div className="p-4 h-full min-h-[400px] overflow-y-auto custom-scrollbar">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-sm font-bold text-neutral-900 dark:text-white">{format(selectedDate, "EEEE")}</h3>
<p className="text-[10px] text-neutral-500 uppercase tracking-wider font-medium">
{format(selectedDate, "MMMM d, yyyy")}
</p>
</div>
<button
onClick={onAddEvent}
className="flex items-center gap-1.5 px-3 py-1.5 bg-neutral-900 dark:bg-white text-white dark:text-black rounded-full text-[10px] font-bold uppercase tracking-wider hover:scale-105 transition-transform shadow-lg"
>
<Plus className="w-3 h-3" />
Add Event
</button>
</div>
<div className="space-y-2">
{dayEvents.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-neutral-400">
<div className="w-10 h-10 rounded-full bg-neutral-100 dark:bg-white/5 flex items-center justify-center mb-2">
<Clock className="w-5 h-5 opacity-50" />
</div>
<p className="text-xs font-medium">No events scheduled</p>
</div>
) : (
dayEvents.map((event, i) => (
<motion.div
key={event.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
onClick={() => onEventClick(event)}
className={cn(
"p-3 rounded-xl border flex gap-3 items-start cursor-pointer hover:scale-[1.01] transition-all shadow-sm",
EVENT_COLORS[event.type]
)}
>
<div className="flex flex-col items-center min-w-[40px] pt-0.5">
<span className="text-xs font-bold">{format(event.date, "h:mm")}</span>
<span className="text-[9px] opacity-70 uppercase">{format(event.date, "a")}</span>
</div>
<div className="w-px h-full bg-current opacity-20" />
<div className="flex-1">
<h4 className="text-xs font-bold mb-0.5">{event.title}</h4>
{event.location && (
<div className="flex items-center gap-1 text-[10px] opacity-80">
<MapPin className="w-2.5 h-2.5" />
{event.location}
</div>
)}
</div>
</motion.div>
))
)}
</div>
</div>
)
}
Source Code
"use client"
import { format } from "date-fns"
import { Calendar, Clock, MapPin, Pencil, Trash2, X } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
import { cn } from "@/lib/utils"
import { EVENT_COLORS } from "./constants"
import type { Event, ModalMode } from "./types"
type EventModalProps = {
isOpen: boolean
modalMode: ModalMode
selectedDate: Date
selectedEvent: Event | null
dayEvents: Event[]
formData: { title: string; type: Event["type"]; description: string; location: string }
onClose: () => void
onFormChange: (field: string, value: string) => void
onSave: () => void
onDelete: () => void
onEdit: () => void
onEventSelect: (event: Event) => void
onCreateNew: () => void
}
export function EventModal({
isOpen,
modalMode,
selectedDate,
selectedEvent,
dayEvents,
formData,
onClose,
onFormChange,
onSave,
onDelete,
onEdit,
onEventSelect,
onCreateNew,
}: EventModalProps) {
return (
<AnimatePresence>
{isOpen && (
<div className="absolute inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
className="relative w-full max-w-sm bg-white dark:bg-[#111] rounded-2xl border border-neutral-200 dark:border-white/10 overflow-hidden"
>
<div className="flex items-center justify-between p-3 border-b border-neutral-100 dark:border-white/5">
<h3 className="text-xs font-bold uppercase tracking-wider text-neutral-500">
{modalMode === "create" && "New Event"}
{modalMode === "edit" && "Edit Event"}
{modalMode === "details" && "Event Details"}
{modalMode === "day-list" && format(selectedDate, "MMM d, yyyy")}
</h3>
<button
onClick={onClose}
className="p-1 hover:bg-neutral-100 dark:hover:bg-white/10 rounded-full transition-colors"
>
<X className="w-3.5 h-3.5 text-neutral-500" />
</button>
</div>
<div className="p-4">
{modalMode === "day-list" && (
<div className="space-y-2">
{dayEvents.map((event) => (
<button
key={event.id}
onClick={() => onEventSelect(event)}
className={cn(
"w-full text-left p-2.5 rounded-lg border text-xs transition-all hover:scale-[1.01]",
EVENT_COLORS[event.type]
)}
>
<div className="font-semibold">{event.title}</div>
<div className="text-[10px] opacity-70 mt-0.5">{format(event.date, "h:mm a")}</div>
</button>
))}
<button
onClick={onCreateNew}
className="w-full py-2 text-[10px] font-bold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded-lg transition-colors"
>
+ Add New Event
</button>
</div>
)}
{modalMode === "details" && selectedEvent && (
<div className="space-y-3">
<div className={cn("inline-flex px-2 py-0.5 rounded-md text-[10px] font-bold uppercase border", EVENT_COLORS[selectedEvent.type])}>
{selectedEvent.type}
</div>
<h4 className="text-sm font-bold text-neutral-900 dark:text-white">{selectedEvent.title}</h4>
<div className="space-y-2 text-xs text-neutral-600 dark:text-neutral-400">
<div className="flex items-center gap-2">
<Calendar className="w-3.5 h-3.5" />
{format(selectedEvent.date, "EEEE, MMMM d, yyyy")}
</div>
<div className="flex items-center gap-2">
<Clock className="w-3.5 h-3.5" />
{format(selectedEvent.date, "h:mm a")}
</div>
{selectedEvent.location && (
<div className="flex items-center gap-2">
<MapPin className="w-3.5 h-3.5" />
{selectedEvent.location}
</div>
)}
</div>
{selectedEvent.description && (
<p className="text-xs text-neutral-500 dark:text-neutral-400 leading-relaxed border-t border-neutral-100 dark:border-white/5 pt-3">
{selectedEvent.description}
</p>
)}
<div className="flex gap-2 pt-2">
<button
onClick={onEdit}
className="flex-1 flex items-center justify-center gap-1.5 py-2 bg-neutral-100 dark:bg-white/5 rounded-lg text-[10px] font-bold uppercase tracking-wider hover:bg-neutral-200 dark:hover:bg-white/10 transition-colors"
>
<Pencil className="w-3 h-3" />
Edit
</button>
<button
onClick={onDelete}
className="flex items-center justify-center gap-1.5 px-3 py-2 bg-rose-50 dark:bg-rose-900/20 text-rose-600 dark:text-rose-400 rounded-lg text-[10px] font-bold uppercase tracking-wider hover:bg-rose-100 dark:hover:bg-rose-900/30 transition-colors"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
)}
{(modalMode === "create" || modalMode === "edit") && (
<div className="space-y-3">
<div>
<label className="text-[10px] font-bold uppercase tracking-wider text-neutral-500 mb-1 block">Title</label>
<input
type="text"
value={formData.title}
onChange={(e) => onFormChange("title", e.target.value)}
className="w-full px-3 py-2 bg-neutral-50 dark:bg-white/5 border border-neutral-200 dark:border-white/10 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500/50"
placeholder="Event title"
/>
</div>
<div>
<label className="text-[10px] font-bold uppercase tracking-wider text-neutral-500 mb-1 block">Type</label>
<select
value={formData.type}
onChange={(e) => onFormChange("type", e.target.value)}
className="w-full px-3 py-2 bg-neutral-50 dark:bg-white/5 border border-neutral-200 dark:border-white/10 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500/50"
>
<option value="meeting">Meeting</option>
<option value="personal">Personal</option>
<option value="work">Work</option>
<option value="urgent">Urgent</option>
</select>
</div>
<div>
<label className="text-[10px] font-bold uppercase tracking-wider text-neutral-500 mb-1 block">Location</label>
<input
type="text"
value={formData.location}
onChange={(e) => onFormChange("location", e.target.value)}
className="w-full px-3 py-2 bg-neutral-50 dark:bg-white/5 border border-neutral-200 dark:border-white/10 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500/50"
placeholder="Optional"
/>
</div>
<div>
<label className="text-[10px] font-bold uppercase tracking-wider text-neutral-500 mb-1 block">Description</label>
<textarea
value={formData.description}
onChange={(e) => onFormChange("description", e.target.value)}
rows={2}
className="w-full px-3 py-2 bg-neutral-50 dark:bg-white/5 border border-neutral-200 dark:border-white/10 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500/50 resize-none"
placeholder="Optional"
/>
</div>
<button
onClick={onSave}
disabled={!formData.title.trim()}
className="w-full py-2.5 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg text-[10px] font-bold uppercase tracking-wider transition-colors"
>
{modalMode === "edit" ? "Save Changes" : "Create Event"}
</button>
</div>
)}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
)
}
Source Code
"use client"
import {
format,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
isSameMonth,
isSameDay,
addDays,
isToday,
} from "date-fns"
import { Plus } from "lucide-react"
import { motion } from "framer-motion"
import { cn } from "@/lib/utils"
import { EVENT_COLORS, PROFILE_DAY, USER_IMAGE } from "./constants"
import type { Event } from "./types"
type MonthViewProps = {
currentDate: Date
selectedDate: Date
getEventsForDay: (day: Date) => Event[]
onDateClick: (day: Date) => void
onEventClick: (event: Event, e?: React.MouseEvent) => void
}
export function MonthView({
currentDate,
selectedDate,
getEventsForDay,
onDateClick,
onEventClick,
}: MonthViewProps) {
const start = startOfWeek(startOfMonth(currentDate))
const end = endOfWeek(endOfMonth(currentDate))
const days: Date[] = []
let day = start
while (day <= end) {
days.push(day)
day = addDays(day, 1)
}
return (
<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((label) => (
<div
key={label}
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"
>
{label}
</div>
))}
{days.map((d, i) => {
const isSelected = isSameDay(d, selectedDate)
const isCurrentMonth = isSameMonth(d, currentDate)
const dayEvents = getEventsForDay(d)
const isProfileDay = d.getDate() === PROFILE_DAY && isSameMonth(d, currentDate)
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-lg 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 && "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) && !isProfileDay && "bg-indigo-50/30 dark:bg-indigo-900/20 shadow-inner"
)}
>
<div className="flex justify-between items-start z-10">
<span
className={cn(
"text-[10px] font-semibold leading-none w-5 h-5 flex items-center justify-center rounded-full",
isToday(d)
? "bg-indigo-600 text-white shadow-sm 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>
{isProfileDay ? (
<div className="w-5 h-5 rounded-md overflow-hidden border border-neutral-200/80 dark:border-white/15 shadow-sm shrink-0">
<img src={USER_IMAGE} alt="Profile" className="w-full h-full object-cover" />
</div>
) : dayEvents.length > 0 ? (
<div className="flex -space-x-1">
{dayEvents.slice(0, 3).map((e, idx) => (
<div
key={idx}
className={cn(
"w-1.5 h-1.5 rounded-full border border-white dark:border-black",
e.type === "urgent" ? "bg-rose-500" : e.type === "work" ? "bg-blue-500" : "bg-indigo-500"
)}
/>
))}
</div>
) : null}
</div>
{isProfileDay ? (
<div className="relative z-10 mt-1">
<span className="text-[8px] font-semibold uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
Special
</span>
</div>
) : (
<div className="space-y-1 mt-1 relative z-10">
{dayEvents.slice(0, 2).map((event) => (
<div
key={event.id}
onClick={(e) => onEventClick(event, e)}
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] cursor-pointer",
EVENT_COLORS[event.type] || EVENT_COLORS.personal
)}
>
{event.title}
</div>
))}
{dayEvents.length > 2 && (
<div className="text-[8px] text-neutral-400 font-medium pl-1">+{dayEvents.length - 2} more</div>
)}
</div>
)}
{!isProfileDay && (
<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>
)
}
Source Code
"use client"
import { useState } from "react"
import {
format,
addMonths,
subMonths,
startOfWeek,
endOfWeek,
isSameMonth,
isSameDay,
addDays,
addWeeks,
subWeeks,
setHours,
setMinutes,
} from "date-fns"
import { cn } from "@/lib/utils"
import { STATIC_MOCK_EVENTS, PROFILE_DAY } from "./constants"
import { SchedulerHeader } from "./scheduler-header"
import { MonthView } from "./month-view"
import { WeekView } from "./week-view"
import { DayView } from "./day-view"
import { EventModal } from "./event-modal"
import { ProfileCard } from "./profile-card"
import type { Event, ModalMode, ProSchedulerProps, SchedulerView } from "./types"
export type { Event } from "./types"
export const ProScheduler = ({
className,
events: initialEvents = [],
onAddEvent,
}: ProSchedulerProps) => {
const [currentDate, setCurrentDate] = useState(new Date())
const [selectedDate, setSelectedDate] = useState(new Date())
const [view, setView] = useState<SchedulerView>("month")
const [events, setEvents] = useState<Event[]>([...initialEvents, ...STATIC_MOCK_EVENTS()])
const [showEventModal, setShowEventModal] = useState(false)
const [modalMode, setModalMode] = useState<ModalMode>("create")
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null)
const [showProfileCard, setShowProfileCard] = useState(false)
const [formData, setFormData] = useState({
title: "",
type: "personal" as Event["type"],
description: "",
location: "",
})
const getEventsForDay = (day: Date) => events.filter((e) => isSameDay(e.date, day))
const resetForm = () => {
setFormData({ title: "", type: "personal", description: "", location: "" })
}
const closeModal = () => {
setShowEventModal(false)
setSelectedEvent(null)
resetForm()
}
const openCreateModal = (day: Date) => {
setSelectedDate(day)
setSelectedEvent(null)
setModalMode("create")
resetForm()
setShowEventModal(true)
}
const openDayListModal = (day: Date) => {
setSelectedDate(day)
setSelectedEvent(null)
setModalMode("day-list")
setShowEventModal(true)
}
const openEventDetails = (event: Event, e?: React.MouseEvent) => {
e?.stopPropagation()
setSelectedEvent(event)
setSelectedDate(event.date)
setModalMode("details")
setShowEventModal(true)
}
const openEditModal = () => {
if (!selectedEvent) return
setFormData({
title: selectedEvent.title,
type: selectedEvent.type,
description: selectedEvent.description || "",
location: selectedEvent.location || "",
})
setModalMode("edit")
}
const onDateClick = (day: Date) => {
setSelectedDate(day)
setCurrentDate(day)
if (day.getDate() === PROFILE_DAY && isSameMonth(day, currentDate)) {
setShowProfileCard(true)
return
}
const dayEvents = getEventsForDay(day)
if (dayEvents.length > 0) {
openDayListModal(day)
} else {
openCreateModal(day)
}
}
const handleSave = () => {
if (!formData.title.trim()) return
const eventDate = setMinutes(setHours(selectedDate, 10), 0)
if (modalMode === "edit" && selectedEvent) {
setEvents(
events.map((e) =>
e.id === selectedEvent.id
? { ...e, ...formData, date: eventDate }
: e
)
)
} else {
const newEvent: Event = {
id: Math.random().toString(36).slice(2, 11),
title: formData.title,
date: eventDate,
type: formData.type,
description: formData.description,
location: formData.location,
}
setEvents([...events, newEvent])
onAddEvent?.(selectedDate)
}
closeModal()
}
const handleDelete = () => {
if (!selectedEvent) return
setEvents(events.filter((e) => e.id !== selectedEvent.id))
closeModal()
}
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 headerLabel = () => {
if (view === "day") return format(selectedDate, "MMMM d, yyyy")
if (view === "week") {
const start = startOfWeek(currentDate)
const end = endOfWeek(currentDate)
return `${format(start, "MMM d")} – ${format(end, "MMM d, yyyy")}`
}
return format(currentDate, "MMMM yyyy")
}
const switchView = (next: SchedulerView) => {
setView(next)
if (next === "day") setCurrentDate(selectedDate)
}
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()
}
const handleFormChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
return (
<div
className={cn(
"relative rounded-[24px] overflow-hidden border border-neutral-200/80 dark:border-white/10 bg-white dark:bg-[#0a0a0a]",
className
)}
>
<div className="relative h-full flex flex-col">
<SchedulerHeader
headerLabel={headerLabel()}
view={view}
onNavigate={navigateDate}
onToday={() => {
const now = new Date()
setCurrentDate(now)
setSelectedDate(now)
}}
onSwitchView={switchView}
onDownload={downloadCSV}
/>
<div className="flex-1 overflow-auto bg-neutral-50/30 dark:bg-transparent custom-scrollbar">
{view === "month" && (
<MonthView
currentDate={currentDate}
selectedDate={selectedDate}
getEventsForDay={getEventsForDay}
onDateClick={onDateClick}
onEventClick={openEventDetails}
/>
)}
{view === "week" && (
<WeekView
currentDate={currentDate}
selectedDate={selectedDate}
getEventsForDay={getEventsForDay}
onDateClick={onDateClick}
onEventClick={openEventDetails}
/>
)}
{view === "day" && (
<DayView
selectedDate={selectedDate}
getEventsForDay={getEventsForDay}
onAddEvent={() => openCreateModal(selectedDate)}
onEventClick={(event) => openEventDetails(event)}
/>
)}
</div>
</div>
<EventModal
isOpen={showEventModal}
modalMode={modalMode}
selectedDate={selectedDate}
selectedEvent={selectedEvent}
dayEvents={getEventsForDay(selectedDate)}
formData={formData}
onClose={closeModal}
onFormChange={handleFormChange}
onSave={handleSave}
onDelete={handleDelete}
onEdit={openEditModal}
onEventSelect={openEventDetails}
onCreateNew={() => openCreateModal(selectedDate)}
/>
<ProfileCard isOpen={showProfileCard} onClose={() => setShowProfileCard(false)} />
</div>
)
}
Source Code
"use client"
import { X } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
import { USER_IMAGE, COVER_IMAGE } from "./constants"
type ProfileCardProps = {
isOpen: boolean
onClose: () => void
}
export function ProfileCard({ isOpen, onClose }: ProfileCardProps) {
return (
<AnimatePresence>
{isOpen && (
<div className="absolute inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
className="relative w-full max-w-xs bg-white dark:bg-[#111] rounded-2xl border border-neutral-200 dark:border-white/10 overflow-hidden"
>
<button
onClick={onClose}
className="absolute top-2 right-2 z-10 p-1 bg-black/20 hover:bg-black/40 rounded-full transition-colors"
>
<X className="w-3.5 h-3.5 text-white" />
</button>
<div className="relative h-28 overflow-hidden">
<img src={COVER_IMAGE} alt="Cover" className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
</div>
<div className="p-4 -mt-8 relative">
<div className="w-14 h-14 rounded-xl border-2 border-white dark:border-[#111] overflow-hidden mb-2">
<img src={USER_IMAGE} alt="Vikas Yadav" className="w-full h-full object-cover" />
</div>
<h3 className="text-sm font-bold text-neutral-900 dark:text-white">VIKAS YADAV</h3>
<p className="text-[10px] text-neutral-500 uppercase tracking-wider font-medium mb-3">
Designer · Dehradun, India
</p>
<p className="text-xs text-neutral-600 dark:text-neutral-400 leading-relaxed">
Special day — portfolio review and design milestone celebration with the Velocity UI team.
</p>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
)
}
Source Code
"use client"
import { ChevronLeft, ChevronRight, Download } from "lucide-react"
import type { SchedulerView } from "./types"
type SchedulerHeaderProps = {
headerLabel: string
view: SchedulerView
onNavigate: (direction: "prev" | "next") => void
onToday: () => void
onSwitchView: (view: SchedulerView) => void
onDownload: () => void
}
export function SchedulerHeader({
headerLabel,
view,
onNavigate,
onToday,
onSwitchView,
onDownload,
}: SchedulerHeaderProps) {
return (
<div className="flex flex-col md:flex-row justify-between items-center p-3 md:p-4 md:px-6 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={() => onNavigate("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">
{headerLabel}
</div>
<button
onClick={() => onNavigate("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={onToday}
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={() => onSwitchView(v)}
className={`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={onDownload}
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>
)
}
types.ts
export type SchedulerView = "month" | "week" | "day"
export type ModalMode = "create" | "details" | "edit" | "day-list"
export type Event = {
id: string
title: string
date: Date
type: "meeting" | "personal" | "work" | "urgent"
description?: string
location?: string
}
export type ProSchedulerProps = {
className?: string
events?: Event[]
onAddEvent?: (date: Date) => void
}
Source Code
"use client"
import { format, startOfWeek, addDays, isSameDay, isToday } from "date-fns"
import { Clock, Plus } from "lucide-react"
import { motion } from "framer-motion"
import { cn } from "@/lib/utils"
import { EVENT_COLORS } from "./constants"
import type { Event } from "./types"
type WeekViewProps = {
currentDate: Date
selectedDate: Date
getEventsForDay: (day: Date) => Event[]
onDateClick: (day: Date) => void
onEventClick: (event: Event, e?: React.MouseEvent) => void
}
export function WeekView({
currentDate,
selectedDate,
getEventsForDay,
onDateClick,
onEventClick,
}: WeekViewProps) {
const start = startOfWeek(currentDate)
const days = Array.from({ length: 7 }, (_, i) => addDays(start, i))
return (
<div className="grid grid-cols-7 gap-[1px] bg-neutral-200/50 dark:bg-white/5 h-full min-h-[400px]">
{days.map((d, i) => {
const isSelected = isSameDay(d, selectedDate)
const dayEvents = getEventsForDay(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-sm font-bold mx-auto w-6 h-6 flex items-center justify-center rounded-full transition-all",
isToday(d)
? "bg-neutral-900 text-white dark:bg-white dark:text-black shadow-md"
: "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}
onClick={(e) => onEventClick(event, e)}
className={cn(
"p-1.5 rounded-md border text-[10px] shadow-sm transition-all hover:scale-[1.02] cursor-pointer",
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>
)
}
Dependencies
framer-motion: latestlucide-react: latestdate-fns: latestclsx: latesttailwind-merge: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| events | Event[] | - | Array of event objects to display. |
| onAddEvent | (date: Date) => void | - | Callback fired when a new event is added. |
| className | string | - | Additional CSS classes for the container. |
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.