My-Weekly-ToDo-List/src/components/DatePicker.tsx
mARTin 798bcfe44a feat: Add Google Tasks, Apple Reminders, task import/sync, holidays, and UI enhancements
- Add Google Tasks integration and Apple Reminders support
- Add task import/export with list management APIs
- Add goal API for weekly goals
- Add German holidays library
- Add ImportListModal component
- Enhance WeeklyView with major UI improvements
- Enhance CalendarSettings with new connection options
- Add external task fields to database schema
- Add Playwright test suites for auth and tasks
- Add iCloud reminders Python scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 16:56:14 +01:00

339 lines
12 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import {
format,
addMonths,
subMonths,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
addDays,
isSameMonth,
isSameDay,
setMonth,
setYear,
getYear,
getMonth
} from 'date-fns';
import { enUS, de } from 'date-fns/locale';
interface DatePickerProps {
selected: Date;
onSelect: (date: Date) => void;
onClose: () => void;
language?: string;
}
export default function DatePicker({ selected, onSelect, onClose, language = 'en' }: DatePickerProps) {
const [currentMonth, setCurrentMonth] = useState(new Date(selected));
const [view, setView] = useState<'calendar' | 'month-year'>('calendar');
const modalRef = useRef<HTMLDivElement>(null);
const locale = language === 'de' ? de : enUS;
// Close on click outside
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]);
// Calendar Header
const renderHeader = () => {
return (
<div className="datepicker-header">
<button onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}>
<svg width="24" height="24" 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
className="datepicker-current-month"
onClick={() => setView(view === 'calendar' ? 'month-year' : 'calendar')}
>
{format(currentMonth, 'MMMM yyyy', { locale })}
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{ marginLeft: '6px', opacity: 0.5 }}>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
<button onClick={() => setCurrentMonth(addMonths(currentMonth, 1))}>
<svg width="24" height="24" 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>
);
};
// Days of Week Header
const renderDays = () => {
const days = [];
const startDate = startOfWeek(currentMonth, { weekStartsOn: 1 }); // Monday start
for (let i = 0; i < 7; i++) {
days.push(
<div className="datepicker-day-name" key={i}>
{format(addDays(startDate, i), 'EEEEEE', { locale })}
</div>
);
}
return <div className="datepicker-days-row">{days}</div>;
};
// Calendar Cells
const renderCells = () => {
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(monthStart);
const startDate = startOfWeek(monthStart, { weekStartsOn: 1 });
const endDate = endOfWeek(monthEnd, { weekStartsOn: 1 });
const dateFormat = 'd';
const rows = [];
let days = [];
let day = startDate;
let formattedDate = '';
while (day <= endDate) {
for (let i = 0; i < 7; i++) {
formattedDate = format(day, dateFormat);
const cloneDay = day;
days.push(
<div
className={`datepicker-cell ${!isSameMonth(day, monthStart)
? 'disabled'
: isSameDay(day, selected)
? 'selected'
: ''
} ${isSameDay(day, new Date()) ? 'today' : ''}`}
key={day.toString()}
onClick={() => {
onSelect(cloneDay);
onClose();
}}
>
<span className="number">{formattedDate}</span>
</div>
);
day = addDays(day, 1);
}
rows.push(
<div className="datepicker-row" key={day.toString()}>
{days}
</div>
);
days = [];
}
return <div className="datepicker-body">{rows}</div>;
};
// Month/Year Selection View
const renderMonthYearSelector = () => {
const years = [];
const currentYear = getYear(new Date());
for (let y = currentYear - 5; y <= currentYear + 5; y++) {
years.push(y);
}
const months = Array.from({ length: 12 }, (_, i) => {
return format(setMonth(new Date(), i), 'MMM', { locale });
});
return (
<div className="datepicker-month-year-view">
<div className="datepicker-years">
{years.map(year => (
<div
key={year}
className={`datepicker-year-option ${getYear(currentMonth) === year ? 'selected' : ''}`}
onClick={() => setCurrentMonth(setYear(currentMonth, year))}
>
{year}
</div>
))}
</div>
<div className="datepicker-months">
{months.map((month, index) => (
<div
key={month}
className={`datepicker-month-option ${getMonth(currentMonth) === index ? 'selected' : ''}`}
onClick={() => {
setCurrentMonth(setMonth(currentMonth, index));
setView('calendar');
}}
>
{month}
</div>
))}
</div>
</div>
);
};
return (
<div className="datepicker-modal" ref={modalRef}>
{renderHeader()}
{view === 'calendar' ? (
<>
{renderDays()}
{renderCells()}
</>
) : (
renderMonthYearSelector()
)}
<style jsx>{`
.datepicker-modal {
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
background: white;
border-radius: 8px;
border-bottom: 5px solid #00ceb0; /* Weekly Teal/Green */
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
padding: 20px;
z-index: 1000;
width: 320px;
font-family: var(--font-inter, sans-serif);
user-select: none;
animation: fadeIn 0.2s ease-out;
}
.datepicker-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.datepicker-header button {
background: none;
border: none;
cursor: pointer;
font-size: 1.5rem;
color: #999;
padding: 0 8px;
line-height: 1;
font-weight: 300;
}
.datepicker-header button:hover {
color: #333;
}
.datepicker-current-month {
font-weight: 800;
font-size: 1rem;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.datepicker-days-row {
display: grid;
grid-template-columns: repeat(7, 1fr); /* Force 7 columns */
margin-bottom: 12px;
justify-items: center; /* Center content */
}
.datepicker-day-name {
width: 40px;
text-align: center;
font-size: 0.85rem;
color: #000;
font-weight: 700;
}
.datepicker-body {
display: flex; /* Still flex col for rows, OR better yet, just one grid */
flex-direction: column;
gap: 4px;
}
/* Alternative: Use one big grid for body cells? */
/* But current implementation renders rows. Let's keep rows but make them grids. */
.datepicker-row {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0; /* Gap handled by padding or just empty space? Current had space-between */
justify-items: center;
}
/* Actually, space-between in flex meant they spread out. Grid 1fr means equal width. */
/* This is much better for alignment. */
.datepicker-cell {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 4px;
font-size: 1rem;
font-weight: 600;
transition: all 0.1s ease;
}
.datepicker-cell:hover:not(.disabled) {
background-color: #f5f5f5;
}
.datepicker-cell.selected {
background-color: #f0f0f0;
color: #000;
font-weight: 900;
}
.datepicker-cell.today {
color: #00ceb0;
}
.datepicker-cell.disabled {
color: #e0e0e0;
pointer-events: none;
}
.datepicker-month-year-view {
display: flex;
flex-direction: column;
gap: 10px;
height: 280px;
}
.datepicker-years {
display: flex;
overflow-x: auto;
gap: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.datepicker-year-option {
padding: 4px 8px;
cursor: pointer;
border-radius: 4px;
font-weight: 500;
}
.datepicker-year-option.selected {
background: #000;
color: white;
}
.datepicker-months {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.datepicker-month-option {
padding: 12px;
text-align: center;
cursor: pointer;
border-radius: 4px;
font-weight: 600;
}
.datepicker-month-option:hover {
background: #f0f0f0;
}
.datepicker-month-option.selected {
background: #000;
color: white;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}
`}</style>
</div >
);
}