Skip to content

Commit

Permalink
fix: make date picker work with specific times (fixes #15)
Browse files Browse the repository at this point in the history
  • Loading branch information
tschoffelen committed Sep 7, 2024
1 parent b3e8555 commit d56c9c6
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 126 deletions.
2 changes: 1 addition & 1 deletion packages/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"lucide-react": "^0.437.0",
"react": "^18.3.1",
"react-cytoscapejs": "^2.0.0",
"react-date-range": "^2.0.1",
"react-day-picker": "8.10.1",
"react-dom": "^18.3.1",
"react-json-tree": "^0.19.0",
"react-router-dom": "^6.26.1",
Expand Down
193 changes: 107 additions & 86 deletions packages/dashboard/src/components/layout/date-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
import { createContext, useContext, useState } from "react";
import { DateRangePicker, createStaticRanges } from "react-date-range";
import { Calendar } from "lucide-react";
import { subDays, format, subHours } from "date-fns";
import { enGB } from "date-fns/locale";
import { createContext, useContext, useEffect, useState } from "react";
import { CalendarIcon } from "lucide-react";
import { subDays, format, subHours, parse } from "date-fns";

import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";

import "react-date-range/dist/styles.css";
import "react-date-range/dist/theme/default.css";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import { Input } from "@/components/ui/input";

const store = createContext({});
export const useDateRange = () => useContext(store);
const { Provider } = store;

const ranges = [
{
label: "Last hours",
startDate: subHours(new Date(), 1),
endDate: new Date(),
},
{
label: "Last 4 hours",
startDate: subHours(new Date(), 4),
endDate: new Date(),
},
{
label: "Last 24 hours",
startDate: subDays(new Date(), 1),
endDate: new Date(),
},
{
label: "Last 3 days",
startDate: subDays(new Date(), 3),
endDate: new Date(),
},
{
label: "Last 7 days",
startDate: subDays(new Date(), 7),
endDate: new Date(),
},
];

export const DateRangeProvider = ({ children }) => {
const [startDate, setStartDate] = useState(subDays(new Date(), 1));
const [endDate, setEndDate] = useState(new Date());
const [startDate, setStartDate] = useState(ranges[2].startDate);
const [endDate, setEndDate] = useState(ranges[2].endDate);
const [label, setLabel] = useState("Last 24 hours");

return (
Expand All @@ -43,76 +68,26 @@ const DatePicker = ({}) => {
const { startDate, endDate, label, setStartDate, setEndDate, setLabel } =
useDateRange();
const [open, setOpen] = useState(false);
const [dateRange, setDateRange] = useState([
{ startDate, endDate, key: "selection" },
]);

const handleClose = (range) => {
setOpen(false);
const { startDate, endDate } = range[0];
const selected = ranges.find(
(option) => option.isSelected(range[0]) === true,
useEffect(() => {
const range = ranges.find(
(range) =>
range.startDate.valueOf() === startDate.valueOf() &&
range.endDate.valueOf() === endDate.valueOf(),
);
const label = selected
? selected.label
: `${format(startDate, "PP")}${format(endDate, "PP")}`;

setStartDate(startDate);
setEndDate(endDate);
setLabel(label);
};
setLabel(
range?.label || `${format(startDate, "PP")}${format(endDate, "PP")}`,
);
}, [startDate, endDate]);

const getDayIndex = (date) => Math.floor(date.valueOf() / 86400000);
const handleClose = ({ to, from, label = "" }) => {
setOpen(false);

const handleSelect = (item) => {
setDateRange([item.selection]);
if (
getDayIndex(item.selection.startDate) !==
getDayIndex(item.selection.endDate)
) {
// automatically close after a new range has been selected
handleClose([item.selection]);
}
setStartDate(to);
setEndDate(from);
setLabel(label || `${format(to, "PP")}${format(from, "PP")}`);
};

const ranges = createStaticRanges([
{
label: "Last hours",
range: () => ({
startDate: subHours(new Date(), 1),
endDate: new Date(),
}),
},
{
label: "Last 4 hours",
range: () => ({
startDate: subHours(new Date(), 4),
endDate: new Date(),
}),
},
{
label: "Last 24 hours",
range: () => ({
startDate: subDays(new Date(), 1),
endDate: new Date(),
}),
},
{
label: "Last 3 days",
range: () => ({
startDate: subDays(new Date(), 3),
endDate: new Date(),
}),
},
{
label: "Last 7 days",
range: () => ({
startDate: subDays(new Date(), 7),
endDate: new Date(),
}),
},
]);

return (
<Popover open={open} onOpenChange={(open) => setOpen(open)}>
<PopoverTrigger asChild>
Expand All @@ -121,21 +96,67 @@ const DatePicker = ({}) => {
onClick={() => setOpen(true)}
className="px-3"
>
<Calendar className="h-[1.1rem] w-[1.1rem] mr-2" />
<CalendarIcon className="h-[1.1rem] w-[1.1rem] mr-2" />
{label}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<DateRangePicker
locale={enGB}
onChange={handleSelect}
minDate={new Date("2020-01-01")} // TODO: define based on data retention config
maxDate={new Date()}
ranges={dateRange}
shownDate={dateRange[0].endDate}
direction="horizontal"
staticRanges={ranges}
/>
<div className="flex divide-x">
<div className="p-3 w-40">
{ranges.map((option) => (
<Button
key={option.label}
variant={option.label === label ? "default" : "ghost"}
className="w-full justify-start"
onClick={() =>
handleClose({
to: option.startDate,
from: option.endDate,
label: option.label,
})
}
>
{option.label}
</Button>
))}
</div>
<div>
<Calendar
className="p-4 flex-1"
initialFocus
mode="range"
defaultMonth={startDate}
selected={{ from: startDate, to: endDate }}
onSelect={(range) => {
if (range?.from) {
setStartDate(range.from);
}
if (range?.to) {
setEndDate(range.to);
}
}}
/>
<div className="flex items-center justify-between gap-2 p-4 pt-0">
<Input
type="time"
className="flex-1"
value={format(startDate, "HH:mm")}
onChange={(e) =>
setStartDate(parse(e.target.value, "HH:mm", startDate))
}
/>
<span className="text-muted">&ndash;</span>
<Input
type="time"
className="flex-1"
value={format(endDate, "HH:mm")}
onChange={(e) =>
setEndDate(parse(e.target.value, "HH:mm", endDate))
}
/>
</div>
</div>
</div>
</PopoverContent>
</Popover>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/dashboard/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const buttonVariants = cva(
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
"bg-primary text-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
Expand Down
64 changes: 64 additions & 0 deletions packages/dashboard/src/components/ui/calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"

import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"

export type CalendarProps = React.ComponentProps<typeof DayPicker>

function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"

export { Calendar }
Loading

0 comments on commit d56c9c6

Please sign in to comment.