209 lines
6.3 KiB
Svelte
209 lines
6.3 KiB
Svelte
<script>
|
|
import Tooltip from './Tooltip.svelte';
|
|
|
|
export let year;
|
|
export let month;
|
|
export let holidays = [];
|
|
export let optimizedDaysOff = [];
|
|
export let consecutiveDaysOff = [];
|
|
export let selectedCountryCode;
|
|
|
|
// Function to determine the first day of the week based on locale
|
|
function getFirstDayOfWeek(locale) {
|
|
// Convert 'us' to proper locale format
|
|
const normalizedLocale = locale.toLowerCase() === 'us' ? 'en-US' : `en-${locale.toUpperCase()}`;
|
|
|
|
try {
|
|
// Try to get firstDay from Intl.Locale weekInfo
|
|
// @ts-ignore .weekInfo exists on all browsers except Firefox
|
|
const weekFirstDay = new Intl.Locale(normalizedLocale)?.weekInfo?.firstDay;
|
|
if (weekFirstDay !== undefined) {
|
|
return weekFirstDay;
|
|
}
|
|
} catch (e) {
|
|
// Fallback if weekInfo is not supported
|
|
}
|
|
|
|
// Fallback: US starts on Sunday (0), most others on Monday (1)
|
|
return normalizedLocale === 'en-US' ? 0 : 1;
|
|
}
|
|
|
|
// Reactive declarations
|
|
$: daysInMonth = getDaysInMonth(year, month);
|
|
$: locale = selectedCountryCode ? new Intl.Locale(selectedCountryCode).toString() : 'us';
|
|
$: firstDayOfWeek = getFirstDayOfWeek(locale);
|
|
$: adjustedFirstDay = (getFirstDayOfMonth(year, month) - firstDayOfWeek + 7) % 7;
|
|
|
|
function getDaysInMonth(year, month) {
|
|
return new Date(year, month + 1, 0).getDate();
|
|
}
|
|
|
|
function getFirstDayOfMonth(year, month) {
|
|
return new Date(year, month, 1).getDay();
|
|
}
|
|
|
|
function getHoliday(day) {
|
|
return holidays.find(holiday =>
|
|
holiday.date.getFullYear() === year &&
|
|
holiday.date.getMonth() === month &&
|
|
holiday.date.getDate() === day
|
|
);
|
|
}
|
|
|
|
function isOptimizedDayOff(day) {
|
|
return optimizedDaysOff.some(date =>
|
|
date.getFullYear() === year &&
|
|
date.getMonth() === month &&
|
|
date.getDate() === day
|
|
);
|
|
}
|
|
|
|
// Determine the dominant month for each consecutive days off period
|
|
function getDominantMonth(period) {
|
|
const startMonth = period.startDate.getMonth();
|
|
const endMonth = period.endDate.getMonth();
|
|
|
|
if (startMonth === endMonth) {
|
|
return startMonth;
|
|
}
|
|
|
|
const startDays = getDaysInMonth(year, startMonth) - period.startDate.getDate() + 1;
|
|
const endDays = period.endDate.getDate();
|
|
|
|
return startDays > endDays ? startMonth : endMonth;
|
|
}
|
|
|
|
function isConsecutiveDayOff(day) {
|
|
return consecutiveDaysOff.some(period => {
|
|
const start = period.startDate;
|
|
const end = period.endDate;
|
|
const date = new Date(year, month, day);
|
|
return date >= start && date <= end;
|
|
});
|
|
}
|
|
|
|
// Function to determine if a day is a weekend
|
|
function isWeekend(day) {
|
|
const dayOfWeek = (adjustedFirstDay + day - 1) % 7;
|
|
// Calculate the indices for Saturday and Sunday
|
|
const saturdayIndex = (6 - firstDayOfWeek + 7) % 7;
|
|
const sundayIndex = (7 - firstDayOfWeek + 7) % 7;
|
|
return dayOfWeek === saturdayIndex || dayOfWeek === sundayIndex;
|
|
}
|
|
|
|
// Fixed array of day initials starting from Sunday
|
|
const dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
|
|
|
// Reactive declaration to get the ordered day initials
|
|
$: orderedDayInitials = dayInitials.slice(firstDayOfWeek).concat(dayInitials.slice(0, firstDayOfWeek));
|
|
</script>
|
|
|
|
<div class="calendar">
|
|
<div class="month-name">{new Date(year, month).toLocaleString('default', { month: 'long' })}</div>
|
|
|
|
<!-- Day initials header -->
|
|
{#each orderedDayInitials as dayInitial}
|
|
<div class="day-initial">{dayInitial}</div>
|
|
{/each}
|
|
|
|
{#each Array.from({ length: adjustedFirstDay }) as _}
|
|
<div class="day"></div>
|
|
{/each}
|
|
{#each Array.from({ length: daysInMonth }, (_, i) => i + 1) as day}
|
|
<div class="day {isWeekend(day) ? 'weekend' : ''} {getHoliday(day) ? 'holiday' : ''} {isOptimizedDayOff(day) ? 'optimized' : ''} {isConsecutiveDayOff(day) ? 'consecutive-day' : ''}">
|
|
{day}
|
|
{#if getHoliday(day)}
|
|
<Tooltip text={getHoliday(day).name} />
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="consecutive-days-off">
|
|
<ul>
|
|
{#each consecutiveDaysOff.filter(period => getDominantMonth(period) === month) as period}
|
|
<li>
|
|
{period.startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} to
|
|
{period.endDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}:
|
|
<strong>{period.totalDays} days</strong>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
|
|
<style>
|
|
.calendar {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
gap: 1px;
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
height: auto;
|
|
}
|
|
.day-initial {
|
|
text-align: center;
|
|
font-weight: bold;
|
|
color: #c5c6c7;
|
|
font-size: 0.6em;
|
|
}
|
|
.day {
|
|
aspect-ratio: 1;
|
|
text-align: center;
|
|
font-size: 0.7em;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #ddd;
|
|
background-color: #222;
|
|
position: relative;
|
|
}
|
|
.day:hover {
|
|
:global(.tooltip) {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
}
|
|
.weekend {
|
|
background-color: #585858;
|
|
}
|
|
.optimized {
|
|
background-color: #4caf50;
|
|
}
|
|
.holiday {
|
|
background-color: #3b1e6e;
|
|
cursor: pointer;
|
|
}
|
|
.consecutive-day {
|
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
|
}
|
|
.month-name {
|
|
grid-column: span 7;
|
|
text-align: center;
|
|
letter-spacing: 0.1em;
|
|
font-size: 1em;
|
|
text-transform: uppercase;
|
|
color: #c5c6c7;
|
|
margin-bottom: 5px;
|
|
}
|
|
.consecutive-days-off {
|
|
margin-top: 10px;
|
|
color: #fff;
|
|
}
|
|
.consecutive-days-off ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
.consecutive-days-off li {
|
|
font-size: 1em;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.month-name {
|
|
font-size: 0.9em;
|
|
}
|
|
.consecutive-days-off li {
|
|
font-size: 0.8em;
|
|
}
|
|
}
|
|
</style> |