aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
420 lines (417 loc) • 15.4 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import { Calendar, X, ChevronLeft, ChevronRight } from 'lucide-react';
import React, { useState, useRef, useEffect } from 'react';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import { MotionFramer } from '../../primitives/motion/MotionFramer.js';
import { GlassButton } from '../button/GlassButton.js';
import '../button/GlassFab.js';
import '../button/GlassMagneticButton.js';
import { CardContent } from '../card/index.js';
import { GlassCard } from '../card/GlassCard.js';
/**
* GlassDateRangePicker component
* A glassmorphism date range picker with calendar interface
*/
const GlassDateRangePicker = ({
value,
defaultValue,
onChange,
placeholder = "Select date range",
dateFormat = "short",
locale = "en-US",
minDate,
maxDate,
disabled = false,
size = "md",
className,
popoverClassName,
showClear = true,
presets = [{
label: "Today",
getValue: () => {
const today = new Date();
return {
from: today,
to: today
};
}
}, {
label: "Yesterday",
getValue: () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return {
from: yesterday,
to: yesterday
};
}
}, {
label: "Last 7 days",
getValue: () => {
const today = new Date();
const lastWeek = new Date();
lastWeek.setDate(today.getDate() - 6);
return {
from: lastWeek,
to: today
};
}
}, {
label: "Last 30 days",
getValue: () => {
const today = new Date();
const lastMonth = new Date();
lastMonth.setDate(today.getDate() - 29);
return {
from: lastMonth,
to: today
};
}
}, {
label: "This month",
getValue: () => {
const today = new Date();
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
return {
from: firstDay,
to: today
};
}
}, {
label: "Last month",
getValue: () => {
const today = new Date();
const firstDay = new Date(today.getFullYear(), today.getMonth() - 1, 1);
const lastDay = new Date(today.getFullYear(), today.getMonth(), 0);
return {
from: firstDay,
to: lastDay
};
}
}],
rangeLabel = "to",
...props
}) => {
const [isOpen, setIsOpen] = useState(false);
const [currentRange, setCurrentRange] = useState(value || defaultValue || {
from: null,
to: null
});
const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectingFrom, setSelectingFrom] = useState(true);
const triggerRef = useRef(null);
const popoverRef = useRef(null);
// Size configurations
const sizeConfigs = {
sm: {
trigger: "h-8 glass-px-3 glass-text-sm",
calendar: "w-64"
},
md: {
trigger: "h-10 glass-px-4 glass-text-base",
calendar: "w-80"
},
lg: {
trigger: "h-12 glass-px-6 glass-text-lg",
calendar: "w-96"
}
};
// Update current range when value prop changes
useEffect(() => {
if (value) {
setCurrentRange(value);
}
}, [value]);
// Handle range change
const handleRangeChange = newRange => {
setCurrentRange(newRange);
onChange?.(newRange);
};
// Handle date selection
const handleDateSelect = date => {
const newRange = {
...currentRange
};
if (selectingFrom || !currentRange.from) {
newRange.from = date;
newRange.to = null;
setSelectingFrom(false);
} else if (!currentRange.to || date < currentRange.from) {
newRange.from = date;
newRange.to = null;
setSelectingFrom(false);
} else {
newRange.to = date;
setSelectingFrom(true);
setIsOpen(false);
}
handleRangeChange(newRange);
};
// Handle preset selection
const handlePresetSelect = preset => {
const newRange = preset.getValue();
handleRangeChange(newRange);
setIsOpen(false);
};
// Clear selection
const handleClear = () => {
handleRangeChange({
from: null,
to: null
});
setSelectingFrom(true);
};
// Format date for display
const formatDate = date => {
if (!date) return "";
const options = {
year: "numeric",
month: dateFormat === "numeric" ? "2-digit" : dateFormat === "long" ? "long" : "short",
day: "2-digit"
};
return new Intl.DateTimeFormat(locale, options).format(date);
};
// Get display value
const getDisplayValue = () => {
if (!currentRange.from && !currentRange.to) {
return placeholder;
}
if (currentRange.from && !currentRange.to) {
return `${formatDate(currentRange.from)} ${rangeLabel} ...`;
}
if (currentRange.from && currentRange.to) {
return `${formatDate(currentRange.from)} ${rangeLabel} ${formatDate(currentRange.to)}`;
}
return placeholder;
};
// Check if date is in range
const isDateInRange = date => {
if (!currentRange.from || !currentRange.to) return false;
return date >= currentRange.from && date <= currentRange.to;
};
// Check if date is range start or end
const isRangeBoundary = date => {
if (!currentRange.from && !currentRange.to) return false;
return currentRange.from && date.toDateString() === currentRange.from.toDateString() || currentRange.to && date.toDateString() === currentRange.to.toDateString();
};
// Generate calendar data
const calendarData = React.useMemo(() => {
const year = currentMonth.getFullYear();
const month = currentMonth.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);
// Build weeks until we've passed the last day and flushed any partial week
while (currentDateIter <= lastDay || currentWeek.length > 0) {
currentWeek.push(new Date(currentDateIter));
if (currentWeek.length === 7) {
weeks.push([...currentWeek]);
currentWeek = [];
// If we've already passed the month end and just flushed the trailing week, break
if (currentDateIter > lastDay) break;
}
currentDateIter.setDate(currentDateIter.getDate() + 1);
}
return {
year,
month,
monthName: new Intl.DateTimeFormat(locale, {
month: "long"
}).format(currentMonth),
weeks
};
}, [currentMonth, locale]);
// Navigate months
const navigateMonth = direction => {
setCurrentMonth(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 disabled
const isDisabled = date => {
if (minDate && date < minDate) return true;
if (maxDate && date > maxDate) return true;
return false;
};
// Check if date is today
const isToday = date => {
const today = new Date();
return date.toDateString() === today.toDateString();
};
// Check if date is in current month
const isCurrentMonth = date => {
return date.getMonth() === currentMonth.getMonth();
};
// Close popover when clicking outside
useEffect(() => {
const handleClickOutside = event => {
if (triggerRef.current && !triggerRef.current.contains(event.target) && popoverRef.current && !popoverRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const config = sizeConfigs[size];
return jsxs("div", {
"data-glass-component": true,
className: 'relative',
children: [jsxs("div", {
ref: triggerRef,
className: cn("relative flex items-center justify-between bg-white/10 glass-backdrop-blur-md border border-white/20", "glass-radius-lg cursor-pointer transition-all duration-200", "hover:bg-white/15 hover:border-white/30 focus-within:bg-white/15 focus-within:border-white/30", config.trigger, disabled && "opacity-50 cursor-not-allowed", className),
onClick: e => !disabled && setIsOpen(!isOpen),
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2 glass-flex-1 glass-min-w-0",
children: [jsx(Calendar, {
className: 'w-4 h-4 text-primary/60 glass-flex-shrink-0'
}), jsx("span", {
className: cn("truncate", !currentRange.from && !currentRange.to ? "glass-text-primary/50" : "glass-text-primary"),
children: getDisplayValue()
})]
}), jsx("div", {
className: "glass-flex glass-items-center glass-gap-1",
children: showClear && (currentRange.from || currentRange.to) && jsx("button", {
onClick: e => {
e.stopPropagation();
handleClear();
},
className: 'glass-p-1 hover:glass-surface-subtle/20 glass-radius-md transition-colors',
children: jsx(X, {
className: 'w-3 h-3 text-primary/60'
})
})
})]
}), isOpen && jsx(MotionFramer, {
preset: "fadeIn",
className: 'absolute z-50 glass-mt-2',
children: jsx("div", {
ref: popoverRef,
className: cn("bg-black/20 glass-backdrop-blur-md border border-white/20 glass-radius-xl shadow-2xl", config.calendar, popoverClassName),
children: jsx(GlassCard, {
variant: "outline",
className: 'glass-border-0 bg-transparent',
children: jsx(CardContent, {
className: "glass-p-4",
children: jsxs("div", {
className: "glass-flex glass-gap-6",
children: [presets && presets.length > 0 && jsxs("div", {
className: "glass-flex-shrink-0",
children: [jsx("h4", {
className: 'glass-text-sm font-medium text-primary/80 mb-3',
children: "Quick Select"
}), jsx("div", {
className: "glass-gap-1",
children: presets.map((preset, index) => jsx(GlassButton, {
variant: "ghost",
size: "sm",
onClick: e => handlePresetSelect(preset),
className: 'glass-w-full glass-justify-start text-left',
children: preset.label
}, preset.label))
})]
}), jsxs("div", {
className: "glass-flex-1",
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-justify-between mb-4',
children: [jsxs("h3", {
className: 'glass-text-lg font-semibold text-primary',
children: [calendarData.monthName, " ", calendarData.year]
}), jsxs("div", {
className: "glass-flex glass-gap-1",
children: [jsx(GlassButton, {
variant: "ghost",
size: "sm",
onClick: e => navigateMonth("prev"),
children: jsx(ChevronLeft, {
className: 'w-4 h-4'
})
}), jsx(GlassButton, {
variant: "ghost",
size: "sm",
onClick: e => navigateMonth("next"),
children: jsx(ChevronRight, {
className: 'w-4 h-4'
})
})]
})]
}), jsxs("div", {
className: "glass-gap-2",
children: [jsx("div", {
className: "glass-grid glass-grid-cols-7 glass-gap-1",
children: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map(day => jsx("div", {
className: 'text-center glass-text-sm font-medium text-primary/60 glass-py-2',
children: day
}, day))
}), jsx("div", {
className: "glass-grid glass-grid-cols-7 glass-gap-1",
children: calendarData.weeks.flat().map((date, index) => {
const isInRange = isDateInRange(date);
const isBoundary = isRangeBoundary(date);
const isDisabledDate = isDisabled(date);
const isCurrentMonthDate = isCurrentMonth(date);
const isTodayDate = isToday(date);
return jsx(MotionFramer, {
preset: "scaleIn",
className: 'aspect-square',
children: jsx("button", {
onClick: e => !isDisabledDate && handleDateSelect(date),
disabled: isDisabledDate,
className: cn("w-full h-full glass-radius-lg glass-text-sm font-medium transition-all duration-200", "flex items-center justify-center", "hover:bg-white/20 focus:bg-white/25 focus:outline-none", "disabled:opacity-50 disabled:cursor-not-allowed", {
"glass-text-primary/60": !isCurrentMonthDate,
"glass-text-primary": isCurrentMonthDate,
"bg-primary/20 text-primary-foreground": isBoundary,
"bg-primary/10": isInRange && !isBoundary,
"bg-white/10 glass-text-primary font-semibold": isTodayDate && !isInRange && !isBoundary,
"ring-2 ring-primary/50": isBoundary
}),
children: date.getDate()
})
}, index);
})
})]
}), jsxs("div", {
className: 'glass-flex glass-justify-between glass-items-center glass-mt-4 pt-4 glass-border-t glass-border-white/10',
children: [jsx("div", {
className: 'glass-text-sm text-primary/60',
children: selectingFrom ? "Select start date" : "Select end date"
}), jsxs("div", {
className: "glass-flex glass-gap-2",
children: [jsx(GlassButton, {
variant: "ghost",
size: "sm",
onClick: e => setIsOpen(false),
children: "Cancel"
}), jsx(GlassButton, {
variant: "primary",
size: "sm",
onClick: e => setIsOpen(false),
children: "Apply"
})]
})]
})]
})]
})
})
})
})
})]
});
};
export { GlassDateRangePicker, GlassDateRangePicker as default };
//# sourceMappingURL=GlassDateRangePicker.js.map