From fbd39bb67f25341d0955c5c2ce66e763922767de Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 21 Jun 2025 23:43:35 +0000 Subject: [PATCH] replace available from component with a nicer search which takes human input --- .../frontend/src/components/Parameters.tsx | 72 +++--- .../frontend/src/components/ui/DatePicker.tsx | 111 ++++++++++ .../frontend/src/components/ui/calendar.tsx | 208 ++++++++++++++++++ .../frontend/src/components/ui/popover.tsx | 46 ++++ 4 files changed, 390 insertions(+), 47 deletions(-) create mode 100644 crawler/frontend/src/components/ui/DatePicker.tsx create mode 100644 crawler/frontend/src/components/ui/calendar.tsx create mode 100644 crawler/frontend/src/components/ui/popover.tsx diff --git a/crawler/frontend/src/components/Parameters.tsx b/crawler/frontend/src/components/Parameters.tsx index b30439f..1e356ef 100644 --- a/crawler/frontend/src/components/Parameters.tsx +++ b/crawler/frontend/src/components/Parameters.tsx @@ -4,11 +4,10 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { Button } from "./ui/button"; -import { Calendar } from "./ui/calendar"; +import { Calendar29 } from "./ui/DatePicker"; import { Dialog, DialogContent, DialogTrigger } from "./ui/dialog"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form"; import { Input } from "./ui/input"; -import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; @@ -47,6 +46,7 @@ export function Parameters( register, } = useForm() const [action, setAction] = useState<'fetch-data' | 'visualize' | null>(null) + const [availableFromRawInput, setAvailableFromRawInput] = useState("now"); const formSchema = z.object({ metric: z.nativeEnum(Metric, { required_error: "Metric is required" }), @@ -63,11 +63,12 @@ export function Parameters( resolver: zodResolver(formSchema), defaultValues: { metric: Metric.qmprice, + listing_type: ListingType.RENT, min_bedrooms: 1, max_bedrooms: 3, max_price: 3000, min_price: 2000, - min_sqm: 0, + min_sqm: 50, last_seen_days: 7, available_from: new Date(), }, @@ -81,7 +82,6 @@ export function Parameters( props.onSubmit(action, values) } } - const now = new Date(); @@ -144,6 +144,19 @@ export function Parameters( )} /> + ( + + Min square meters + + field.onChange(Number(e.target.value))} /> + + + + )} + /> ( - - Min square meters - - field.onChange(Number(e.target.value))} /> + + Available from + + + + Applicable for renting listings only + )} @@ -222,44 +238,6 @@ export function Parameters( )} /> - ( - - Available from - - - - - - - - - date < new Date() || date > new Date(now.setMonth(now.getMonth() + 3)) - } - captionLayout="dropdown" - /> - - - - Applicable for renting listings only - - - - )} - /> diff --git a/crawler/frontend/src/components/ui/DatePicker.tsx b/crawler/frontend/src/components/ui/DatePicker.tsx new file mode 100644 index 0000000..6d592d6 --- /dev/null +++ b/crawler/frontend/src/components/ui/DatePicker.tsx @@ -0,0 +1,111 @@ +"use client" + +import { parseDate } from "chrono-node" +import { CalendarIcon } from "lucide-react" +import * as React from "react" + +import { Button } from "@/components/ui/button" +import { Calendar } from "@/components/ui/calendar" +import { Input } from "@/components/ui/input" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" + +function formatDate(date: Date | undefined) { + if (!date) { + return "" + } + + return date.toLocaleDateString("en-US", { + day: "2-digit", + month: "long", + year: "numeric", + }) +} + +export function Calendar29( + props: { + onSelect?: (date?: Date) => void, + selected?: Date | undefined, + rawInputValue?: string | undefined + onChangeRawInputValue?: (rawInputValue: string) => void + } +) { + const [open, setOpen] = React.useState(false) + const [value, setValue] = React.useState(props.rawInputValue ?? "now") + + const [date, setDate] = React.useState( + parseDate(value) || undefined + ) + const [month, setMonth] = React.useState(date) + + return ( +
+
+ { + setValue(e.target.value) + if (props.onChangeRawInputValue) { + props.onChangeRawInputValue(e.target.value) + } + const date = parseDate(e.target.value) + if (date) { + setDate(date) + setMonth(date) + if (props.onSelect) { + props.onSelect(date) + } + } + }} + onKeyDown={(e) => { + if (e.key === "ArrowDown") { + e.preventDefault() + setOpen(true) + } + }} + /> + + + + + + { + setDate(date) + setValue(formatDate(date)) + if (props.onChangeRawInputValue) { + props.onChangeRawInputValue(formatDate(date)) + } + setOpen(false) + if (props.onSelect) { + props.onSelect(date) + } + }} + /> + + +
+
+ {formatDate(date)}. +
+
+ ) +} diff --git a/crawler/frontend/src/components/ui/calendar.tsx b/crawler/frontend/src/components/ui/calendar.tsx new file mode 100644 index 0000000..a0a553d --- /dev/null +++ b/crawler/frontend/src/components/ui/calendar.tsx @@ -0,0 +1,208 @@ +import * as React from "react" +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react" +import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"] +}) { + const defaultClassNames = getDefaultClassNames() + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }} + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "flex gap-4 flex-col md:flex-row relative", + defaultClassNames.months + ), + month: cn("flex flex-col w-full gap-4", defaultClassNames.month), + nav: cn( + "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_next + ), + month_caption: cn( + "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", + defaultClassNames.dropdown_root + ), + dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", + defaultClassNames.weekday + ), + week: cn("flex w-full mt-2", defaultClassNames.week), + week_number_header: cn( + "select-none w-(--cell-size)", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] select-none text-muted-foreground", + defaultClassNames.week_number + ), + day: cn( + "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", + defaultClassNames.day + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), + today: cn( + "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", + defaultClassNames.today + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ) + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ) + } + + if (orientation === "right") { + return ( + + ) + } + + return ( + + ) + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ) + }, + ...components, + }} + {...props} + /> + ) +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames() + + const ref = React.useRef(null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) + + return ( +