aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
337 lines (334 loc) • 14 kB
JavaScript
'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