Final working solution: shadcn date picker with timezone fix
- 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>
This commit is contained in:
144
frontend/components/ui/calendar.tsx
Normal file
144
frontend/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
"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 }
|
||||
Reference in New Issue
Block a user