UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

337 lines (334 loc) 14 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { Calendar, ChevronLeft, ChevronRight, Clock, MapPin, Users } from 'lucide-react'; import { useState, useMemo } from 'react'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { GlassButton } from '../button/GlassButton.js'; import '../button/GlassFab.js'; import '../button/GlassMagneticButton.js'; import { CardHeader, CardTitle, CardContent } from '../card/index.js'; import { GlassCard } from '../card/GlassCard.js'; /** * GlassCalendar component * A full-featured calendar with glassmorphism styling and event support */ const GlassCalendar = ({ selectedDate, onDateSelect, events = [], view = "month", showEvents = true, showToday = true, showWeekends = true, dateFormat = "short", locale = "en-US", minDate, maxDate, className, loading = false, ...props }) => { useReducedMotion(); const [currentDate, setCurrentDate] = useState(selectedDate || new Date()); const [selectedDateState, setSelectedDateState] = useState(selectedDate || null); // Get events for a specific date - OPTIMIZED with memoization const eventsByDate = useMemo(() => { const eventMap = new Map(); if (!events || !Array.isArray(events)) return eventMap; events.forEach(event => { const eventDate = new Date(event.date); const dateKey = eventDate.toDateString(); if (!eventMap.has(dateKey)) { eventMap.set(dateKey, []); } eventMap.get(dateKey).push(event); }); return eventMap; }, [events]); // Get month data const monthData = useMemo(() => { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startDate = new Date(firstDay); startDate.setDate(startDate.getDate() - firstDay.getDay()); const weeks = []; let currentWeek = []; const currentDateIter = new Date(startDate); // SAFE calendar generation - prevent infinite loops let dayCount = 0; const maxDays = 42; // 6 weeks max = prevent infinite loops while (dayCount < maxDays && (currentDateIter <= lastDay || currentWeek.length < 7)) { currentWeek.push(new Date(currentDateIter)); if (currentWeek.length === 7) { weeks.push([...currentWeek]); currentWeek = []; } currentDateIter.setDate(currentDateIter.getDate() + 1); dayCount++; // Safety break for full weeks if (weeks.length >= 6) break; } if (currentWeek.length > 0) { weeks.push(currentWeek); } return { year, month, monthName: new Intl.DateTimeFormat(locale, { month: "long" }).format(currentDate), weeks }; }, [currentDate, locale]); // Navigate to previous/next month const navigateMonth = direction => { setCurrentDate(prev => { const newDate = new Date(prev); if (direction === "prev") { newDate.setMonth(newDate.getMonth() - 1); } else { newDate.setMonth(newDate.getMonth() + 1); } return newDate; }); }; // Check if date is today const isToday = date => { const today = new Date(); return date.toDateString() === today.toDateString(); }; // Check if date is selected const isSelected = date => { return selectedDateState && date.toDateString() === selectedDateState.toDateString(); }; // Check if date is in current month const isCurrentMonth = date => { return date.getMonth() === currentDate.getMonth(); }; // Check if date is disabled const isDisabled = date => { if (minDate && date < minDate) return true; if (maxDate && date > maxDate) return true; return false; }; // Handle date click const handleDateClick = date => { if (isDisabled(date)) return; setSelectedDateState(date); onDateSelect?.(date); }; // Get event type color const getEventColor = event => { if (event.color) return event.color; switch (event.type) { case "meeting": return "bg-blue-500/80"; case "event": return "bg-green-500/80"; case "reminder": return "bg-yellow-500/80"; case "task": return "bg-purple-500/80"; default: return "bg-gray-500/80"; } }; // Loading skeleton if (loading) { return jsx(GlassCard, { "data-glass-component": true, className: cn("glass-p-6", className), children: jsxs("div", { className: 'animate-pulse glass-gap-4', children: [jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsx("div", { className: 'h-8 glass-surface-subtle/20 glass-radius-md w-32 glass-contrast-guard' }), jsxs("div", { className: "glass-flex glass-gap-2", children: [jsx("div", { className: 'w-8 h-8 glass-surface-subtle/20 glass-radius-md glass-contrast-guard' }), jsx("div", { className: 'w-8 h-8 glass-surface-subtle/20 glass-radius-md glass-contrast-guard' })] })] }), jsx("div", { className: "glass-grid glass-grid-cols-7 glass-gap-2", children: Array.from({ length: 35 }).map((_, i) => jsx("div", { className: 'aspect-square glass-surface-subtle/10 glass-radius-lg glass-contrast-guard' }, i)) })] }) }); } return jsx("div", { className: "glass-w-full glass-typography-reset", children: jsxs(GlassCard, { variant: "elevated", className: cn("overflow-hidden", className), ...props, children: [jsx(CardHeader, { className: "glass-border-b glass-border-white/10", children: jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsxs(CardTitle, { className: 'glass-subheading font-semibold text-primary glass-flex glass-items-center glass-gap-2', children: [jsx(Calendar, { className: 'w-6 h-6' }), monthData.monthName, " ", monthData.year] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx(GlassButton, { variant: "tertiary", size: "sm", onClick: e => navigateMonth("prev"), disabled: loading, elevation: "level1", intensity: "medium", tint: "neutral", border: "subtle", children: jsx(ChevronLeft, { className: 'w-4 h-4 glass-focus glass-touch-target glass-contrast-guard' }) }), jsx(GlassButton, { variant: "tertiary", size: "sm", onClick: e => setCurrentDate(new Date()), disabled: loading, elevation: "level1", intensity: "medium", tint: "neutral", border: "subtle", children: "Today" }), jsx(GlassButton, { variant: "tertiary", size: "sm", onClick: e => navigateMonth("next"), disabled: loading, elevation: "level1", intensity: "medium", tint: "neutral", border: "subtle", children: jsx(ChevronRight, { className: 'w-4 h-4 glass-focus glass-touch-target glass-contrast-guard' }) })] })] }) }), jsxs(CardContent, { className: "glass-p-4", children: [jsx("div", { className: 'glass-grid glass-gap-2 mb-4', style: { gridTemplateColumns: `repeat(${showWeekends ? 7 : 5}, minmax(0, 1fr))` }, children: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((day, index) => { const isWeekend = index === 0 || index === 6; if (!showWeekends && isWeekend) return null; return jsx("div", { className: cn("text-center glass-body font-medium glass-py-2 glass-radius-lg", isWeekend ? "glass-text-primary/60" : "glass-text-primary/80"), children: day }, day); }) }), jsx("div", { className: "glass-grid glass-gap-2", style: { gridTemplateColumns: `repeat(${showWeekends ? 7 : 5}, minmax(0, 1fr))` }, children: monthData.weeks.flat().map((date, index) => { const dateKey = date.toDateString(); const dayEvents = eventsByDate.get(dateKey) || []; const hasEvents = dayEvents.length > 0; const isWeekend = date.getDay() === 0 || date.getDay() === 6; if (!showWeekends && isWeekend) return null; return jsx("div", { className: 'aspect-square', children: jsxs("button", { onClick: e => handleDateClick(date), disabled: isDisabled(date), className: cn("w-full h-full glass-radius-lg transition-all duration-200 glass-focus glass-accent-primary glass-touch-target glass-contrast-guard", "flex flex-col items-center justify-start glass-p-1", "hover:bg-white/10 focus:bg-white/15 focus:outline-none hover:scale-105", "disabled:opacity-50 disabled:cursor-not-allowed", { "glass-foundation-complete glass-glass-backdrop-blur-md bg-transparent border-white/40": isSelected(date) || isToday(date) && showToday && !isSelected(date), "glass-text-primary/60": !isCurrentMonth(date), "glass-text-primary/90": isCurrentMonth(date) }), children: [jsx("span", { className: 'glass-body font-medium leading-none glass-touch-target glass-contrast-guard', children: date.getDate() }), showEvents && hasEvents && jsxs("div", { className: "glass-flex glass-flex-col glass-gap-0.5 glass-mt-1 glass-w-full", children: [dayEvents.slice(0, 2).map(event => jsx("div", { className: cn("w-full h-1 glass-radius-full", getEventColor(event)) }, event.id)), dayEvents.length > 2 && jsxs("div", { className: 'glass-w-full glass-text-xs text-primary/60 text-center', children: ["+", dayEvents.length - 2] })] })] }) }, `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`); }) }), selectedDateState && showEvents && jsxs("div", { className: 'mt-6 pt-4 glass-border-t glass-border-white/10', children: [jsxs("h3", { className: 'glass-text-lg font-semibold text-primary mb-3', children: ["Events for", " ", selectedDateState.toLocaleDateString(locale, { weekday: "long", month: "long", day: "numeric" })] }), jsxs("div", { className: "glass-gap-3", children: [(eventsByDate.get(selectedDateState.toDateString()) || []).map(event => jsx("div", { className: "glass-w-full", children: jsx(GlassCard, { className: 'glass-foundation-complete glass-glass-glass-backdrop-blur-md bg-transparent glass-border-white/40 glass-shadow-2xl glass-p-4 hover:shadow-2xl hover:bg-transparent transition-all cursor-pointer glass-contrast-guard', children: jsxs("div", { className: "glass-flex glass-items-start glass-gap-3", children: [jsx("div", { className: cn("w-3 h-3 glass-radius-full glass-mt-2 flex-shrink-0", getEventColor(event)) }), jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [jsx("h4", { className: 'font-medium text-primary glass-text-sm', children: event.title }), jsxs("div", { className: 'glass-flex glass-items-center glass-gap-4 glass-mt-2 glass-text-xs text-primary/70', children: [event.startTime && jsxs("div", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx(Clock, { className: 'w-3 h-3' }), event.startTime, event.endTime && ` - ${event.endTime}`] }), event.location && jsxs("div", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx(MapPin, { className: 'w-3 h-3' }), event.location] }), event.attendees && event.attendees.length > 0 && jsxs("div", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx(Users, { className: 'w-3 h-3' }), event.attendees.length, " attendee", event.attendees.length !== 1 ? "s" : ""] })] }), event.description && jsx("p", { className: 'glass-text-xs text-primary/60 glass-mt-2 line-clamp-2', children: event.description })] })] }) }) }, event.id)), (eventsByDate.get(selectedDateState.toDateString()) || []).length === 0 && jsx("div", { className: 'text-center text-primary/50 glass-py-8', children: "No events scheduled for this date" })] })] })] })] }) }); }; export { GlassCalendar, GlassCalendar as default }; //# sourceMappingURL=GlassCalendar.js.map