- Implemented shadcn/ui date picker with Czech localization - Added month/year dropdown navigation for easy date selection - Fixed critical timezone bug causing "No valid days found" error - Changed from toISOString() to local date formatting - Dates now correctly sent as 2025-01-01 instead of 2024-12-31 - Calendar auto-closes after date selection - All features tested and working: - Journey calculation with correct date ranges - "Vyplnit na web" button visible and functional - Excel export working - Backend successfully processes January 2025 data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
145 lines
5.3 KiB
TypeScript
145 lines
5.3 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
import { DayPicker, getDefaultClassNames } from "react-day-picker"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
import { buttonVariants } from "@/components/ui/button"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
|
|
export type CalendarProps = React.ComponentProps<typeof DayPicker> & {
|
|
buttonVariant?: "ghost" | "outline"
|
|
}
|
|
|
|
function Calendar({
|
|
className,
|
|
classNames,
|
|
showOutsideDays = true,
|
|
captionLayout = "label",
|
|
buttonVariant = "ghost",
|
|
...props
|
|
}: CalendarProps) {
|
|
const defaultClassNames = getDefaultClassNames()
|
|
|
|
return (
|
|
<DayPicker
|
|
showOutsideDays={showOutsideDays}
|
|
className={cn(
|
|
"bg-background group/calendar p-3 [--cell-size:2rem]",
|
|
className
|
|
)}
|
|
captionLayout={captionLayout}
|
|
classNames={{
|
|
root: cn("w-fit", defaultClassNames.root),
|
|
months: cn(
|
|
"relative flex flex-col gap-4 md:flex-row",
|
|
defaultClassNames.months
|
|
),
|
|
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
|
month_caption: cn(
|
|
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size] text-sm font-medium",
|
|
defaultClassNames.month_caption
|
|
),
|
|
dropdowns: cn(
|
|
"flex items-center gap-1",
|
|
defaultClassNames.dropdowns
|
|
),
|
|
dropdown: cn(
|
|
"flex h-[--cell-size] items-center justify-center rounded-md border border-input bg-background px-3 text-sm font-medium ring-offset-background hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
defaultClassNames.dropdown
|
|
),
|
|
dropdown_icon: cn("ml-2 h-4 w-4", defaultClassNames.dropdown_icon),
|
|
weekdays: cn(
|
|
"mt-2 flex w-full flex-row",
|
|
defaultClassNames.weekdays
|
|
),
|
|
weekday: cn(
|
|
"flex h-[--cell-size] w-[--cell-size] items-center justify-center text-xs font-normal text-muted-foreground",
|
|
defaultClassNames.weekday
|
|
),
|
|
week: cn("mt-0.5 flex w-full flex-row", defaultClassNames.week),
|
|
day: cn(
|
|
"flex h-[--cell-size] w-[--cell-size] items-center justify-center rounded-md p-0 text-sm",
|
|
defaultClassNames.day
|
|
),
|
|
day_button: cn(
|
|
buttonVariants({ variant: buttonVariant }),
|
|
"h-[--cell-size] w-[--cell-size] select-none p-0 font-normal hover:bg-accent hover:text-accent-foreground focus-visible:z-10 aria-selected:opacity-100",
|
|
defaultClassNames.day_button
|
|
),
|
|
range_start: cn("day-range-start", defaultClassNames.range_start),
|
|
range_end: cn("day-range-end", defaultClassNames.range_end),
|
|
selected: cn(
|
|
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
defaultClassNames.selected
|
|
),
|
|
today: cn(
|
|
"bg-accent text-accent-foreground",
|
|
defaultClassNames.today
|
|
),
|
|
outside: cn(
|
|
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
|
defaultClassNames.outside
|
|
),
|
|
disabled: cn(
|
|
"text-muted-foreground opacity-50",
|
|
defaultClassNames.disabled
|
|
),
|
|
range_middle: cn(
|
|
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
|
defaultClassNames.range_middle
|
|
),
|
|
hidden: cn("invisible", defaultClassNames.hidden),
|
|
...classNames,
|
|
}}
|
|
components={{
|
|
Chevron: ({ orientation }) => {
|
|
const Icon = orientation === "left" ? ChevronLeft : ChevronRight
|
|
return <Icon className="h-4 w-4" />
|
|
},
|
|
Dropdown: (props) => {
|
|
const { value, onChange, options } = props
|
|
const selected = options?.find((option) => option.value === value)
|
|
const handleChange = (newValue: string) => {
|
|
const changeEvent = {
|
|
target: { value: newValue },
|
|
} as React.ChangeEvent<HTMLSelectElement>
|
|
onChange?.(changeEvent)
|
|
}
|
|
|
|
return (
|
|
<Select value={value?.toString()} onValueChange={handleChange}>
|
|
<SelectTrigger className="h-7 w-fit gap-1 border-none px-2 py-1 font-medium shadow-none hover:bg-accent focus:ring-0">
|
|
<SelectValue>{selected?.label}</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent position="popper" className="min-w-[var(--radix-popper-anchor-width)]">
|
|
{options?.map((option) => (
|
|
<SelectItem
|
|
key={option.value}
|
|
value={option.value?.toString() ?? ""}
|
|
disabled={option.disabled}
|
|
>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)
|
|
},
|
|
}}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
Calendar.displayName = "Calendar"
|
|
|
|
export { Calendar }
|