158 lines
7.0 KiB
TypeScript
158 lines
7.0 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { format, addMonths, subMonths, startOfMonth, endOfMonth, startOfWeek, endOfWeek, addDays, isSameMonth, isSameDay } from 'date-fns';
|
|
import { enUS, de } from 'date-fns/locale';
|
|
|
|
interface SimpleDatePickerProps {
|
|
selected: Date;
|
|
onSelect: (date: Date) => void;
|
|
onClose: () => void;
|
|
language?: string;
|
|
}
|
|
|
|
export default function SimpleDatePicker({ selected, onSelect, onClose, language = 'en' }: SimpleDatePickerProps) {
|
|
const [currentMonth, setCurrentMonth] = useState(new Date(selected));
|
|
const modalRef = useRef<HTMLDivElement>(null);
|
|
const locale = language === 'de' ? de : enUS;
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
|
|
onClose();
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, [onClose]);
|
|
|
|
const renderHeader = () => (
|
|
<div className="datepicker-header" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1rem', padding: '0 0.5rem' }}>
|
|
<button onClick={() => setCurrentMonth(subMonths(currentMonth, 1))} style={{ padding: '0.25rem', background: 'none', border: 'none', cursor: 'pointer', color: '#9ca3af', transition: 'color 0.2s' }}>
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
|
|
</button>
|
|
<div style={{ fontWeight: 'bold', fontSize: '0.875rem', letterSpacing: '0.05em', textTransform: 'uppercase', color: '#374151' }}>
|
|
{format(currentMonth, 'MMMM yyyy', { locale })}
|
|
</div>
|
|
<button onClick={() => setCurrentMonth(addMonths(currentMonth, 1))} style={{ padding: '0.25rem', background: 'none', border: 'none', cursor: 'pointer', color: '#9ca3af', transition: 'color 0.2s' }}>
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
|
|
</button>
|
|
</div>
|
|
);
|
|
|
|
const renderDays = () => {
|
|
const days = [];
|
|
const startDate = startOfWeek(currentMonth, { weekStartsOn: 1 });
|
|
for (let i = 0; i < 7; i++) {
|
|
days.push(
|
|
<div key={i} style={{ textAlign: 'center', fontSize: '0.75rem', fontWeight: 'bold', color: '#9ca3af', padding: '0.5rem 0' }}>
|
|
{format(addDays(startDate, i), 'EEEEEE', { locale })}
|
|
</div>
|
|
);
|
|
}
|
|
return <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', marginBottom: '0.5rem' }}>{days}</div>;
|
|
};
|
|
|
|
const renderCells = () => {
|
|
const monthStart = startOfMonth(currentMonth);
|
|
const monthEnd = endOfMonth(monthStart);
|
|
const startDate = startOfWeek(monthStart, { weekStartsOn: 1 });
|
|
const endDate = endOfWeek(monthEnd, { weekStartsOn: 1 });
|
|
|
|
const cells = [];
|
|
let day = startDate;
|
|
|
|
while (day <= endDate) {
|
|
const row = [];
|
|
for (let i = 0; i < 7; i++) {
|
|
const cloneDay = day;
|
|
const isSelected = isSameDay(day, selected);
|
|
const isToday = isSameDay(day, new Date());
|
|
const isCurrentMonth = isSameMonth(day, monthStart);
|
|
const isDaySelected = isSelected;
|
|
|
|
row.push(
|
|
<div
|
|
key={day.toString()}
|
|
onClick={() => {
|
|
onSelect(cloneDay);
|
|
onClose();
|
|
}}
|
|
style={{
|
|
height: '2rem',
|
|
width: '2rem',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
fontSize: '0.875rem',
|
|
borderRadius: '50%',
|
|
cursor: 'pointer',
|
|
transition: 'all 0.2s',
|
|
margin: '0 auto',
|
|
color: !isCurrentMonth ? '#d1d5db' : isDaySelected ? 'white' : '#374151',
|
|
background: isDaySelected ? 'black' : 'transparent',
|
|
fontWeight: isDaySelected ? 'bold' : 'normal',
|
|
boxShadow: isDaySelected ? '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)' : 'none',
|
|
transform: isDaySelected ? 'scale(1.1)' : 'none',
|
|
...(isToday && !isDaySelected ? { color: '#ef4444', fontWeight: 'bold' } : {})
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (!isDaySelected) e.currentTarget.style.backgroundColor = '#f3f4f6';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (!isDaySelected) e.currentTarget.style.backgroundColor = 'transparent';
|
|
}}
|
|
>
|
|
{format(day, 'd')}
|
|
</div>
|
|
);
|
|
day = addDays(day, 1);
|
|
}
|
|
cells.push(row);
|
|
}
|
|
|
|
return (
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '0.5rem 0' }}>
|
|
{cells.flat()}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div ref={modalRef} style={{
|
|
position: 'absolute',
|
|
top: '100%',
|
|
right: 0,
|
|
marginTop: '0.5rem',
|
|
backgroundColor: 'white',
|
|
borderRadius: '0.5rem',
|
|
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
|
padding: '1rem',
|
|
zIndex: 50,
|
|
width: '18rem',
|
|
border: '1px solid #f3f4f6',
|
|
animation: 'fadeIn 0.15s ease-out'
|
|
}}>
|
|
{/* Decorative triangle */}
|
|
<div style={{
|
|
position: 'absolute',
|
|
top: '-0.3rem',
|
|
right: '1rem',
|
|
width: '0.75rem',
|
|
height: '0.75rem',
|
|
backgroundColor: 'white',
|
|
transform: 'rotate(45deg)',
|
|
borderTop: '1px solid #f3f4f6',
|
|
borderLeft: '1px solid #f3f4f6'
|
|
}}></div>
|
|
{renderHeader()}
|
|
{renderDays()}
|
|
{renderCells()}
|
|
<style jsx>{`
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(-5px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|