mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 06:17:09 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user