fix(ui): optimize bundle size and fix calendar crash

- optimize vite manualChunks to reduce bundle sizes
- update calendar to react-day-picker v9 API
- fix calendar crash by removing broken drag-to-select
- improve calendar UI with compact styling and better navigation
- resolve fast-refresh lint error by separating button variants
This commit is contained in:
kaitranntt
2025-12-17 18:04:50 -05:00
parent 1eec1bb4d2
commit 572703f439
3 changed files with 187 additions and 56 deletions
@@ -2,16 +2,18 @@
* Date Range Filter Component
*
* Provides date range selection with preset options for analytics.
* Uses react-day-picker for date selection UI.
* Uses react-day-picker for date selection UI within a Popover.
*/
import React from 'react';
import { format } from 'date-fns';
import type { DateRange } from 'react-day-picker';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { format, subDays } from 'date-fns';
import { CalendarIcon } from 'lucide-react';
import type { DateRange } from 'react-day-picker';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
interface DateRangeFilterProps {
value?: DateRange;
@@ -26,70 +28,94 @@ interface DateRangeFilterProps {
export function DateRangeFilter({
value,
onChange,
presets = [],
presets = [
{
label: 'Last 7 days',
range: {
from: subDays(new Date(), 7),
to: new Date(),
},
},
{
label: 'Last 30 days',
range: {
from: subDays(new Date(), 30),
to: new Date(),
},
},
{
label: 'Last 90 days',
range: {
from: subDays(new Date(), 90),
to: new Date(),
},
},
],
className,
}: DateRangeFilterProps) {
const handlePresetClick = (range: DateRange) => {
onChange(range);
};
const [isOpen, setIsOpen] = React.useState(false);
const handleFromChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const from = e.target.value ? new Date(e.target.value) : undefined;
onChange({ from, to: value?.to });
};
// Helper to check if a preset is currently selected
const isPresetSelected = (presetRange: DateRange) => {
if (!value || !value.from || !value.to || !presetRange.from || !presetRange.to) {
return false;
}
const handleToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const to = e.target.value ? new Date(e.target.value) : undefined;
onChange({ from: value?.from, to });
// Compare dates (ignoring time components if needed, but day-picker usually returns start of day)
// Simple comparison using formatted strings or timestamps
return (
format(value.from, 'yyyy-MM-dd') === format(presetRange.from, 'yyyy-MM-dd') &&
format(value.to, 'yyyy-MM-dd') === format(presetRange.to, 'yyyy-MM-dd')
);
};
return (
<div className={cn('flex flex-wrap items-center gap-2', className)}>
{/* Preset Buttons */}
{presets.map((preset, index) => (
<div className={cn('flex items-center gap-2', className)}>
{presets.map((preset) => (
<Button
key={index}
variant={isSameRange(value, preset.range) ? 'default' : 'outline'}
key={preset.label}
variant={isPresetSelected(preset.range) ? 'default' : 'outline'}
size="sm"
onClick={() => handlePresetClick(preset.range)}
onClick={() => onChange(preset.range)}
>
{preset.label}
</Button>
))}
{/* Custom Date Range Inputs */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<CalendarIcon className="h-4 w-4 text-muted-foreground" />
<Input
type="date"
value={value?.from ? format(value.from, 'yyyy-MM-dd') : ''}
onChange={handleFromChange}
placeholder="From"
className="w-40"
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
id="date"
variant={'outline'}
className={cn(
'w-auto min-w-[240px] justify-start text-left font-normal',
!value && 'text-muted-foreground'
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value?.from ? (
value.to ? (
<>
{format(value.from, 'LLL dd, y')} - {format(value.to, 'LLL dd, y')}
</>
) : (
format(value.from, 'LLL dd, y')
)
) : (
<span>Pick a date</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<Calendar
initialFocus
mode="range"
defaultMonth={value?.from}
selected={value}
onSelect={onChange}
numberOfMonths={2}
/>
</div>
<span className="text-muted-foreground">to</span>
<Input
type="date"
value={value?.to ? format(value.to, 'yyyy-MM-dd') : ''}
onChange={handleToChange}
placeholder="To"
className="w-40"
/>
</div>
</PopoverContent>
</Popover>
</div>
);
}
// Helper to compare date ranges
function isSameRange(a?: DateRange, b?: DateRange): boolean {
if (!a || !b) return a === b;
const fromA = a.from?.getTime() ?? 0;
const fromB = b.from?.getTime() ?? 0;
const toA = a.to?.getTime() ?? 0;
const toB = b.to?.getTime() ?? 0;
return fromA === fromB && toA === toB;
}
+90
View File
@@ -0,0 +1,90 @@
import * as React from 'react';
import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from 'lucide-react';
import { DayPicker } from 'react-day-picker';
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button-variants';
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn('p-2', className)}
classNames={{
root: 'relative',
months: 'flex flex-col sm:flex-row gap-2 sm:gap-4',
month: 'flex flex-col gap-2',
month_caption: 'flex justify-center pt-1 relative items-center h-8',
caption_label: 'text-sm font-medium',
nav: 'absolute inset-x-0 top-2 flex items-center justify-between px-1 z-10',
button_previous: cn(
buttonVariants({ variant: 'ghost' }),
'h-7 w-7 bg-transparent p-0 opacity-70 hover:opacity-100 hover:bg-accent rounded-md'
),
button_next: cn(
buttonVariants({ variant: 'ghost' }),
'h-7 w-7 bg-transparent p-0 opacity-70 hover:opacity-100 hover:bg-accent rounded-md'
),
month_grid: 'w-full border-collapse',
weekdays: 'flex w-full',
weekday: 'text-muted-foreground w-8 font-normal text-xs text-center',
weeks: 'flex flex-col',
week: 'flex w-full',
day: cn(
'relative h-8 w-8 p-0 text-center text-xs font-normal focus-within:relative focus-within:z-20',
'hover:bg-accent hover:text-accent-foreground rounded-full select-none'
),
day_button: cn(
buttonVariants({ variant: 'ghost' }),
'h-8 w-8 p-0 font-normal rounded-full text-xs',
'aria-selected:opacity-100'
),
// Range Selection Styles
range_start: cn(
'aria-selected:bg-primary aria-selected:text-primary-foreground',
'aria-selected:rounded-l-full aria-selected:rounded-r-none',
'aria-selected:hover:bg-primary aria-selected:hover:text-primary-foreground'
),
range_end: cn(
'aria-selected:bg-primary aria-selected:text-primary-foreground',
'aria-selected:rounded-r-full aria-selected:rounded-l-none',
'aria-selected:hover:bg-primary aria-selected:hover:text-primary-foreground'
),
range_middle: cn(
'aria-selected:bg-accent aria-selected:text-accent-foreground',
'aria-selected:rounded-none'
),
selected: cn(
'bg-primary text-primary-foreground rounded-full',
'hover:bg-primary hover:text-primary-foreground',
'focus:bg-primary focus:text-primary-foreground'
),
today: 'bg-accent text-accent-foreground rounded-full',
outside:
'text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
disabled: 'text-muted-foreground opacity-50',
hidden: 'invisible',
...classNames,
}}
components={{
Chevron: ({ orientation, className, ...props }) => {
const Icon =
orientation === 'left'
? ChevronLeft
: orientation === 'right'
? ChevronRight
: orientation === 'up'
? ChevronUp
: ChevronDown;
return <Icon className={cn('h-4 w-4', className)} {...props} />;
},
}}
{...props}
/>
);
}
Calendar.displayName = 'Calendar';
export { Calendar };
+15
View File
@@ -23,16 +23,31 @@ export default defineConfig({
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'radix-ui': [
'@radix-ui/react-alert-dialog',
'@radix-ui/react-checkbox',
'@radix-ui/react-collapsible',
'@radix-ui/react-dialog',
'@radix-ui/react-dropdown-menu',
'@radix-ui/react-label',
'@radix-ui/react-popover',
'@radix-ui/react-scroll-area',
'@radix-ui/react-select',
'@radix-ui/react-separator',
'@radix-ui/react-slot',
'@radix-ui/react-switch',
'@radix-ui/react-tabs',
'@radix-ui/react-tooltip',
],
'tanstack': ['@tanstack/react-query', '@tanstack/react-table'],
'form-utils': ['react-hook-form', '@hookform/resolvers', 'zod'],
'icons': ['lucide-react'],
// Charts - large library, separate chunk
'charts': ['recharts'],
// Code editor / syntax highlighting
'code-highlight': ['prism-react-renderer'],
// Notifications
'notifications': ['sonner'],
// Utilities
'utils': ['date-fns', 'clsx', 'class-variance-authority', 'tailwind-merge', 'yaml'],
},
},
},