Files
blitzkit/packages/website/src/components/DatePicker/index.tsx
T

232 lines
6.3 KiB
TypeScript
Raw Normal View History

2024-06-09 21:00:40 -05:00
import {
ChevronLeftIcon,
ChevronRightIcon,
ChevronUpIcon,
ReloadIcon,
} from '@radix-ui/react-icons';
import {
Button,
ChevronDownIcon,
Flex,
Grid,
IconButton,
Link,
Popover,
Text,
} from '@radix-ui/themes';
2024-09-14 19:43:14 -05:00
import { times } from 'lodash-es';
2024-06-09 21:00:40 -05:00
import { useState } from 'react';
2024-11-02 18:52:50 -05:00
import './index.css';
2024-06-09 21:00:40 -05:00
const DAY_TITLES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const YEAR_OPTIONS = 7;
2024-06-10 13:06:02 -05:00
interface DatePickerProps {
2024-06-10 13:40:11 -05:00
onDateChange?: (date: Date) => void;
defaultDate?: Date;
2024-06-10 13:06:02 -05:00
}
2024-06-10 13:40:11 -05:00
export function DatePicker({
onDateChange,
defaultDate = new Date(),
}: DatePickerProps) {
const [year, setYear] = useState(defaultDate.getFullYear());
const [month, setMonth] = useState(defaultDate.getMonth());
const [day, setDay] = useState(defaultDate.getDate());
2024-06-10 13:06:02 -05:00
const [yearScroll, setYearScroll] = useState(0);
2024-06-09 21:00:40 -05:00
const [yearOffset, setYearOffset] = useState(0);
const [monthOffset, setMonthOffset] = useState(0);
const date = new Date(year + yearOffset, month + monthOffset, day);
2024-06-10 13:06:02 -05:00
const daysInLastMonth = new Date(
year + yearOffset,
month + monthOffset,
0,
).getDate();
const daysInMonth = new Date(
year + yearOffset,
month + monthOffset + 1,
0,
).getDate();
const firstDayOfMonth = new Date(
year + yearOffset,
month + monthOffset,
1,
).getDay();
2024-06-09 21:00:40 -05:00
const lastDayOfMonth = new Date(
2024-06-10 13:06:02 -05:00
year + yearOffset,
2024-06-09 21:00:40 -05:00
month + monthOffset,
daysInMonth,
).getDay();
return (
2024-11-02 18:52:50 -05:00
<Flex direction="column" width="fit-content" className="date-picker">
2024-06-09 21:00:40 -05:00
<Flex justify="between" mb="2" align="center" py="2">
<IconButton
size="3"
variant="ghost"
2024-06-10 13:06:02 -05:00
onClick={() => {
if (monthOffset === 0) {
setYearOffset((state) => state - 1);
setMonthOffset(11);
} else {
setMonthOffset((state) => state - 1);
}
}}
2024-06-09 21:00:40 -05:00
ml="2"
>
<ChevronLeftIcon />
</IconButton>
<Flex align="center" gap="2">
<IconButton
size="3"
variant="ghost"
onClick={() => {
2024-06-10 13:40:11 -05:00
setYear(defaultDate.getFullYear());
setMonth(defaultDate.getMonth());
setDay(defaultDate.getDate());
2024-06-09 21:00:40 -05:00
setYearOffset(0);
setMonthOffset(0);
}}
>
<ReloadIcon />
</IconButton>
<Flex gap="1">
2024-06-10 13:06:02 -05:00
<Text>
{date.toLocaleString(undefined, {
month: 'long',
})}
</Text>
2024-06-09 21:00:40 -05:00
2024-06-10 13:06:02 -05:00
<Popover.Root onOpenChange={() => setYearScroll(0)}>
2024-06-09 21:00:40 -05:00
<Popover.Trigger>
<Link underline="always" href="#">
{date.getFullYear()}
</Link>
</Popover.Trigger>
<Popover.Content side="right" align="center">
<Flex direction="column" gap="1">
<IconButton
variant="ghost"
2024-06-10 13:06:02 -05:00
onClick={() => setYearScroll((state) => state + 1)}
2024-06-09 21:00:40 -05:00
mb="2"
>
<ChevronUpIcon />
</IconButton>
{times(YEAR_OPTIONS, (index) => {
const thisYear =
2024-06-10 13:06:02 -05:00
year +
yearOffset -
index +
Math.floor(YEAR_OPTIONS / 2) +
yearScroll;
const selected = year + yearOffset === thisYear;
2024-06-09 21:00:40 -05:00
return (
2024-06-10 13:06:02 -05:00
<Popover.Close key={thisYear}>
<Button
variant={selected ? 'solid' : 'soft'}
radius="large"
onClick={() => {
setYearOffset(thisYear - year);
}}
>
{thisYear}
</Button>
</Popover.Close>
2024-06-09 21:00:40 -05:00
);
})}
<IconButton
variant="ghost"
2024-06-10 13:06:02 -05:00
onClick={() => setYearScroll((state) => state - 1)}
2024-06-09 21:00:40 -05:00
mt="2"
>
<ChevronDownIcon />
</IconButton>
</Flex>
</Popover.Content>
</Popover.Root>
</Flex>
</Flex>
<IconButton
size="3"
variant="ghost"
2024-06-10 13:06:02 -05:00
onClick={() => {
if (monthOffset === 11) {
setYearOffset((state) => state + 1);
setMonthOffset(0);
} else {
setMonthOffset((state) => state + 1);
}
}}
2024-06-09 21:00:40 -05:00
mr="2"
>
<ChevronRightIcon />
</IconButton>
</Flex>
<Flex mb="1">
{times(7, (index) => (
<Text
2024-11-02 18:52:50 -05:00
className="day"
2024-06-09 21:00:40 -05:00
size="2"
align="center"
key={index}
color="gray"
>
{DAY_TITLES[index]}
</Text>
))}
</Flex>
<Grid columns="7" gap="1" width="fit-content">
{times(firstDayOfMonth, (index) => (
<IconButton radius="large" key={index} disabled>
{daysInLastMonth - firstDayOfMonth + index + 1}
</IconButton>
))}
{times(daysInMonth, (index) => {
2024-06-10 13:06:02 -05:00
const selected =
index === day - 1 && monthOffset === 0 && yearOffset === 0;
2024-06-09 21:00:40 -05:00
return (
<IconButton
radius="large"
key={index}
variant={selected ? 'solid' : 'soft'}
onClick={() => {
2024-06-10 13:06:02 -05:00
const newYear =
year + yearOffset + Math.floor((month + monthOffset) / 12);
const newMonth = (month + monthOffset) % 12;
const newDay = index + 1;
setYear(newYear);
setMonth(newMonth);
setDay(newDay);
2024-06-09 21:00:40 -05:00
setYearOffset(0);
setMonthOffset(0);
2024-06-10 13:06:02 -05:00
2024-06-10 13:40:11 -05:00
onDateChange?.(new Date(newYear, newMonth, newDay));
2024-06-09 21:00:40 -05:00
}}
>
{index + 1}
</IconButton>
);
})}
{times(6 - lastDayOfMonth, (index) => (
<IconButton radius="large" key={index} disabled>
{index + 1}
</IconButton>
))}
</Grid>
2024-06-10 13:40:11 -05:00
</Flex>
2024-06-09 21:00:40 -05:00
);
}