Add new utils file, deploy

This commit is contained in:
Zachary
2024-11-10 21:27:02 +01:00
parent eb7ec1f8bd
commit 09c38c8b1f
3 changed files with 194 additions and 330 deletions

View File

@@ -5,8 +5,20 @@
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap');
body {
font-family: 'Poppins', sans-serif; /* Modern, cooler font */
background-color: #121212; /* Dark background */
color: #e0e0e0; /* Light gray text for readability */
margin: 0;
padding: 0;
line-height: 1.6;
}
</style>
</head> </head>
<body style="margin: 0; font-family: Arial, sans-serif; background-color: black; color: white;"> <body>
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View File

@@ -6,6 +6,10 @@
export let holidays = []; export let holidays = [];
export let optimizedDaysOff = []; export let optimizedDaysOff = [];
// Reactive declarations
$: daysInMonth = getDaysInMonth(year, month);
$: firstDay = getFirstDayOfMonth(year, month);
function getDaysInMonth(year, month) { function getDaysInMonth(year, month) {
return new Date(year, month + 1, 0).getDate(); return new Date(year, month + 1, 0).getDate();
} }
@@ -14,9 +18,6 @@
return new Date(year, month, 1).getDay(); return new Date(year, month, 1).getDay();
} }
let daysInMonth = getDaysInMonth(year, month);
let firstDay = getFirstDayOfMonth(year, month);
function getHoliday(day) { function getHoliday(day) {
return holidays.find(holiday => return holidays.find(holiday =>
holiday.date.getFullYear() === year && holiday.date.getFullYear() === year &&

View File

@@ -1,19 +1,20 @@
<script> <script>
import { onMount } from 'svelte';
import countries from 'i18n-iso-countries'; import countries from 'i18n-iso-countries';
import enLocale from 'i18n-iso-countries/langs/en.json'; import enLocale from 'i18n-iso-countries/langs/en.json';
import CalendarMonth from '../lib/CalendarMonth.svelte'; import CalendarMonth from '../lib/CalendarMonth.svelte';
import Holidays from 'date-holidays'; import { getHolidaysForYear, optimizeDaysOff, calculateConsecutiveDaysOff } from '../lib/holidayUtils.js';
countries.registerLocale(enLocale); countries.registerLocale(enLocale);
let countriesList = countries.getNames('en'); let countriesList = countries.getNames('en');
let year = new Date().getFullYear(); let year = new Date().getFullYear();
let months = Array.from({ length: 12 }, (_, i) => i); let months = Array.from({ length: 12 }, (_, i) => i);
let selectedCountry = 'Belgium'; // Default country name let selectedCountry = 'Belgium';
let holidays = []; let holidays = [];
let daysOff = 24; // Default days off per year let daysOff = 24;
let optimizedDaysOff = []; let optimizedDaysOff = [];
let extendedHolidays = []; let consecutiveDaysOff = [];
function handleYearChange(event) { function handleYearChange(event) {
year = parseInt(event.target.value); year = parseInt(event.target.value);
@@ -32,376 +33,226 @@
function updateHolidays() { function updateHolidays() {
const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry); const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry);
if (countryCode) { if (countryCode) {
const hd = new Holidays(countryCode); holidays = getHolidaysForYear(countryCode, year);
holidays = hd.getHolidays(year) optimizedDaysOff = optimizeDaysOff(holidays, year, daysOff);
.filter(holiday => holiday.type === 'public') // Filter for public holidays consecutiveDaysOff = calculateConsecutiveDaysOff(holidays, optimizedDaysOff, year);
.map(holiday => ({
date: new Date(holiday.date),
name: holiday.name
}));
console.log('Holidays:', holidays);
optimizeDaysOff();
calculateExtendedHolidays();
} }
} }
function optimizeDaysOff() { function handleKeyDown(event) {
// Reset optimized days off switch (event.key) {
optimizedDaysOff = []; case 'ArrowRight':
event.preventDefault();
// Combine holidays and weekends year++;
const allDays = holidays.map(h => h.date); updateHolidays();
let daysToUse = daysOff;
// Create a list of potential days to take off, sorted by their potential to extend holidays
const potentialDaysOff = [];
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
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
const isHoliday = allDays.some(d => d.getTime() === date.getTime());
if (!isWeekend && !isHoliday) {
const prevDay = new Date(date);
prevDay.setDate(date.getDate() - 1);
const nextDay = new Date(date);
nextDay.setDate(date.getDate() + 1);
const extendsHoliday =
allDays.some(d => d.getTime() === prevDay.getTime()) ||
allDays.some(d => d.getTime() === nextDay.getTime());
if (extendsHoliday) {
potentialDaysOff.push(date);
}
}
}
}
// Sort potential days off by their ability to extend existing chains with multiple holidays
potentialDaysOff.sort((a, b) => {
const aConsecutive = calculateConsecutiveDaysIncludingHoliday(a, allDays);
const bConsecutive = calculateConsecutiveDaysIncludingHoliday(b, allDays);
return bConsecutive - aConsecutive || a.getTime() - b.getTime();
});
// Select days off from the sorted list, prioritizing those that extend chains with multiple holidays
for (let i = 0; i < potentialDaysOff.length && daysToUse > 0; i++) {
const date = potentialDaysOff[i];
const prevDay = new Date(date);
prevDay.setDate(date.getDate() - 1);
const nextDay = new Date(date);
nextDay.setDate(date.getDate() + 1);
// Check if adding this day creates a longer chain with multiple holidays
if (calculateConsecutiveDaysIncludingHoliday(date, allDays) > 0) {
optimizedDaysOff.push(date);
daysToUse--;
}
}
// Attempt to create full week chains
if (daysToUse > 0) {
for (let i = 0; i < optimizedDaysOff.length && daysToUse > 0; i++) {
const date = optimizedDaysOff[i];
const startOfWeek = new Date(date);
startOfWeek.setDate(date.getDate() - date.getDay() + 1); // Start of the week (Monday)
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 4); // End of the week (Friday)
for (let d = new Date(startOfWeek); d <= endOfWeek && daysToUse > 0; d.setDate(d.getDate() + 1)) {
if (!optimizedDaysOff.some(optDate => optDate.getTime() === d.getTime()) && !allDays.some(holiday => holiday.getTime() === d.getTime())) {
optimizedDaysOff.push(new Date(d));
daysToUse--;
}
}
}
}
console.log('Optimized Days Off:', optimizedDaysOff);
}
function calculateConsecutiveDaysIncludingHoliday(date, allDays) {
let consecutiveDays = 0;
let prevDay = new Date(date);
let nextDay = new Date(date);
let includesHoliday = false;
let holidayCount = 0;
// Count consecutive days before the date
while (true) {
prevDay.setDate(prevDay.getDate() - 1);
if (prevDay.getDay() === 0 || prevDay.getDay() === 6 || allDays.some(d => d.getTime() === prevDay.getTime())) {
consecutiveDays++;
if (allDays.some(d => d.getTime() === prevDay.getTime())) {
includesHoliday = true;
holidayCount++;
}
} else {
break; break;
} case 'ArrowLeft':
} event.preventDefault();
year--;
// Count consecutive days after the date updateHolidays();
while (true) { break;
nextDay.setDate(nextDay.getDate() + 1); case 'ArrowUp':
if (nextDay.getDay() === 0 || nextDay.getDay() === 6 || allDays.some(d => d.getTime() === nextDay.getTime())) { event.preventDefault();
consecutiveDays++; daysOff++;
if (allDays.some(d => d.getTime() === nextDay.getTime())) { updateHolidays();
includesHoliday = true; break;
holidayCount++; case 'ArrowDown':
} event.preventDefault();
} else { if (daysOff > 0) {
daysOff--;
updateHolidays();
}
break; break;
}
}
return includesHoliday ? holidayCount : 0;
}
function formatDate(date) {
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
function handleDaysOffChange(event) {
const value = parseInt(event.target.textContent);
if (!isNaN(value)) {
daysOff = value;
optimizeDaysOff();
calculateExtendedHolidays();
} else {
event.target.textContent = daysOff; // Revert to previous valid value if input is invalid
} }
} }
function handleDaysOffInput(event) { onMount(() => {
const value = event.target.textContent; window.addEventListener('keydown', handleKeyDown);
event.target.textContent = value.replace(/\D/g, ''); return () => {
} window.removeEventListener('keydown', handleKeyDown);
};
});
function calculateExtendedHolidays() {
const allDays = holidays.map(h => h.date); // Include all holiday dates
let remainingDaysOff = daysOff; // Track remaining days off
extendedHolidays = holidays
.filter(holiday => holiday.date.getDay() !== 0 && holiday.date.getDay() !== 6) // Only non-weekend holidays
.map(holiday => {
let startDate = new Date(holiday.date);
let endDate = new Date(holiday.date);
let daysUsed = 0;
// Extend before the holiday
while (daysUsed < remainingDaysOff) {
const prevDay = new Date(startDate);
prevDay.setDate(startDate.getDate() - 1);
if (!allDays.some(d => d.getTime() === prevDay.getTime()) && prevDay.getDay() !== 0 && prevDay.getDay() !== 6) {
startDate = prevDay;
daysUsed++;
} else {
break;
}
}
// Extend after the holiday
while (daysUsed < remainingDaysOff) {
const nextDay = new Date(endDate);
nextDay.setDate(endDate.getDate() + 1);
if (!allDays.some(d => d.getTime() === nextDay.getTime()) && nextDay.getDay() !== 0 && nextDay.getDay() !== 6) {
endDate = nextDay;
daysUsed++;
} else {
break;
}
}
remainingDaysOff -= daysUsed; // Deduct used days from remaining
// Calculate total consecutive days including weekends
const totalDays = calculateTotalConsecutiveDays(startDate, endDate, allDays);
return {
holidayName: holiday.name,
startDate: formatDate(startDate),
endDate: formatDate(endDate),
totalDays
};
});
// If there are remaining days off, try to use them to extend any holiday further
if (remainingDaysOff > 0) {
extendedHolidays.forEach(extended => {
let startDate = new Date(extended.startDate);
let endDate = new Date(extended.endDate);
// Extend before the holiday
while (remainingDaysOff > 0) {
const prevDay = new Date(startDate);
prevDay.setDate(startDate.getDate() - 1);
if (!allDays.some(d => d.getTime() === prevDay.getTime()) && prevDay.getDay() !== 0 && prevDay.getDay() !== 6) {
startDate = prevDay;
remainingDaysOff--;
} else {
break;
}
}
// Extend after the holiday
while (remainingDaysOff > 0) {
const nextDay = new Date(endDate);
nextDay.setDate(endDate.getDate() + 1);
if (!allDays.some(d => d.getTime() === nextDay.getTime()) && nextDay.getDay() !== 0 && nextDay.getDay() !== 6) {
endDate = nextDay;
remainingDaysOff--;
} else {
break;
}
}
extended.startDate = formatDate(startDate);
extended.endDate = formatDate(endDate);
extended.totalDays = calculateTotalConsecutiveDays(startDate, endDate, allDays);
});
}
}
function calculateTotalConsecutiveDays(startDate, endDate, allDays) {
let totalDays = 0;
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
const isWeekend = currentDate.getDay() === 0 || currentDate.getDay() === 6;
const isHoliday = allDays.some(d => d.getTime() === currentDate.getTime());
if (isWeekend || isHoliday || optimizedDaysOff.some(d => d.getTime() === currentDate.getTime())) {
totalDays++;
}
currentDate.setDate(currentDate.getDate() + 1);
}
return totalDays;
}
// Initialize holidays on load
updateHolidays(); updateHolidays();
</script> </script>
<style> <style>
header {
header, footer {
text-align: center; text-align: center;
background-color: black; color: #e0e0e0; /* Monochrome light text */
padding: 20px; padding: 15px;
color: white; border-bottom: 1px solid #333; /* Subtle border for separation */
}
h1 {
margin: 0;
font-size: 2em; font-size: 2em;
font-family: 'Arial', sans-serif;
} }
main {
max-width: 800px; footer {
border-top: 1px solid #333;
font-size: 0.9em;
}
.content-box {
max-width: 900px;
margin: 20px auto; margin: 20px auto;
padding: 20px; padding: 10px 0;
background-color: #333; /* Dark gray for main background */ background-color: #1e1e1e; /* Slightly lighter dark background for content boxes */
text-align: center; text-align: center;
font-size: 1.2em; font-size: 1em; /* Slightly smaller font size */
color: white; /* Ensure text is white for readability */ color: #e0e0e0; /* Light gray text */
border-radius: 10px; /* Add border-radius for a smoother look */ border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Add shadow for depth */ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.7);
margin-bottom: 40px;
} }
select, input {
margin: 0 5px; input {
font-size: 1em; margin: 0 10px;
padding: 5px; font-size: 0.9em; /* Slightly smaller font size */
background-color: #555; /* Darker background for inputs */ padding: 8px;
color: white; /* White text for inputs */ background-color: #2a2a2a; /* Darker gray for inputs */
border: none; color: #e0e0e0; /* Light text for inputs */
border: 1px solid #444;
border-radius: 5px; border-radius: 5px;
transition: background-color 0.3s;
width: auto; /* Dynamic width based on content */
} }
input::hover {
background-color: #333; /* Slightly lighter on hover */
}
ul { ul {
list-style-type: none; /* Remove bullet points */ list-style-type: none;
padding: 0; padding: 0;
} }
li { li {
margin: 10px 0; margin: 15px 0;
padding: 15px;
background-color: #2a2a2a;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
} }
.calendar-grid { .calendar-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* Adjust to fit available space */ grid-template-columns: repeat(3, 1fr); /* Default to 3 columns */
gap: 10px; gap: 20px;
justify-items: center; justify-items: center;
padding: 20px; padding: 20px;
} }
.calendar-container {
width: 100%;
aspect-ratio: 1;
background-color: #444; /* Slightly lighter gray for calendar */
color: white;
border-radius: 5px;
padding: 10px;
box-sizing: border-box; /* Ensure padding is included in width */
}
/* Media query for smaller screens */ @media (max-width: 768px) {
@media (max-width: 600px) {
.calendar-grid { .calendar-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); /* Adjust for smaller screens */ grid-template-columns: repeat(2, 1fr); /* 2 columns on smaller screens */
} }
} }
.editable-span { .calendar-container {
display: inline-block; width: 100%;
border-bottom: 1px dotted white; /* Dotted underline */ aspect-ratio: 1;
color: white; background-color: #2a2a2a; /* Dark gray for calendar */
font-size: 1em; color: #e0e0e0;
width: 3em; /* Adjust width as needed */ border-radius: 5px;
padding: 10px;
box-sizing: border-box;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.editable-input {
border: none;
border-bottom: 1px dotted #e0e0e0;
background: none;
color: inherit;
font-size: inherit;
font-family: inherit;
width: auto;
text-align: center; text-align: center;
margin: 0 5px; margin: 0 10px;
outline: none; outline: none;
} }
.editable-span:focus { .arrow-controls {
border-bottom: 1px solid white; /* Solid underline on focus */ display: inline-flex;
align-items: center;
} }
.highlight { button {
background-color: #4caf50; /* Green color for highlighting */ background-color: #2a2a2a;
color: white; border: 1px solid #444;
color: #e0e0e0;
font-size: 1em; /* Slightly smaller font size */
cursor: pointer;
padding: 5px 10px;
margin: 0 10px;
border-radius: 5px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1);
transition: background-color 0.3s, color 0.3s, transform 0.1s;
}
button:hover {
background-color: #333;
color: #fff; /* Change color on hover */
}
button:active {
transform: translateY(2px); /* Simulate button press */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
button:focus {
outline: 2px solid #61dafb; /* Accessibility focus outline */
}
.bold {
font-weight: bold;
font-size: 1.2em;
} }
</style> </style>
<header>
<h1>Stretch My Holidays</h1>
</header>
<main> <main>
I live in <div class="content-box">
<input list="countries" bind:value={selectedCountry} on:change={handleCountryChange} /> <p>
<datalist id="countries"> I live in
{#each Object.values(countriesList) as name} <input list="countries" class="editable-input bold" bind:value={selectedCountry} on:change={handleCountryChange} aria-label="Select country" />
<option value={name}></option> and have
{/each} <span class="arrow-controls">
</datalist> <button on:click={() => { daysOff++; updateHolidays(); }} aria-label="Increase days off"></button>
and have <span class="bold">{daysOff}</span>&nbsp;days off
<span contenteditable="true" class="editable-span" on:input={handleDaysOffInput} on:blur={handleDaysOffChange}>{daysOff}</span> days off per year <button on:click={() => { if (daysOff > 0) { daysOff--; updateHolidays(); } }} aria-label="Decrease days off"></button>
</span> in
<span class="arrow-controls">
<button on:click={() => { year--; updateHolidays(); }} aria-label="Previous year"></button>
<span class="bold">{year}</span>
<button on:click={() => { year++; updateHolidays(); }} aria-label="Next year"></button>
</span>
</p>
<div> <datalist id="countries">
<label for="year">Select Year: </label> {#each Object.values(countriesList) as name}
<input type="number" id="year" bind:value={year} on:input={handleYearChange} min="1900" max="2100" /> <option value={name}></option>
</div>
<div>
Extended Holidays:
<ul>
{#each extendedHolidays as extended}
<li>
{extended.totalDays} day holiday, including {extended.holidayName} from {extended.startDate} to {extended.endDate}
</li>
{/each} {/each}
</ul> </datalist>
</div> </div>
<div class="calendar-grid"> <div class="content-box">
{#each months as month} <div class="calendar-grid">
<div class="calendar-container"> {#each months as month}
<CalendarMonth {year} {month} {holidays} {optimizedDaysOff} /> <div class="calendar-container">
</div> <CalendarMonth {year} {month} {holidays} {optimizedDaysOff} />
{/each} </div>
{/each}
</div>
</div> </div>
</main> </main>
<footer>
<p>© 2023 Stretch My Holidays. All rights reserved.</p>
</footer>