Enhance calendar functionality with start date management and excluded month visibility. Added start date state and date picker for user-defined start dates. Updated holiday and PTO calculations to respect the start date. Improved UI to toggle excluded months and display active months based on the start date. Refactored related utility functions for better date handling.

This commit is contained in:
zachd
2025-11-10 00:28:42 +01:00
parent 1c4aa803bd
commit ef6a167431
3 changed files with 503 additions and 46 deletions

View File

@@ -8,6 +8,8 @@
export let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }>; export let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }>;
export let selectedCountryCode: string; export let selectedCountryCode: string;
export let weekendDays: number[] = [6, 0]; export let weekendDays: number[] = [6, 0];
export let startDate: Date = new Date(year, 0, 1);
export let isActive: boolean = true;
// Function to determine the first day of the week based on locale // Function to determine the first day of the week based on locale
function getFirstDayOfWeek(locale: string): number { function getFirstDayOfWeek(locale: string): number {
@@ -84,12 +86,19 @@
return weekendDays.includes(date.getDay()); return weekendDays.includes(date.getDay());
} }
function isPastDate(day: number): boolean {
const date = new Date(year, month, day);
// Normalize startDate to current year for comparison
const startDateInYear = new Date(year, startDate.getMonth(), startDate.getDate());
return date < startDateInYear;
}
const dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; const dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
$: orderedDayInitials = dayInitials.slice(firstDayOfWeek).concat(dayInitials.slice(0, firstDayOfWeek)); $: orderedDayInitials = dayInitials.slice(firstDayOfWeek).concat(dayInitials.slice(0, firstDayOfWeek));
</script> </script>
<div class="calendar"> <div class="calendar {isActive ? '' : 'excluded-month'}">
<div class="month-name">{new Date(year, month).toLocaleString('default', { month: 'long' })}</div> <div class="month-name">{new Date(year, month).toLocaleString('default', { month: 'long' })}</div>
{#each orderedDayInitials as dayInitial} {#each orderedDayInitials as dayInitial}
@@ -101,7 +110,8 @@
{/each} {/each}
{#each Array.from({ length: daysInMonth }, (_, i) => i + 1) as day} {#each Array.from({ length: daysInMonth }, (_, i) => i + 1) as day}
{@const holiday = getHoliday(day)} {@const holiday = getHoliday(day)}
<div class="day {isWeekend(new Date(year, month, day)) ? 'weekend' : ''} {holiday ? 'holiday' : ''} {isOptimizedDayOff(day) ? 'optimized' : ''} {isConsecutiveDayOff(day) ? 'consecutive-day' : ''}"> {@const pastDate = isPastDate(day)}
<div class="day {isWeekend(new Date(year, month, day)) ? 'weekend' : ''} {holiday ? 'holiday' : ''} {isOptimizedDayOff(day) ? 'optimized' : ''} {isConsecutiveDayOff(day) ? 'consecutive-day' : ''} {pastDate ? 'past-date' : ''}">
<span class={holiday?.hidden ? 'strikethrough' : ''}>{day}</span> <span class={holiday?.hidden ? 'strikethrough' : ''}>{day}</span>
{#if holiday} {#if holiday}
<Tooltip text={holiday.name} /> <Tooltip text={holiday.name} />
@@ -137,6 +147,14 @@
color: #c5c6c7; color: #c5c6c7;
font-size: 0.6em; font-size: 0.6em;
} }
.excluded-month .month-name {
color: #666;
}
.excluded-month .day-initial {
color: #666;
}
.day { .day {
aspect-ratio: 1; aspect-ratio: 1;
text-align: center; text-align: center;
@@ -202,4 +220,12 @@
text-decoration: line-through; text-decoration: line-through;
opacity: 0.5; opacity: 0.5;
} }
.past-date {
opacity: 0.4;
}
.past-date span {
text-decoration: line-through;
}
</style> </style>

View File

@@ -31,49 +31,56 @@ export function getHolidaysForYear(countryCode: string, year: number, stateCode?
} }
// Find optimal placement of PTO days to maximize consecutive time off // Find optimal placement of PTO days to maximize consecutive time off
export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number, weekendDays: number[] = [0, 6]): Date[] { export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number, weekendDays: number[] = [0, 6], startDate?: Date): Date[] {
const effectiveStartDate = startDate || new Date(year, 0, 1);
const filteredHolidays = holidays.filter(h => h.date.getFullYear() === year && h.date >= effectiveStartDate);
const allDaysOff = new Set([ const allDaysOff = new Set([
...holidays.filter(h => h.date.getFullYear() === year).map(h => dateKey(h.date)), ...filteredHolidays.map(h => dateKey(h.date)),
...getWeekends(year, weekendDays).map(d => dateKey(d)) ...getWeekends(year, weekendDays, effectiveStartDate).map(d => dateKey(d))
]); ]);
const gaps = findGaps(allDaysOff, year, weekendDays); const gaps = findGaps(allDaysOff, year, weekendDays, effectiveStartDate);
return selectDaysOff(rankGapsByEfficiency(gaps, allDaysOff, weekendDays), daysOff, allDaysOff, weekendDays); return selectDaysOff(rankGapsByEfficiency(gaps, allDaysOff, weekendDays), daysOff, allDaysOff, weekendDays);
} }
// Calculate periods of consecutive days off (weekends + holidays + PTO) // Calculate periods of consecutive days off (weekends + holidays + PTO)
export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number, weekendDays: number[] = [0, 6]) { export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number, weekendDays: number[] = [0, 6], startDate?: Date) {
const effectiveStartDate = startDate || new Date(year, 0, 1);
const filteredHolidays = holidays.filter(h => h.date >= effectiveStartDate);
const filteredOptimizedDaysOff = optimizedDaysOff.filter(d => d >= effectiveStartDate);
const allDaysOff = new Set([ const allDaysOff = new Set([
...holidays.map(h => dateKey(h.date)), ...filteredHolidays.map(h => dateKey(h.date)),
...optimizedDaysOff.map(d => dateKey(d)), ...filteredOptimizedDaysOff.map(d => dateKey(d)),
...getWeekends(year, weekendDays).map(d => dateKey(d)) ...getWeekends(year, weekendDays, effectiveStartDate).map(d => dateKey(d))
]); ]);
const consecutiveDaysOff = []; const consecutiveDaysOff = [];
let currentGroup = []; let currentGroup = [];
for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
if (isWeekend(d, weekendDays) || isHoliday(d, holidays) || allDaysOff.has(dateKey(d))) { if (isWeekend(d, weekendDays) || isHoliday(d, filteredHolidays) || allDaysOff.has(dateKey(d))) {
currentGroup.push(new Date(d)); currentGroup.push(new Date(d));
} else if (currentGroup.length > 0) { } else if (currentGroup.length > 0) {
if (isValidConsecutiveGroup(currentGroup, weekendDays)) { if (isValidConsecutiveGroup(currentGroup, weekendDays)) {
consecutiveDaysOff.push(createPeriod(currentGroup, optimizedDaysOff)); consecutiveDaysOff.push(createPeriod(currentGroup, filteredOptimizedDaysOff));
} }
currentGroup = []; currentGroup = [];
} }
} }
if (currentGroup.length > 0 && isValidConsecutiveGroup(currentGroup, weekendDays)) { if (currentGroup.length > 0 && isValidConsecutiveGroup(currentGroup, weekendDays)) {
consecutiveDaysOff.push(createPeriod(currentGroup, optimizedDaysOff)); consecutiveDaysOff.push(createPeriod(currentGroup, filteredOptimizedDaysOff));
} }
return consecutiveDaysOff; return consecutiveDaysOff;
} }
// Get all weekend days for a year // Get all weekend days for a year
function getWeekends(year: number, weekendDays: number[]): Date[] { function getWeekends(year: number, weekendDays: number[], startDate?: Date): Date[] {
const effectiveStartDate = startDate || new Date(year, 0, 1);
const weekends = []; const weekends = [];
for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
if (d.getMonth() === d.getMonth() && isWeekend(d, weekendDays)) { if (d.getMonth() === d.getMonth() && isWeekend(d, weekendDays)) {
weekends.push(new Date(d)); weekends.push(new Date(d));
} }
@@ -82,11 +89,12 @@ function getWeekends(year: number, weekendDays: number[]): Date[] {
} }
// Find gaps between days off that could be filled with PTO // Find gaps between days off that could be filled with PTO
function findGaps(allDaysOff: Set<string>, year: number, weekendDays: number[]) { function findGaps(allDaysOff: Set<string>, year: number, weekendDays: number[], startDate?: Date) {
const effectiveStartDate = startDate || new Date(year, 0, 1);
const gaps = []; const gaps = [];
let gapStart = null; let gapStart = null;
for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) { for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
if (!allDaysOff.has(dateKey(d)) && !isWeekend(d, weekendDays)) { if (!allDaysOff.has(dateKey(d)) && !isWeekend(d, weekendDays)) {
if (!gapStart) gapStart = new Date(d); if (!gapStart) gapStart = new Date(d);
} else if (gapStart) { } else if (gapStart) {

View File

@@ -17,6 +17,8 @@
let daysOff: number = 0; let daysOff: number = 0;
let optimizedDaysOff: Date[] = []; let optimizedDaysOff: Date[] = [];
let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }> = []; let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }> = [];
let showExcludedMonths: boolean = true;
let visibleMonths: number[] = [];
let countriesInput: HTMLInputElement | null = null; let countriesInput: HTMLInputElement | null = null;
let statesInput: HTMLInputElement | null = null; let statesInput: HTMLInputElement | null = null;
let showHowItWorks: boolean = false; let showHowItWorks: boolean = false;
@@ -35,12 +37,27 @@
let showWeekendSettings: boolean = false; let showWeekendSettings: boolean = false;
let weekendDays: number[] = []; let weekendDays: number[] = [];
// Start date state
let startDate: Date = new Date(new Date().getFullYear(), 0, 1);
let showDatePicker: boolean = false;
let datePickerValue: string = '';
$: selectedCountryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || ''; $: selectedCountryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || '';
$: if (selectedCountryCode || selectedStateCode || daysOff || year) { $: if (selectedCountryCode || selectedStateCode || daysOff || year || startDate) {
updateHolidays(); updateHolidays();
} }
// Reactive: when year changes, load start date for that year
$: if (year !== undefined && year) {
startDate = getStartDate(year);
}
// Reactive: when startDate or year changes, update excluded months visibility
$: if (year !== undefined && year && startDate) {
showExcludedMonths = !hasExcludedMonths();
}
$: if (daysOff) { $: if (daysOff) {
localStorage.setItem('daysOff', daysOff.toString()); localStorage.setItem('daysOff', daysOff.toString());
} }
@@ -59,8 +76,11 @@
const stateName = target.value; const stateName = target.value;
selectedStateCode = Object.keys(statesList).find(code => statesList[code] === stateName) || ''; selectedStateCode = Object.keys(statesList).find(code => statesList[code] === stateName) || '';
selectedState = stateName; selectedState = stateName;
localStorage.setItem('selectedState', selectedState); // Save state per country
localStorage.setItem('selectedStateCode', selectedStateCode); if (selectedCountryCode) {
localStorage.setItem(`selectedState_${selectedCountryCode}`, selectedState);
localStorage.setItem(`selectedStateCode_${selectedCountryCode}`, selectedStateCode);
}
} }
onMount(() => { onMount(() => {
@@ -73,14 +93,22 @@
const storedYear = localStorage.getItem('year'); const storedYear = localStorage.getItem('year');
const storedCountry = localStorage.getItem('selectedCountry'); const storedCountry = localStorage.getItem('selectedCountry');
const storedDaysOff = localStorage.getItem('daysOff'); const storedDaysOff = localStorage.getItem('daysOff');
const storedState = localStorage.getItem('selectedState');
const storedStateCode = localStorage.getItem('selectedStateCode');
year = storedYear ? parseInt(storedYear, 10) : defaultYear; year = storedYear ? parseInt(storedYear, 10) : defaultYear;
selectedCountry = storedCountry || defaultCountry; selectedCountry = storedCountry || defaultCountry;
daysOff = storedDaysOff ? parseInt(storedDaysOff, 10) : defaultDaysOff; daysOff = storedDaysOff ? parseInt(storedDaysOff, 10) : defaultDaysOff;
selectedState = storedState || '';
selectedStateCode = storedStateCode || ''; // Load state per country
const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || '';
if (countryCode) {
selectedState = localStorage.getItem(`selectedState_${countryCode}`) || '';
selectedStateCode = localStorage.getItem(`selectedStateCode_${countryCode}`) || '';
} else {
selectedState = '';
selectedStateCode = '';
}
startDate = getStartDate(year);
// showExcludedMonths will be set by reactive statement
updateHolidays(); updateHolidays();
}); });
@@ -113,14 +141,17 @@
function handleCountryChange(event: Event) { function handleCountryChange(event: Event) {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const fullValue = target.value; const fullValue = target.value;
if (selectedCountryCode) { selectedCountry = fullValue;
daysOff = ptoData[selectedCountryCode] || 0; // Get the country code for the new country (selectedCountryCode will update reactively)
selectedState = ''; // Reset state const newCountryCode = Object.keys(countriesList).find(code => countriesList[code] === fullValue) || '';
selectedStateCode = ''; // Reset state code if (newCountryCode) {
updateStatesList(selectedCountryCode); // Update states list for the new country // Update days off to the new country's default
daysOff = ptoData[newCountryCode] || 0;
// Load state for the new country
selectedState = localStorage.getItem(`selectedState_${newCountryCode}`) || '';
selectedStateCode = localStorage.getItem(`selectedStateCode_${newCountryCode}`) || '';
// updateStatesList and updateHolidays will be called by reactive statements
localStorage.setItem('selectedCountry', selectedCountry); localStorage.setItem('selectedCountry', selectedCountry);
localStorage.setItem('selectedState', selectedState);
localStorage.setItem('selectedStateCode', selectedStateCode);
localStorage.setItem('daysOff', daysOff.toString()); localStorage.setItem('daysOff', daysOff.toString());
} }
} }
@@ -135,8 +166,8 @@
hidden: isHolidayHidden(holiday) hidden: isHolidayHidden(holiday)
})); }));
const visibleHolidays = holidays.filter(h => !h.hidden); const visibleHolidays = holidays.filter(h => !h.hidden);
optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff, weekendDays); optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff, weekendDays, startDate);
consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays); consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays, startDate);
} else { } else {
holidays = []; holidays = [];
optimizedDaysOff = []; optimizedDaysOff = [];
@@ -147,14 +178,18 @@
function resetToDefault() { function resetToDefault() {
year = defaultYear; year = defaultYear;
selectedCountry = defaultCountry; selectedCountry = defaultCountry;
selectedState = ''; const defaultCountryCode = Object.keys(countriesList).find(code => countriesList[code] === defaultCountry) || '';
selectedStateCode = ''; // Load state for default country
daysOff = defaultDaysOff; if (defaultCountryCode) {
selectedState = localStorage.getItem(`selectedState_${defaultCountryCode}`) || '';
selectedStateCode = localStorage.getItem(`selectedStateCode_${defaultCountryCode}`) || '';
} else {
selectedState = '';
selectedStateCode = '';
}
// Keep current daysOff value, don't reset it
localStorage.setItem('year', year.toString()); localStorage.setItem('year', year.toString());
localStorage.setItem('selectedCountry', selectedCountry); localStorage.setItem('selectedCountry', selectedCountry);
localStorage.setItem('selectedState', selectedState);
localStorage.setItem('selectedStateCode', selectedStateCode);
localStorage.setItem('daysOff', daysOff.toString());
} }
function handleKeyDown(event: KeyboardEvent) { function handleKeyDown(event: KeyboardEvent) {
@@ -252,6 +287,156 @@
$: visibleHolidaysCount = holidays.filter(h => !h.hidden).length; $: visibleHolidaysCount = holidays.filter(h => !h.hidden).length;
// Get start date for a given year from localStorage
function getStartDate(year: number): Date {
try {
const stored = localStorage.getItem('startDates');
if (stored) {
const startDates: string[] = JSON.parse(stored);
// Find date that matches the year (extract year from date string)
const dateStr = startDates.find(date => {
const dateYear = parseInt(date.split('-')[0] || date.split('T')[0].split('-')[0]);
return dateYear === year;
});
if (dateStr) {
// Parse date string - handle both YYYY-MM-DD and ISO format
const parsed = dateStr.includes('T') ? dateStr.split('T')[0] : dateStr;
const [y, m, d] = parsed.split('-').map(Number);
return new Date(y, m - 1, d);
}
}
} catch (e) {
console.error('Error loading start date:', e);
}
return new Date(year, 0, 1); // Default to Jan 1st
}
// Save start date for a given year to localStorage
function saveStartDate(year: number, date: Date) {
try {
const stored = localStorage.getItem('startDates');
let startDates: string[] = stored ? JSON.parse(stored) : [];
// Remove existing date for this year (extract year from date string)
startDates = startDates.filter(dateStr => {
const dateYear = parseInt(dateStr.split('-')[0] || dateStr.split('T')[0].split('-')[0]);
return dateYear !== year;
});
// Add new date
startDates.push(formatDateForInput(date));
localStorage.setItem('startDates', JSON.stringify(startDates));
} catch (e) {
console.error('Error saving start date:', e);
}
}
// Format date as YYYY-MM-DD for date input (no timezone conversion)
function formatDateForInput(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// Format start date for display
function formatStartDate(date: Date): string {
const today = new Date();
if (date.getTime() === new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime() && date.getFullYear() === year) {
return 'Today';
}
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const day = date.getDate();
const suffix = getDaySuffix(day);
return `${monthNames[date.getMonth()]}&nbsp;${day}${suffix}`;
}
// Get day suffix (st, nd, rd, th)
function getDaySuffix(day: number): string {
if (day > 3 && day < 21) return 'th';
switch (day % 10) {
case 1: return 'st';
case 2: return 'nd';
case 3: return 'rd';
default: return 'th';
}
}
// Handle start date change
function handleStartDateChange(newDate: Date) {
startDate = newDate;
saveStartDate(year, newDate);
updateHolidays();
// showExcludedMonths will be updated by reactive statement
}
// Handle date picker input change (auto-save)
function handleDatePickerChange() {
if (datePickerValue) {
// Parse YYYY-MM-DD format in local time (consistent with getStartDate)
const [y, m, d] = datePickerValue.split('-').map(Number);
const newDate = new Date(y, m - 1, d);
handleStartDateChange(newDate);
}
}
// Set start date to today
function setStartDateToToday() {
const today = new Date();
if (today.getFullYear() === year) {
handleStartDateChange(new Date(today.getFullYear(), today.getMonth(), today.getDate()));
showDatePicker = false;
}
}
// Reset start date to Jan 1st
function resetStartDateToJan1() {
const jan1st = new Date(year, 0, 1);
handleStartDateChange(jan1st);
showDatePicker = false;
}
// Check if today is in the current year
function isTodayInYear(): boolean {
const today = new Date();
return today.getFullYear() === year;
}
// Helper: Get start date normalized to the current year
function getStartDateInYear(): Date {
return new Date(year, startDate.getMonth(), startDate.getDate());
}
// Check if a month is active (not entirely before the start date)
function isMonthActive(monthIndex: number): boolean {
const startDateInYear = getStartDateInYear();
const startMonth = startDateInYear.getMonth();
// Month is active if:
// 1. The start date falls within this month (same month), OR
// 2. The month starts on or after the start date (later month)
// This means only months entirely before the start date's month are excluded
return monthIndex >= startMonth;
}
// Check if start date is Jan 1st
function isStartDateJan1st(): boolean {
const startDateInYear = getStartDateInYear();
return startDateInYear.getTime() === new Date(year, 0, 1).getTime();
}
// Check if there are any excluded months (months entirely before the start date)
function hasExcludedMonths(): boolean {
return months.some(month => !isMonthActive(month));
}
// Filter months based on showExcludedMonths setting
// Explicitly depend on startDate, year, and showExcludedMonths to ensure proper reactivity
// Use a computed value that depends on all relevant variables
$: visibleMonths = (startDate && year !== undefined && year)
? (showExcludedMonths
? months
: months.filter(month => isMonthActive(month)))
: [];
function toggleWeekendDay(dayNumber: number) { function toggleWeekendDay(dayNumber: number) {
if (weekendDays.includes(dayNumber)) { if (weekendDays.includes(dayNumber)) {
weekendDays = weekendDays.filter(d => d !== dayNumber); weekendDays = weekendDays.filter(d => d !== dayNumber);
@@ -318,7 +503,7 @@
.content-box p { .content-box p {
text-align: center; text-align: center;
line-height: 2; line-height: 3;
} }
input { input {
@@ -365,6 +550,29 @@
} }
} }
.toggle-excluded-months-container {
text-align: center;
margin: 20px 0;
}
.toggle-excluded-months {
padding: 8px 16px;
background-color: #333;
border: 1px solid #555;
border-radius: 5px;
color: #fff;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.3s;
width: auto;
display: inline-block;
white-space: nowrap;
}
.toggle-excluded-months:hover {
background-color: #444;
}
.calendar-container { .calendar-container {
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
@@ -557,7 +765,6 @@
} }
.content-box button { .content-box button {
margin-left: 10px;
background-color: #444; background-color: #444;
border: none; border: none;
color: #fff; color: #fff;
@@ -641,6 +848,161 @@
margin-bottom: 15px; margin-bottom: 15px;
color: #fff; color: #fff;
} }
.start-date-link {
text-decoration: underline;
text-decoration-style: dotted;
cursor: pointer;
color: inherit;
}
.start-date-link:hover {
text-decoration-style: solid;
}
.date-picker-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.date-picker-modal {
background-color: #222;
border-radius: 10px;
padding: 25px;
max-width: min(400px, calc(100vw - 40px));
width: 100%;
box-sizing: border-box;
position: relative;
color: #fff;
}
.date-picker-close {
position: absolute;
top: 10px;
right: 10px;
background: transparent;
border: none;
color: #fff;
font-size: 24px;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 1;
}
.date-picker-close:hover,
.date-picker-close:active,
.date-picker-close:focus {
background: transparent;
color: #ccc;
}
.date-picker-modal h3 {
margin: 0 0 10px 0;
padding: 0;
text-align: center;
font-size: 1.5em;
}
.date-picker-modal p {
margin: 0 0 20px 0;
padding: 0;
text-align: center;
color: #ccc;
}
.date-picker-controls {
margin: 0 0 20px 0;
padding: 0;
width: 100%;
}
.date-input {
width: 100%;
padding: 10px;
font-size: 1em;
background-color: #222;
border: 1px solid #555;
border-radius: 5px;
color: #fff;
box-sizing: border-box;
text-align: center;
margin: 0;
display: block;
}
.date-input:focus {
outline: 2px solid #61dafb;
border-color: #61dafb;
}
/* Make calendar icon white */
.date-input::-webkit-calendar-picker-indicator {
filter: invert(1);
cursor: pointer;
}
.date-input::-webkit-inner-spin-button,
.date-input::-webkit-clear-button {
filter: invert(1);
}
.date-picker-buttons {
display: flex;
gap: 10px;
justify-content: center;
margin: 0 0 15px 0;
width: 100%;
box-sizing: border-box;
}
.date-picker-button,
.date-picker-save {
margin: 0;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
box-sizing: border-box;
}
.date-picker-button {
flex: 1;
min-width: 0;
padding: 20px;
background-color: #444;
color: #fff;
font-size: 0.9em;
}
.date-picker-button:hover {
background-color: #555;
}
.date-picker-save {
width: 100%;
margin: 0;
font-size: 1.1em;
font-weight: bold;
background-color: #61dafb;
color: #000;
}
.date-picker-save:hover {
background-color: #4fa8c5;
}
</style> </style>
<main> <main>
@@ -689,15 +1051,27 @@
<span class="bold">{daysOff}</span> <span class="bold">{daysOff}</span>
<button on:click={() => { daysOff++; updateHolidays(); }} aria-label="Increase days off"></button> <button on:click={() => { daysOff++; updateHolidays(); }} aria-label="Increase days off"></button>
</span> </span>
days&nbsp;off in days&nbsp;off from
<a href="#" on:click|preventDefault={() => { showDatePicker = true; datePickerValue = formatDateForInput(startDate); }} class="bold start-date-link">
{@html formatStartDate(startDate)}
</a>
until the&nbsp;end&nbsp;of
<span class="arrow-controls"> <span class="arrow-controls">
<button on:click={() => { year--; updateHolidays(); }} aria-label="Previous year"></button> <button on:click={() => { year--; updateHolidays(); }} aria-label="Previous year"></button>
<span class="bold">{year}</span> <span class="bold">{year}</span>
<button on:click={() => { year++; updateHolidays(); }} aria-label="Next year"></button> <button on:click={() => { year++; updateHolidays(); }} aria-label="Next year"></button>
</span> </span>
</p> </p>
{#if year !== defaultYear || selectedCountry !== defaultCountry || daysOff !== defaultDaysOff} {#if year !== defaultYear || selectedCountry !== defaultCountry}
<a href="#" on:click|preventDefault={resetToDefault} class="reset-link">Reset to my country</a> {@const yearDifferent = year !== defaultYear}
{@const countryDifferent = selectedCountry !== defaultCountry}
<a href="#" on:click|preventDefault={resetToDefault} class="reset-link">
{yearDifferent && countryDifferent
? 'Reset to current country and year'
: yearDifferent
? 'Reset to current year'
: 'Reset to current country'}
</a>
{/if} {/if}
<datalist id="countries"> <datalist id="countries">
@@ -781,13 +1155,27 @@
</div> </div>
{/if} {/if}
{#if hasExcludedMonths()}
<div class="toggle-excluded-months-container">
<button
class="toggle-excluded-months"
on:click={() => showExcludedMonths = !showExcludedMonths}
aria-label={showExcludedMonths ? 'Hide excluded months' : 'Show excluded months'}
>
{showExcludedMonths ? 'Hide' : 'Show'} excluded months
</button>
</div>
{/if}
<div class="calendar-grid"> <div class="calendar-grid">
{#each months as month} {#each visibleMonths as month}
<div class="calendar-container"> <div class="calendar-container">
<CalendarMonth <CalendarMonth
year={year} year={year}
month={month} month={month}
holidays={holidays} holidays={holidays}
startDate={startDate}
isActive={isMonthActive(month)}
optimizedDaysOff={optimizedDaysOff} optimizedDaysOff={optimizedDaysOff}
consecutiveDaysOff={consecutiveDaysOff} consecutiveDaysOff={consecutiveDaysOff}
selectedCountryCode={selectedCountryCode} selectedCountryCode={selectedCountryCode}
@@ -798,6 +1186,41 @@
</div> </div>
</div> </div>
{#if showDatePicker}
<div class="date-picker-overlay" on:click|self={() => showDatePicker = false}>
<div class="date-picker-modal" on:click|stopPropagation>
<button class="date-picker-close" on:click={() => showDatePicker = false} aria-label="Close">×</button>
<h3>Set Start Date</h3>
<p>Choose when your time off period begins for {year}</p>
<div class="date-picker-controls">
<input
type="date"
bind:value={datePickerValue}
on:change={handleDatePickerChange}
class="date-input"
min={formatDateForInput(new Date(year, 0, 1))}
max={formatDateForInput(new Date(year, 11, 31))}
/>
</div>
<div class="date-picker-buttons">
{#if isTodayInYear()}
<button on:click={setStartDateToToday} class="date-picker-button">
Set to today
</button>
{/if}
{#if !isStartDateJan1st()}
<button on:click={resetStartDateToJan1} class="date-picker-button">
Reset to Jan 1st
</button>
{/if}
</div>
<button class="date-picker-button date-picker-save" on:click={() => showDatePicker = false}>
Done
</button>
</div>
</div>
{/if}
<button type="button" class="toggle-text" on:click={toggleHowItWorks}> <button type="button" class="toggle-text" on:click={toggleHowItWorks}>
{showHowItWorks ? 'Hide Details' : 'How does this work?'} {showHowItWorks ? 'Hide Details' : 'How does this work?'}
</button> </button>