diff --git a/src/lib/CalendarMonth.svelte b/src/lib/CalendarMonth.svelte index d546ad6..bc2ad8e 100644 --- a/src/lib/CalendarMonth.svelte +++ b/src/lib/CalendarMonth.svelte @@ -1,16 +1,15 @@ -
{new Date(year, month).toLocaleString('default', { month: 'long' })}
- {#each orderedDayInitials as dayInitial}
{dayInitial}
{/each} @@ -113,7 +106,7 @@
{day} {#if getHoliday(day)} - + {/if}
{/each} diff --git a/src/lib/holidayUtils.js b/src/lib/holidayUtils.ts similarity index 73% rename from src/lib/holidayUtils.js rename to src/lib/holidayUtils.ts index 614dcf0..098e2e4 100644 --- a/src/lib/holidayUtils.js +++ b/src/lib/holidayUtils.ts @@ -5,32 +5,32 @@ const MS_IN_A_DAY = 86400000; const MAX_GAP_LENGTH = 5; // Helper function to check if a date is a weekend -const isWeekend = date => date.getDay() === 0 || date.getDay() === 6; +const isWeekend = (date: Date): boolean => date.getDay() === 0 || date.getDay() === 6; // Helper function to check if two dates are the same day -const isSameDay = (date1, date2) => +const isSameDay = (date1: Date, date2: Date): boolean => date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate(); // Helper function to generate a unique key for a date -const dateKey = date => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`; +const dateKey = (date: Date): string => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`; // Helper function to check if a date is a holiday -const isHoliday = (date, holidays) => holidays.some(h => isSameDay(h.date, date)); +const isHoliday = (date: Date, holidays: { date: Date }[]): boolean => holidays.some(h => isSameDay(h.date, date)); // Helper function to check if a date is a day off -const isDayOff = (date, allDaysOffSet) => allDaysOffSet.has(dateKey(date)); +const isDayOff = (date: Date, allDaysOffSet: Set): boolean => allDaysOffSet.has(dateKey(date)); // Helper function to calculate the number of days between two dates -const daysBetween = (startDate, endDate) => Math.round((endDate - startDate) / MS_IN_A_DAY); +const daysBetween = (startDate: Date, endDate: Date): number => Math.round((endDate.getTime() - startDate.getTime()) / MS_IN_A_DAY); // Helper function to format a date -const formatDate = date => date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); +const formatDate = (date: Date): string => date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); // Get holidays for a specific year and country -export function getHolidaysForYear(countryCode, year, stateCode = '') { - const hd = new Holidays(countryCode, stateCode); +export function getHolidaysForYear(countryCode: string, year: number, stateCode?: string): { date: Date; name: string }[] { + const hd = stateCode ? new Holidays(countryCode, stateCode) : new Holidays(countryCode); return hd.getHolidays(year) .filter(holiday => holiday.type === 'public') .map(holiday => ({ @@ -40,14 +40,9 @@ export function getHolidaysForYear(countryCode, year, stateCode = '') { } // Optimize days off to create the longest possible chains -export function optimizeDaysOff(holidays, year, daysOff) { - // Filter holidays to include only those in the current year +export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number): Date[] { const currentYearHolidays = holidays.filter(h => h.date.getFullYear() === year); - - // Recalculate weekends for the current year const weekends = getWeekends(year); - - // Initialize a new Set for all days off const allDaysOffSet = new Set([ ...currentYearHolidays.map(h => dateKey(h.date)), ...weekends.map(d => dateKey(d)) @@ -59,15 +54,15 @@ export function optimizeDaysOff(holidays, year, daysOff) { } // Calculate consecutive days off -export function calculateConsecutiveDaysOff(holidays, optimizedDaysOff, year) { +export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number): { startDate: Date; endDate: Date; usedDaysOff: number; totalDays: number }[] { const allDaysOffSet = new Set([...holidays.map(h => dateKey(h.date)), ...optimizedDaysOff.map(d => dateKey(d))]); - const consecutiveDaysOff = []; - let currentGroup = []; + const consecutiveDaysOff: { startDate: Date; endDate: Date; usedDaysOff: number; totalDays: number }[] = []; + let currentGroup: Date[] = []; for (let month = 0; month < 12; month++) { for (let day = 1; day <= 31; day++) { const date = new Date(year, month, day); - if (date.getMonth() !== month) break; // Skip invalid dates + if (date.getMonth() !== month) break; if (isWeekend(date) || isHoliday(date, holidays) || isDayOff(date, allDaysOffSet)) { currentGroup.push(date); @@ -80,7 +75,6 @@ export function calculateConsecutiveDaysOff(holidays, optimizedDaysOff, year) { } } - // Check the last group at the end of the year if (currentGroup.length > 2) { addConsecutiveDaysOff(consecutiveDaysOff, currentGroup, optimizedDaysOff); } @@ -89,8 +83,8 @@ export function calculateConsecutiveDaysOff(holidays, optimizedDaysOff, year) { } // Get all weekends for a specific year -function getWeekends(year) { - const weekends = []; +function getWeekends(year: number): Date[] { + const weekends: Date[] = []; for (let month = 0; month < 12; month++) { for (let day = 1; day <= 31; day++) { const date = new Date(year, month, day); @@ -102,9 +96,9 @@ function getWeekends(year) { } // Find gaps between days off -function findGaps(allDaysOffSet, year) { - const gaps = []; - let currentGapStart = null; +function findGaps(allDaysOffSet: Set, year: number): { start: Date; end: Date; gapLength: number }[] { + const gaps: { start: Date; end: Date; gapLength: number }[] = []; + let currentGapStart: Date | null = null; for (let month = 0; month < 12; month++) { for (let day = 1; day <= 31; day++) { @@ -137,7 +131,7 @@ function findGaps(allDaysOffSet, year) { } // Rank gaps by efficiency -function rankGapsByEfficiency(gaps, allDaysOffSet) { +function rankGapsByEfficiency(gaps: { start: Date; end: Date; gapLength: number }[], allDaysOffSet: Set): any[] { return gaps.map(gap => { const backward = calculateChain(gap.start, gap.gapLength, allDaysOffSet, 'backward'); const forward = calculateChain(gap.end, gap.gapLength, allDaysOffSet, 'forward'); @@ -149,7 +143,7 @@ function rankGapsByEfficiency(gaps, allDaysOffSet) { } // Calculate potential chain length and days off used -function calculateChain(startDate, gapLength, allDaysOffSet, direction) { +function calculateChain(startDate: Date, gapLength: number, allDaysOffSet: Set, direction: 'backward' | 'forward'): { chainLength: number; usedDaysOff: number } { let chainLength = gapLength; let usedDaysOff = 0; let currentDate = new Date(startDate); @@ -174,13 +168,12 @@ function calculateChain(startDate, gapLength, allDaysOffSet, direction) { } // Select days off based on ranked gaps -function selectDaysOff(rankedGaps, daysOff, allDaysOffSet, year) { - const selectedDays = []; +function selectDaysOff(rankedGaps: any[], daysOff: number, allDaysOffSet: Set, year: number): Date[] { + const selectedDays: Date[] = []; while (daysOff > 0 && rankedGaps.length > 0) { - const gap = rankedGaps.shift(); // Get the highest-ranked gap + const gap = rankedGaps.shift(); - // Determine the direction and starting point for filling the gap const increment = gap.fillFrom === 'start' ? 1 : -1; const startDate = gap.fillFrom === 'start' ? gap.start : gap.end; @@ -195,7 +188,6 @@ function selectDaysOff(rankedGaps, daysOff, allDaysOffSet, year) { } } - // Recalculate gaps and re-rank them after each assignment const newGaps = findGaps(allDaysOffSet, year); rankedGaps = rankGapsByEfficiency(newGaps, allDaysOffSet); } @@ -204,7 +196,7 @@ function selectDaysOff(rankedGaps, daysOff, allDaysOffSet, year) { } // Add consecutive days off to the list -function addConsecutiveDaysOff(consecutiveDaysOff, currentGroup, optimizedDaysOff) { +function addConsecutiveDaysOff(consecutiveDaysOff: { startDate: Date; endDate: Date; usedDaysOff: number; totalDays: number }[], currentGroup: Date[], optimizedDaysOff: Date[]) { const startDate = currentGroup[0]; const endDate = currentGroup[currentGroup.length - 1]; const totalDays = daysBetween(startDate, endDate) + 1; diff --git a/src/lib/ptoData.js b/src/lib/ptoData.ts similarity index 99% rename from src/lib/ptoData.js rename to src/lib/ptoData.ts index 279da49..e92d179 100644 --- a/src/lib/ptoData.js +++ b/src/lib/ptoData.ts @@ -1,4 +1,4 @@ -export const ptoData = { +export const ptoData: Record = { // Afghanistan AF: 20, // 20 days recreational leave, 15 paid public holidays diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 273042b..60415fd 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,62 +1,62 @@ - @@ -321,7 +313,7 @@ @media (max-width: 600px) { .calendar-grid { - grid-template-columns: repeat(2, 1fr); /* Allow 2 columns on smaller screens */ + grid-template-columns: repeat(2, 1fr); gap: 5px; padding: 5px; } @@ -400,23 +392,23 @@ button { background-color: #333; - border-left: 4px solid #111; /* Lighter border on the left */ - border-top: 4px solid #111; /* Lighter border on the top */ - border-right: 4px solid #555; /* Darker border on the right */ - border-bottom: 4px solid #555; /* Darker border on the bottom */ + border-left: 4px solid #111; + border-top: 4px solid #111; + border-right: 4px solid #555; + border-bottom: 4px solid #555; color: #fff; - font-size: 0.8em; /* Smaller font size */ + font-size: 0.8em; cursor: pointer; - padding: 3px; /* Reduced padding */ - margin: 0 3px; /* Reduced margin */ - border-radius: 4px; /* Slightly less rounded edges */ + padding: 3px; + margin: 0 3px; + border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); transition: transform 0.1s; display: inline-flex; align-items: center; justify-content: center; - width: 30px; /* Smaller width */ - height: 30px; /* Smaller height */ + width: 30px; + height: 30px; font-weight: bold; } @@ -443,18 +435,6 @@ font-size: 1.5em; } - .day { - aspect-ratio: 1; - text-align: center; - font-size: 0.6em; - display: flex; - align-items: center; - justify-content: center; - color: #ddd; - background-color: #222; - position: relative; - } - .how-it-works { margin: 20px auto; padding: 25px; @@ -482,10 +462,22 @@ text-align: center; font-size: 1em; transition: color 0.3s; + display: block; + width: auto; + background: none; + border: none; + padding: 0; } .toggle-text:hover { color: #61dafb; + background: none; + border: none; + padding: 0; + } + + .toggle-text:focus { + outline: none; } .reset-link { @@ -579,7 +571,7 @@

In - {getFlagEmoji(Object.keys(countriesList).find(code => countriesList[code] === selectedCountry))} + {getFlagEmoji(selectedCountryCode)} {#if selectedState} {selectedState}, {/if} @@ -594,9 +586,13 @@

I live in - {getFlagEmoji(Object.keys(countriesList).find(code => countriesList[code] === selectedCountry))} + {getFlagEmoji(selectedCountryCode)} {#if Object.keys(statesList).length > 0} - { handleStateChange(e); }} on:focus={() => { statesInput.value = ''; }} placeholder="State" aria-label="State" /> + handleStateChange(e)} + on:focus={(e) => { (e.target as HTMLInputElement).value = ''; }} + placeholder="State" + aria-label="State" /> {#each Object.entries(statesList) as [code, name]} @@ -604,7 +600,11 @@ in {/if} - { handleCountryChange(e); }} on:focus={() => { countriesInput.value = ''; }} aria-label="Select country" /> + handleCountryChange(e)} + on:focus={(e) => { (e.target as HTMLInputElement).value = ''; }} + placeholder="Country" + aria-label="Select country" /> and have @@ -682,15 +682,15 @@

-
+
+ {#if showHowItWorks}

How does this work?

- This tool detects your country from your IP, uses a default number of government-mandated days off from Wikipedia, and a countriesList[code] === selectedCountry)}.yaml`} target="_blank" rel="noopener noreferrer">list of holidays for {selectedCountry}. + This tool detects your country from your IP, uses a default number of government-mandated days off from Wikipedia, and a list of holidays for {selectedCountry}.

The algorithm prioritizes filling the shortest gaps first. It optimizes for spreading your holidays throughout the year to create the most number of consecutive vacation periods.