for-legacy-web/src/components/navigation/SearchDatePicker.tsx

377 lines
12 KiB
TypeScript

import styled from "styled-components/macro";
import { useState, useEffect, useRef } from "preact/hooks";
import { ChevronLeft, ChevronRight } from "@styled-icons/boxicons-regular";
const Base = styled.div`
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: var(--primary-background);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1002;
overflow: hidden;
padding: 12px;
min-width: 280px;
@media (max-width: 768px) {
min-width: 240px;
right: -20px;
}
`;
const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding: 0 4px;
`;
const MonthYear = styled.div`
font-weight: 600;
color: var(--foreground);
font-size: 14px;
`;
const NavButton = styled.button`
background: transparent;
border: none;
color: var(--tertiary-foreground);
cursor: pointer;
padding: 4px;
border-radius: 4px;
display: flex;
align-items: center;
transition: all 0.1s ease;
&:hover {
background: var(--secondary-background);
color: var(--foreground);
}
`;
const WeekDays = styled.div`
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-bottom: 8px;
`;
const WeekDay = styled.div`
text-align: center;
font-size: 11px;
font-weight: 600;
color: var(--tertiary-foreground);
text-transform: uppercase;
padding: 4px;
`;
const Days = styled.div`
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
`;
const Day = styled.button<{
isToday?: boolean;
isSelected?: boolean;
isOtherMonth?: boolean;
isRangeStart?: boolean;
isRangeEnd?: boolean;
isInRange?: boolean;
isInHoverRange?: boolean;
}>`
background: ${props => {
if (props.isSelected || props.isRangeStart || props.isRangeEnd) return 'var(--accent)';
if (props.isInRange) return 'var(--accent-disabled)';
if (props.isInHoverRange) return 'var(--hover)';
return 'transparent';
}};
color: ${props => {
if (props.isSelected || props.isRangeStart || props.isRangeEnd) return 'var(--accent-contrast)';
if (props.isOtherMonth) return 'var(--tertiary-foreground)';
return 'var(--foreground)';
}};
border: none;
padding: 8px;
border-radius: ${props => {
if (props.isRangeStart || (props.isInHoverRange && props.isOtherMonth === false && !props.isRangeEnd)) return '4px 0 0 4px';
if (props.isRangeEnd || (props.isInHoverRange && props.isOtherMonth === false && !props.isRangeStart)) return '0 4px 4px 0';
if (props.isInRange || props.isInHoverRange) return '0';
return '4px';
}};
cursor: pointer;
font-size: 13px;
transition: all 0.1s ease;
position: relative;
${props => props.isToday && `
&::after {
content: '';
position: absolute;
bottom: 2px;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 4px;
border-radius: 50%;
background: ${props.isSelected || props.isRangeStart || props.isRangeEnd ? 'var(--accent-contrast)' : 'var(--accent)'};
}
`}
&:hover {
background: ${props => {
if (props.isSelected || props.isRangeStart || props.isRangeEnd) return 'var(--accent)';
return 'var(--secondary-background)';
}};
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
`;
const QuickSelects = styled.div`
display: flex;
gap: 4px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--secondary-background);
`;
const RangePreview = styled.div`
margin: 12px 0;
padding: 8px 12px;
background: var(--secondary-background);
border-radius: 4px;
font-size: 13px;
color: var(--foreground);
text-align: center;
.hint {
color: var(--tertiary-foreground);
font-size: 12px;
margin-top: 4px;
}
`;
const QuickSelect = styled.button`
background: transparent;
border: 1px solid var(--secondary-background);
color: var(--foreground);
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.1s ease;
&:hover {
background: var(--secondary-background);
border-color: var(--tertiary-background);
}
`;
interface Props {
onSelect: (date: Date) => void;
onRangeSelect?: (startDate: Date, endDate: Date) => void;
filterType: "before" | "after" | "during" | "between";
}
export default function SearchDatePicker({ onSelect, onRangeSelect, filterType }: Props) {
const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [rangeStart, setRangeStart] = useState<Date | null>(null);
const [rangeEnd, setRangeEnd] = useState<Date | null>(null);
const [hoverDate, setHoverDate] = useState<Date | null>(null);
const monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"];
const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const getDaysInMonth = (date: Date) => {
const year = date.getFullYear();
const month = date.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
const days: Array<{ date: Date; isOtherMonth: boolean }> = [];
// Previous month days
const prevMonthLastDay = new Date(year, month, 0).getDate();
for (let i = startingDayOfWeek - 1; i >= 0; i--) {
days.push({
date: new Date(year, month - 1, prevMonthLastDay - i),
isOtherMonth: true
});
}
// Current month days
for (let i = 1; i <= daysInMonth; i++) {
days.push({
date: new Date(year, month, i),
isOtherMonth: false
});
}
// Next month days to fill the grid
const remainingDays = 42 - days.length; // 6 weeks * 7 days
for (let i = 1; i <= remainingDays; i++) {
days.push({
date: new Date(year, month + 1, i),
isOtherMonth: true
});
}
return days;
};
const isToday = (date: Date) => {
const today = new Date();
return date.toDateString() === today.toDateString();
};
const isSameDate = (date1: Date, date2: Date | null) => {
if (!date2) return false;
return date1.toDateString() === date2.toDateString();
};
const isDateInRange = (date: Date) => {
if (!rangeStart || !rangeEnd) return false;
return date >= rangeStart && date <= rangeEnd;
};
const isDateInHoverRange = (date: Date) => {
if (filterType !== "between" || !rangeStart || rangeEnd || !hoverDate) return false;
const start = rangeStart < hoverDate ? rangeStart : hoverDate;
const end = rangeStart < hoverDate ? hoverDate : rangeStart;
return date >= start && date <= end;
};
const handleDateSelect = (date: Date) => {
if (filterType === "between") {
if (!rangeStart || (rangeStart && rangeEnd)) {
// First click or reset
setRangeStart(date);
setRangeEnd(null);
} else {
// Second click
if (date < rangeStart) {
setRangeEnd(rangeStart);
setRangeStart(date);
} else {
setRangeEnd(date);
}
// Call the callback with both dates
if (onRangeSelect) {
const start = date < rangeStart ? date : rangeStart;
const end = date < rangeStart ? rangeStart : date;
onRangeSelect(start, end);
}
}
} else {
setSelectedDate(date);
onSelect(date);
}
};
const handleQuickSelect = (option: string) => {
const today = new Date();
let date: Date;
switch (option) {
case "today":
date = today;
break;
case "yesterday":
date = new Date(today);
date.setDate(today.getDate() - 1);
break;
case "week":
date = new Date(today);
date.setDate(today.getDate() - 7);
break;
case "month":
date = new Date(today);
date.setMonth(today.getMonth() - 1);
break;
default:
return;
}
handleDateSelect(date);
};
const navigateMonth = (direction: number) => {
const newMonth = new Date(currentMonth);
newMonth.setMonth(currentMonth.getMonth() + direction);
setCurrentMonth(newMonth);
};
const days = getDaysInMonth(currentMonth);
return (
<Base data-date-picker onClick={(e) => e.stopPropagation()}>
<Header>
<NavButton onClick={() => navigateMonth(-1)}>
<ChevronLeft size={20} />
</NavButton>
<MonthYear>
{filterType === "between" && rangeStart && !rangeEnd
? "Select end date"
: `${monthNames[currentMonth.getMonth()]} ${currentMonth.getFullYear()}`
}
</MonthYear>
<NavButton onClick={() => navigateMonth(1)}>
<ChevronRight size={20} />
</NavButton>
</Header>
<WeekDays>
{weekDays.map(day => (
<WeekDay key={day}>{day}</WeekDay>
))}
</WeekDays>
<Days>
{days.map((day, index) => (
<Day
key={index}
onClick={() => handleDateSelect(day.date)}
onMouseEnter={() => setHoverDate(day.date)}
onMouseLeave={() => setHoverDate(null)}
isToday={isToday(day.date)}
isSelected={filterType !== "between" && isSameDate(day.date, selectedDate)}
isRangeStart={filterType === "between" && rangeStart && isSameDate(day.date, rangeStart)}
isRangeEnd={filterType === "between" && rangeEnd && isSameDate(day.date, rangeEnd)}
isInRange={filterType === "between" && isDateInRange(day.date)}
isInHoverRange={isDateInHoverRange(day.date)}
isOtherMonth={day.isOtherMonth}
>
{day.date.getDate()}
</Day>
))}
</Days>
<QuickSelects>
<QuickSelect onClick={() => handleQuickSelect("today")}>
Today
</QuickSelect>
<QuickSelect onClick={() => handleQuickSelect("yesterday")}>
Yesterday
</QuickSelect>
<QuickSelect onClick={() => handleQuickSelect("week")}>
Last week
</QuickSelect>
<QuickSelect onClick={() => handleQuickSelect("month")}>
Last month
</QuickSelect>
</QuickSelects>
</Base>
);
}