Display national holidays on the calendar.
This commit is contained in:
parent
916d1d100a
commit
90dcdec386
@ -16,6 +16,7 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"date-holidays": "^3.25.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.5.0",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"vue": "^3.5.18"
|
"vue": "^3.5.18"
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import CalendarView from './components/CalendarView.vue'
|
import CalendarView from './components/CalendarView.vue'
|
||||||
import EventDialog from './components/EventDialog.vue'
|
import EventDialog from './components/EventDialog.vue'
|
||||||
|
import { useCalendarStore } from './stores/CalendarStore'
|
||||||
|
|
||||||
const eventDialog = ref(null)
|
const eventDialog = ref(null)
|
||||||
|
const calendarStore = useCalendarStore()
|
||||||
|
|
||||||
|
// Initialize holidays when app starts
|
||||||
|
onMounted(() => {
|
||||||
|
calendarStore.initializeHolidaysFromConfig()
|
||||||
|
})
|
||||||
|
|
||||||
const handleCreateEvent = (eventData) => {
|
const handleCreateEvent = (eventData) => {
|
||||||
if (eventDialog.value) {
|
if (eventDialog.value) {
|
||||||
|
@ -20,6 +20,13 @@
|
|||||||
--label-bg: #fafbfe;
|
--label-bg: #fafbfe;
|
||||||
--label-bg-rgb: 250, 251, 254;
|
--label-bg-rgb: 250, 251, 254;
|
||||||
|
|
||||||
|
/* Holiday colors */
|
||||||
|
--holiday-bg: rgba(255, 215, 0, 0.1);
|
||||||
|
--holiday-border: rgba(255, 215, 0, 0.3);
|
||||||
|
--holiday-text: #8b4513;
|
||||||
|
--holiday-label-bg: rgba(255, 215, 0, 0.8);
|
||||||
|
--holiday-label-text: #5d4037;
|
||||||
|
|
||||||
/* Input / recurrence tokens */
|
/* Input / recurrence tokens */
|
||||||
--input-border: var(--muted-alt);
|
--input-border: var(--muted-alt);
|
||||||
--input-focus: var(--accent);
|
--input-focus: var(--accent);
|
||||||
@ -34,28 +41,68 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Month tints (light) */
|
/* Month tints (light) */
|
||||||
.dec { background: hsl(220 50% 95%) }
|
.dec {
|
||||||
.jan { background: hsl(220 50% 92%) }
|
background: hsl(220 50% 95%);
|
||||||
.feb { background: hsl(220 50% 95%) }
|
}
|
||||||
.mar { background: hsl(125 60% 92%) }
|
.jan {
|
||||||
.apr { background: hsl(125 60% 95%) }
|
background: hsl(220 50% 92%);
|
||||||
.may { background: hsl(125 60% 92%) }
|
}
|
||||||
.jun { background: hsl(45 85% 95%) }
|
.feb {
|
||||||
.jul { background: hsl(45 85% 92%) }
|
background: hsl(220 50% 95%);
|
||||||
.aug { background: hsl(45 85% 95%) }
|
}
|
||||||
.sep { background: hsl(18 78% 92%) }
|
.mar {
|
||||||
.oct { background: hsl(18 78% 95%) }
|
background: hsl(125 60% 92%);
|
||||||
.nov { background: hsl(18 78% 92%) }
|
}
|
||||||
|
.apr {
|
||||||
|
background: hsl(125 60% 95%);
|
||||||
|
}
|
||||||
|
.may {
|
||||||
|
background: hsl(125 60% 92%);
|
||||||
|
}
|
||||||
|
.jun {
|
||||||
|
background: hsl(45 85% 95%);
|
||||||
|
}
|
||||||
|
.jul {
|
||||||
|
background: hsl(45 85% 92%);
|
||||||
|
}
|
||||||
|
.aug {
|
||||||
|
background: hsl(45 85% 95%);
|
||||||
|
}
|
||||||
|
.sep {
|
||||||
|
background: hsl(18 78% 92%);
|
||||||
|
}
|
||||||
|
.oct {
|
||||||
|
background: hsl(18 78% 95%);
|
||||||
|
}
|
||||||
|
.nov {
|
||||||
|
background: hsl(18 78% 92%);
|
||||||
|
}
|
||||||
|
|
||||||
/* Light mode — gray shades and colors */
|
/* Light mode — gray shades and colors */
|
||||||
.event-color-0 { background: hsl(0, 0%, 85%) } /* lightest grey */
|
.event-color-0 {
|
||||||
.event-color-1 { background: hsl(0, 0%, 75%) } /* light grey */
|
background: hsl(0, 0%, 85%);
|
||||||
.event-color-2 { background: hsl(0, 0%, 65%) } /* medium grey */
|
} /* lightest grey */
|
||||||
.event-color-3 { background: hsl(0, 0%, 55%) } /* dark grey */
|
.event-color-1 {
|
||||||
.event-color-4 { background: hsl(0, 70%, 70%) } /* red */
|
background: hsl(0, 0%, 75%);
|
||||||
.event-color-5 { background: hsl(90, 70%, 70%) } /* green */
|
} /* light grey */
|
||||||
.event-color-6 { background: hsl(230, 70%, 70%) } /* blue */
|
.event-color-2 {
|
||||||
.event-color-7 { background: hsl(280, 70%, 70%) } /* purple */
|
background: hsl(0, 0%, 65%);
|
||||||
|
} /* medium grey */
|
||||||
|
.event-color-3 {
|
||||||
|
background: hsl(0, 0%, 55%);
|
||||||
|
} /* dark grey */
|
||||||
|
.event-color-4 {
|
||||||
|
background: hsl(0, 70%, 70%);
|
||||||
|
} /* red */
|
||||||
|
.event-color-5 {
|
||||||
|
background: hsl(90, 70%, 70%);
|
||||||
|
} /* green */
|
||||||
|
.event-color-6 {
|
||||||
|
background: hsl(230, 70%, 70%);
|
||||||
|
} /* blue */
|
||||||
|
.event-color-7 {
|
||||||
|
background: hsl(280, 70%, 70%);
|
||||||
|
} /* purple */
|
||||||
|
|
||||||
/* Color tokens (dark) */
|
/* Color tokens (dark) */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
@ -69,7 +116,7 @@
|
|||||||
--muted: #7d8691;
|
--muted: #7d8691;
|
||||||
--muted-alt: #5d646d;
|
--muted-alt: #5d646d;
|
||||||
--accent: #3b82f6;
|
--accent: #3b82f6;
|
||||||
--accent-soft: rgba(59,130,246,0.15);
|
--accent-soft: rgba(59, 130, 246, 0.15);
|
||||||
--accent-hover: #2563eb;
|
--accent-hover: #2563eb;
|
||||||
--danger: #ef4444;
|
--danger: #ef4444;
|
||||||
--danger-hover: #dc2626;
|
--danger-hover: #dc2626;
|
||||||
@ -85,32 +132,79 @@
|
|||||||
--pill-bg: #222a32;
|
--pill-bg: #222a32;
|
||||||
--pill-active-bg: var(--accent);
|
--pill-active-bg: var(--accent);
|
||||||
--pill-active-ink: #fff;
|
--pill-active-ink: #fff;
|
||||||
--pill-hover-bg: rgba(255,255,255,0.08);
|
--pill-hover-bg: rgba(255, 255, 255, 0.08);
|
||||||
|
|
||||||
/* Vue component color mappings (dark) */
|
/* Vue component color mappings (dark) */
|
||||||
--bg: var(--panel);
|
--bg: var(--panel);
|
||||||
--border-color: #333;
|
--border-color: #333;
|
||||||
|
|
||||||
|
/* Holiday colors (dark mode) */
|
||||||
|
--holiday-bg: rgba(255, 193, 7, 0.15);
|
||||||
|
--holiday-border: rgba(255, 193, 7, 0.4);
|
||||||
|
--holiday-text: #ffc107;
|
||||||
|
--holiday-label-bg: rgba(255, 193, 7, 0.2);
|
||||||
|
--holiday-label-text: #fff8e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dec { background: hsl(220 50% 8%) }
|
.dec {
|
||||||
.jan { background: hsl(220 50% 6%) }
|
background: hsl(220 50% 8%);
|
||||||
.feb { background: hsl(220 50% 8%) }
|
}
|
||||||
.mar { background: hsl(125 60% 6%) }
|
.jan {
|
||||||
.apr { background: hsl(125 60% 8%) }
|
background: hsl(220 50% 6%);
|
||||||
.may { background: hsl(125 60% 6%) }
|
}
|
||||||
.jun { background: hsl(45 85% 8%) }
|
.feb {
|
||||||
.jul { background: hsl(45 85% 6%) }
|
background: hsl(220 50% 8%);
|
||||||
.aug { background: hsl(45 85% 8%) }
|
}
|
||||||
.sep { background: hsl(18 78% 6%) }
|
.mar {
|
||||||
.oct { background: hsl(18 78% 8%) }
|
background: hsl(125 60% 6%);
|
||||||
.nov { background: hsl(18 78% 6%) }
|
}
|
||||||
|
.apr {
|
||||||
|
background: hsl(125 60% 8%);
|
||||||
|
}
|
||||||
|
.may {
|
||||||
|
background: hsl(125 60% 6%);
|
||||||
|
}
|
||||||
|
.jun {
|
||||||
|
background: hsl(45 85% 8%);
|
||||||
|
}
|
||||||
|
.jul {
|
||||||
|
background: hsl(45 85% 6%);
|
||||||
|
}
|
||||||
|
.aug {
|
||||||
|
background: hsl(45 85% 8%);
|
||||||
|
}
|
||||||
|
.sep {
|
||||||
|
background: hsl(18 78% 6%);
|
||||||
|
}
|
||||||
|
.oct {
|
||||||
|
background: hsl(18 78% 8%);
|
||||||
|
}
|
||||||
|
.nov {
|
||||||
|
background: hsl(18 78% 6%);
|
||||||
|
}
|
||||||
|
|
||||||
.event-color-0 { background: hsl(0, 0%, 50%) } /* lightest grey */
|
.event-color-0 {
|
||||||
.event-color-1 { background: hsl(0, 0%, 40%) } /* light grey */
|
background: hsl(0, 0%, 50%);
|
||||||
.event-color-2 { background: hsl(0, 0%, 30%) } /* medium grey */
|
} /* lightest grey */
|
||||||
.event-color-3 { background: hsl(0, 0%, 20%) } /* dark grey */
|
.event-color-1 {
|
||||||
.event-color-4 { background: hsl(0, 70%, 40%) } /* red */
|
background: hsl(0, 0%, 40%);
|
||||||
.event-color-5 { background: hsl(90, 70%, 30%) } /* green - darker for perceptional purposes */
|
} /* light grey */
|
||||||
.event-color-6 { background: hsl(230, 70%, 40%) } /* blue */
|
.event-color-2 {
|
||||||
.event-color-7 { background: hsl(280, 70%, 40%) } /* purple */
|
background: hsl(0, 0%, 30%);
|
||||||
|
} /* medium grey */
|
||||||
|
.event-color-3 {
|
||||||
|
background: hsl(0, 0%, 20%);
|
||||||
|
} /* dark grey */
|
||||||
|
.event-color-4 {
|
||||||
|
background: hsl(0, 70%, 40%);
|
||||||
|
} /* red */
|
||||||
|
.event-color-5 {
|
||||||
|
background: hsl(90, 70%, 30%);
|
||||||
|
} /* green - darker for perceptional purposes */
|
||||||
|
.event-color-6 {
|
||||||
|
background: hsl(230, 70%, 40%);
|
||||||
|
} /* blue */
|
||||||
|
.event-color-7 {
|
||||||
|
background: hsl(280, 70%, 40%);
|
||||||
|
} /* purple */
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ const handleEventClick = (eventId) => {
|
|||||||
weekend: props.day.isWeekend,
|
weekend: props.day.isWeekend,
|
||||||
firstday: props.day.isFirstDay,
|
firstday: props.day.isFirstDay,
|
||||||
selected: props.day.isSelected,
|
selected: props.day.isSelected,
|
||||||
|
holiday: props.day.isHoliday,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
:data-date="props.day.date"
|
:data-date="props.day.date"
|
||||||
@ -27,6 +28,13 @@ const handleEventClick = (eventId) => {
|
|||||||
<h1>{{ props.day.displayText }}</h1>
|
<h1>{{ props.day.displayText }}</h1>
|
||||||
<span v-if="props.day.lunarPhase" class="lunar-phase">{{ props.day.lunarPhase }}</span>
|
<span v-if="props.day.lunarPhase" class="lunar-phase">{{ props.day.lunarPhase }}</span>
|
||||||
|
|
||||||
|
<!-- Holiday indicator -->
|
||||||
|
<div v-if="props.day.holiday" class="holiday-info">
|
||||||
|
<span class="holiday-name" :title="props.day.holiday.name">
|
||||||
|
{{ props.day.holiday.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Simple event display for now -->
|
<!-- Simple event display for now -->
|
||||||
<div v-if="props.day.events && props.day.events.length > 0" class="day-events">
|
<div v-if="props.day.events && props.day.events.length > 0" class="day-events">
|
||||||
<div
|
<div
|
||||||
@ -104,4 +112,61 @@ const handleEventClick = (eventId) => {
|
|||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell.holiday {
|
||||||
|
background-color: var(--holiday-bg, rgba(255, 215, 0, 0.1));
|
||||||
|
border-color: var(--holiday-border, rgba(255, 215, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.holiday h1 {
|
||||||
|
color: var(--holiday-text, #8b4513);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holiday-info {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.1em;
|
||||||
|
left: 0.1em;
|
||||||
|
right: 0.1em;
|
||||||
|
font-size: 0.7em;
|
||||||
|
line-height: 1;
|
||||||
|
max-height: 2.4em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.holiday-name {
|
||||||
|
display: block;
|
||||||
|
background: var(--holiday-label-bg, rgba(255, 215, 0, 0.8));
|
||||||
|
color: var(--holiday-label-text, #5d4037);
|
||||||
|
padding: 0.1em 0.2em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-events {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.5em;
|
||||||
|
right: 0.1em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-dot {
|
||||||
|
width: 0.6em;
|
||||||
|
height: 0.6em;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-dot:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -146,7 +146,6 @@ function createWeek(virtualWeek) {
|
|||||||
let monthToLabel = null
|
let monthToLabel = null
|
||||||
let labelYear = null
|
let labelYear = null
|
||||||
|
|
||||||
// Collect repeating base events once
|
|
||||||
const repeatingBases = []
|
const repeatingBases = []
|
||||||
if (calendarStore.events) {
|
if (calendarStore.events) {
|
||||||
for (const ev of calendarStore.events.values()) {
|
for (const ev of calendarStore.events.values()) {
|
||||||
@ -238,6 +237,9 @@ function createWeek(virtualWeek) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get holiday info once per day
|
||||||
|
const holiday = calendarStore.getHolidayForDate(dateStr)
|
||||||
|
|
||||||
days.push({
|
days.push({
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
dayOfMonth: cur.getDate(),
|
dayOfMonth: cur.getDate(),
|
||||||
@ -247,6 +249,8 @@ function createWeek(virtualWeek) {
|
|||||||
isWeekend: calendarStore.weekend[dow],
|
isWeekend: calendarStore.weekend[dow],
|
||||||
isFirstDay: isFirst,
|
isFirstDay: isFirst,
|
||||||
lunarPhase: lunarPhaseSymbol(cur),
|
lunarPhase: lunarPhaseSymbol(cur),
|
||||||
|
holiday: holiday,
|
||||||
|
isHoliday: holiday !== null,
|
||||||
isSelected:
|
isSelected:
|
||||||
selection.value.startDate &&
|
selection.value.startDate &&
|
||||||
selection.value.dayCount > 0 &&
|
selection.value.dayCount > 0 &&
|
||||||
|
@ -17,6 +17,118 @@ const weekend = computed({
|
|||||||
set: (v) => (calendarStore.weekend = [...v]),
|
set: (v) => (calendarStore.weekend = [...v]),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Holiday settings - simplified
|
||||||
|
const holidayMode = computed({
|
||||||
|
get: () => {
|
||||||
|
if (!calendarStore.config.holidays.enabled) {
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
|
return calendarStore.config.holidays.country || 'auto'
|
||||||
|
},
|
||||||
|
set: (v) => {
|
||||||
|
if (v === 'none') {
|
||||||
|
calendarStore.config.holidays.enabled = false
|
||||||
|
calendarStore.config.holidays.country = null
|
||||||
|
calendarStore.config.holidays.state = null
|
||||||
|
} else if (v === 'auto') {
|
||||||
|
const detectedCountry = getDetectedCountryCode()
|
||||||
|
if (detectedCountry) {
|
||||||
|
calendarStore.config.holidays.enabled = true
|
||||||
|
calendarStore.config.holidays.country = 'auto'
|
||||||
|
calendarStore.config.holidays.state = null
|
||||||
|
calendarStore.initializeHolidays('auto', null, null)
|
||||||
|
} else {
|
||||||
|
calendarStore.config.holidays.enabled = false
|
||||||
|
calendarStore.config.holidays.country = null
|
||||||
|
calendarStore.config.holidays.state = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
calendarStore.config.holidays.enabled = true
|
||||||
|
calendarStore.config.holidays.country = v
|
||||||
|
calendarStore.config.holidays.state = null
|
||||||
|
calendarStore.initializeHolidays(v, null, null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const holidayState = computed({
|
||||||
|
get: () => calendarStore.config.holidays.state,
|
||||||
|
set: (v) => {
|
||||||
|
calendarStore.config.holidays.state = v
|
||||||
|
const country =
|
||||||
|
calendarStore.config.holidays.country === 'auto'
|
||||||
|
? 'auto'
|
||||||
|
: calendarStore.config.holidays.country
|
||||||
|
calendarStore.initializeHolidays(country, v, calendarStore.config.holidays.region)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get detected country code
|
||||||
|
function getDetectedCountryCode() {
|
||||||
|
const locale = navigator.language || navigator.languages?.[0]
|
||||||
|
if (!locale) return null
|
||||||
|
|
||||||
|
const parts = locale.split('-')
|
||||||
|
if (parts.length < 2) return null
|
||||||
|
|
||||||
|
return parts[parts.length - 1].toUpperCase()
|
||||||
|
} // Get display name for any country code
|
||||||
|
function getCountryDisplayName(countryCode) {
|
||||||
|
if (!countryCode || countryCode.length !== 2) {
|
||||||
|
return countryCode
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const regionNames = new Intl.DisplayNames([navigator.language || 'en'], { type: 'region' })
|
||||||
|
return regionNames.of(countryCode) || countryCode
|
||||||
|
} catch {
|
||||||
|
return countryCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get display name for auto option
|
||||||
|
const autoDisplayName = computed(() => {
|
||||||
|
const detectedCode = getDetectedCountryCode()
|
||||||
|
if (!detectedCode) return 'Auto'
|
||||||
|
return getCountryDisplayName(detectedCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get state/province name from state code
|
||||||
|
function getStateName(stateCode, countryCode) {
|
||||||
|
return stateCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get available countries and states
|
||||||
|
const availableCountries = computed(() => {
|
||||||
|
try {
|
||||||
|
const countries = calendarStore.getAvailableCountries()
|
||||||
|
const countryArray = Array.isArray(countries) ? countries : ['US', 'GB', 'DE', 'FR', 'CA', 'AU']
|
||||||
|
|
||||||
|
return countryArray.sort((a, b) => {
|
||||||
|
const nameA = getCountryDisplayName(a)
|
||||||
|
const nameB = getCountryDisplayName(b)
|
||||||
|
return nameA.localeCompare(nameB, navigator.language || 'en')
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get available countries:', error)
|
||||||
|
return ['US', 'GB', 'DE', 'FR', 'CA', 'AU']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const availableStates = computed(() => {
|
||||||
|
try {
|
||||||
|
if (holidayMode.value === 'none') return []
|
||||||
|
let country = holidayMode.value
|
||||||
|
if (holidayMode.value === 'auto') {
|
||||||
|
country = getDetectedCountryCode()
|
||||||
|
if (!country) return []
|
||||||
|
}
|
||||||
|
const states = calendarStore.getAvailableStates(country)
|
||||||
|
return Array.isArray(states) ? states : []
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get available states:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
// Toggle behavior: if already open, close instead
|
// Toggle behavior: if already open, close instead
|
||||||
show.value = !show.value
|
show.value = !show.value
|
||||||
@ -29,14 +141,12 @@ function resetAll() {
|
|||||||
if (typeof calendarStore.$reset === 'function') {
|
if (typeof calendarStore.$reset === 'function') {
|
||||||
calendarStore.$reset()
|
calendarStore.$reset()
|
||||||
} else {
|
} else {
|
||||||
// Fallback manual reset if $reset not available
|
|
||||||
calendarStore.today = new Date().toISOString().slice(0, 10)
|
calendarStore.today = new Date().toISOString().slice(0, 10)
|
||||||
calendarStore.now = new Date().toISOString()
|
calendarStore.now = new Date().toISOString()
|
||||||
calendarStore.events = new Map()
|
calendarStore.events = new Map()
|
||||||
calendarStore.weekend = [6, 0] // common default (Sat/Sun) if locale helper not accessible here
|
calendarStore.weekend = [6, 0]
|
||||||
calendarStore.config.first_day = 1
|
calendarStore.config.first_day = 1
|
||||||
}
|
}
|
||||||
// Optional: close dialog after reset
|
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,6 +178,34 @@ defineExpose({ open })
|
|||||||
<WeekdaySelector v-model="weekend" :first-day="firstDay" />
|
<WeekdaySelector v-model="weekend" :first-day="firstDay" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<label class="ec-field">
|
||||||
|
<span>Holiday Region</span>
|
||||||
|
<div class="holiday-row">
|
||||||
|
<select v-model="holidayMode" class="country-select">
|
||||||
|
<option value="none">Do not show holidays</option>
|
||||||
|
<option v-if="getDetectedCountryCode()" value="auto">
|
||||||
|
{{ autoDisplayName }} (Auto)
|
||||||
|
</option>
|
||||||
|
<option v-for="country in availableCountries" :key="country" :value="country">
|
||||||
|
{{ getCountryDisplayName(country) }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
v-if="holidayMode !== 'none' && availableStates.length > 0"
|
||||||
|
v-model="holidayState"
|
||||||
|
class="state-select"
|
||||||
|
>
|
||||||
|
<option value="">None</option>
|
||||||
|
<option v-for="state in availableStates" :key="state" :value="state">
|
||||||
|
{{ state }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="footer-row split">
|
<div class="footer-row split">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
@ -86,6 +224,12 @@ defineExpose({ open })
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
.setting-group h3 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--strong);
|
||||||
|
}
|
||||||
.ec-field {
|
.ec-field {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
@ -94,6 +238,13 @@ defineExpose({ open })
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
.holiday-settings {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-left: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
border: 1px solid var(--muted);
|
border: 1px solid var(--muted);
|
||||||
background: var(--panel-alt, transparent);
|
background: var(--panel-alt, transparent);
|
||||||
@ -101,6 +252,22 @@ select {
|
|||||||
padding: 0.4rem 0.5rem;
|
padding: 0.4rem 0.5rem;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.holiday-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-select {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
/* WeekdaySelector display tweaks */
|
/* WeekdaySelector display tweaks */
|
||||||
.footer-row {
|
.footer-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -10,6 +10,14 @@ import {
|
|||||||
getVirtualOccurrenceEndDate,
|
getVirtualOccurrenceEndDate,
|
||||||
occursOnOrSpansDate,
|
occursOnOrSpansDate,
|
||||||
} from '@/utils/date'
|
} from '@/utils/date'
|
||||||
|
import {
|
||||||
|
initializeHolidays,
|
||||||
|
getHolidayForDate,
|
||||||
|
isHoliday,
|
||||||
|
getAvailableCountries,
|
||||||
|
getAvailableStates,
|
||||||
|
getHolidayConfig,
|
||||||
|
} from '@/utils/holidays'
|
||||||
|
|
||||||
const MIN_YEAR = 1900
|
const MIN_YEAR = 1900
|
||||||
const MAX_YEAR = 2100
|
const MAX_YEAR = 2100
|
||||||
@ -17,14 +25,22 @@ const MAX_YEAR = 2100
|
|||||||
export const useCalendarStore = defineStore('calendar', {
|
export const useCalendarStore = defineStore('calendar', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
today: toLocalString(new Date()),
|
today: toLocalString(new Date()),
|
||||||
now: new Date().toISOString(), // store as ISO string
|
now: new Date().toISOString(),
|
||||||
events: new Map(), // id -> event object (primary)
|
events: new Map(),
|
||||||
weekend: getLocaleWeekendDays(),
|
weekend: getLocaleWeekendDays(),
|
||||||
|
_holidayConfigSignature: null,
|
||||||
|
_holidaysInitialized: false,
|
||||||
config: {
|
config: {
|
||||||
select_days: 1000,
|
select_days: 1000,
|
||||||
min_year: MIN_YEAR,
|
min_year: MIN_YEAR,
|
||||||
max_year: MAX_YEAR,
|
max_year: MAX_YEAR,
|
||||||
first_day: 1, // Force Monday as week start
|
first_day: 1,
|
||||||
|
holidays: {
|
||||||
|
enabled: true,
|
||||||
|
country: 'auto',
|
||||||
|
state: null,
|
||||||
|
region: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@ -35,7 +51,34 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
// Determine if a repeating event occurs on a specific date (YYYY-MM-DD) without iterating all occurrences.
|
// Initialize holidays based on current config
|
||||||
|
initializeHolidaysFromConfig() {
|
||||||
|
if (!this.config.holidays.enabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let country = this.config.holidays.country
|
||||||
|
if (country === 'auto') {
|
||||||
|
const locale = navigator.language || navigator.languages?.[0]
|
||||||
|
if (!locale) return false
|
||||||
|
|
||||||
|
const parts = locale.split('-')
|
||||||
|
if (parts.length < 2) return false
|
||||||
|
|
||||||
|
country = parts[parts.length - 1].toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (country) {
|
||||||
|
return this.initializeHolidays(
|
||||||
|
country,
|
||||||
|
this.config.holidays.state,
|
||||||
|
this.config.holidays.region,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
|
||||||
occursOnDate(event, dateStr) {
|
occursOnDate(event, dateStr) {
|
||||||
return getOccurrenceIndex(event, dateStr) !== null
|
return getOccurrenceIndex(event, dateStr) !== null
|
||||||
},
|
},
|
||||||
@ -48,6 +91,102 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Holiday management
|
||||||
|
initializeHolidays(country, state = null, region = null) {
|
||||||
|
let actualCountry = country
|
||||||
|
if (country === 'auto') {
|
||||||
|
const locale = navigator.language || navigator.languages?.[0]
|
||||||
|
if (!locale) return false
|
||||||
|
|
||||||
|
const parts = locale.split('-')
|
||||||
|
if (parts.length < 2) return false
|
||||||
|
|
||||||
|
actualCountry = parts[parts.length - 1].toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.holidays.country !== 'auto') {
|
||||||
|
this.config.holidays.country = country
|
||||||
|
}
|
||||||
|
this.config.holidays.state = state
|
||||||
|
this.config.holidays.region = region
|
||||||
|
|
||||||
|
this._holidayConfigSignature = null
|
||||||
|
this._holidaysInitialized = false
|
||||||
|
|
||||||
|
return initializeHolidays(actualCountry, state, region)
|
||||||
|
},
|
||||||
|
_ensureHolidaysInitialized() {
|
||||||
|
if (!this.config.holidays.enabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let actualCountry = this.config.holidays.country
|
||||||
|
if (this.config.holidays.country === 'auto') {
|
||||||
|
const locale = navigator.language || navigator.languages?.[0]
|
||||||
|
if (!locale) return false
|
||||||
|
|
||||||
|
const parts = locale.split('-')
|
||||||
|
if (parts.length < 2) return false
|
||||||
|
|
||||||
|
actualCountry = parts[parts.length - 1].toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
const configSignature = `${actualCountry}-${this.config.holidays.state}-${this.config.holidays.region}-${this.config.holidays.enabled}`
|
||||||
|
|
||||||
|
if (this._holidayConfigSignature !== configSignature || !this._holidaysInitialized) {
|
||||||
|
const success = initializeHolidays(
|
||||||
|
actualCountry,
|
||||||
|
this.config.holidays.state,
|
||||||
|
this.config.holidays.region,
|
||||||
|
)
|
||||||
|
if (success) {
|
||||||
|
this._holidayConfigSignature = configSignature
|
||||||
|
this._holidaysInitialized = true
|
||||||
|
}
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._holidaysInitialized
|
||||||
|
},
|
||||||
|
|
||||||
|
getHolidayForDate(dateStr) {
|
||||||
|
if (!this._ensureHolidaysInitialized()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return getHolidayForDate(dateStr)
|
||||||
|
},
|
||||||
|
|
||||||
|
isHoliday(dateStr) {
|
||||||
|
if (!this._ensureHolidaysInitialized()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isHoliday(dateStr)
|
||||||
|
},
|
||||||
|
|
||||||
|
getAvailableCountries() {
|
||||||
|
try {
|
||||||
|
const countries = getAvailableCountries()
|
||||||
|
return Array.isArray(countries) ? countries : ['US', 'GB', 'DE', 'FR', 'CA', 'AU']
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get available countries:', error)
|
||||||
|
return ['US', 'GB', 'DE', 'FR', 'CA', 'AU']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getAvailableStates(country) {
|
||||||
|
try {
|
||||||
|
const states = getAvailableStates(country)
|
||||||
|
return Array.isArray(states) ? states : []
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get available states for', country, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleHolidays() {
|
||||||
|
this.config.holidays.enabled = !this.config.holidays.enabled
|
||||||
|
},
|
||||||
|
|
||||||
// Event management
|
// Event management
|
||||||
generateId() {
|
generateId() {
|
||||||
try {
|
try {
|
||||||
|
179
src/utils/holidays.js
Normal file
179
src/utils/holidays.js
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
// holidays.js — Holiday utilities using date-holidays package
|
||||||
|
import Holidays from 'date-holidays'
|
||||||
|
|
||||||
|
let holidaysInstance = null
|
||||||
|
let currentCountry = null
|
||||||
|
let currentState = null
|
||||||
|
let currentRegion = null
|
||||||
|
let holidayCache = new Map()
|
||||||
|
let yearCache = new Map()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize holidays for a specific country/region
|
||||||
|
* @param {string} country - Country code (e.g., 'US', 'GB', 'DE')
|
||||||
|
* @param {string} [state] - State/province code (e.g., 'CA' for California)
|
||||||
|
* @param {string} [region] - Region code
|
||||||
|
*/
|
||||||
|
export function initializeHolidays(country, state = null, region = null) {
|
||||||
|
if (!country) {
|
||||||
|
console.warn('No country provided for holiday initialization')
|
||||||
|
holidaysInstance = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
holidaysInstance = new Holidays(country, state, region)
|
||||||
|
currentCountry = country
|
||||||
|
currentState = state
|
||||||
|
currentRegion = region
|
||||||
|
|
||||||
|
holidayCache.clear()
|
||||||
|
yearCache.clear()
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to initialize holidays for', country, state, region, error)
|
||||||
|
holidaysInstance = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get holidays for a specific year
|
||||||
|
* @param {number} year - The year to get holidays for
|
||||||
|
* @returns {Array} Array of holiday objects
|
||||||
|
*/
|
||||||
|
export function getHolidaysForYear(year) {
|
||||||
|
if (!holidaysInstance) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yearCache.has(year)) {
|
||||||
|
return yearCache.get(year)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const holidays = holidaysInstance.getHolidays(year)
|
||||||
|
yearCache.set(year, holidays)
|
||||||
|
return holidays
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get holidays for year', year, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get holiday for a specific date
|
||||||
|
* @param {string|Date} date - Date in YYYY-MM-DD format or Date object
|
||||||
|
* @returns {Object|null} Holiday object or null if no holiday
|
||||||
|
*/
|
||||||
|
export function getHolidayForDate(date) {
|
||||||
|
if (!holidaysInstance) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = typeof date === 'string' ? date : date.toISOString().split('T')[0]
|
||||||
|
if (holidayCache.has(cacheKey)) {
|
||||||
|
return holidayCache.get(cacheKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let dateObj
|
||||||
|
if (typeof date === 'string') {
|
||||||
|
const [year, month, day] = date.split('-').map(Number)
|
||||||
|
dateObj = new Date(year, month - 1, day)
|
||||||
|
} else {
|
||||||
|
dateObj = date
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = dateObj.getFullYear()
|
||||||
|
const holidays = getHolidaysForYear(year)
|
||||||
|
|
||||||
|
const holiday = holidays.find((h) => {
|
||||||
|
const holidayDate = new Date(h.date)
|
||||||
|
return (
|
||||||
|
holidayDate.getFullYear() === dateObj.getFullYear() &&
|
||||||
|
holidayDate.getMonth() === dateObj.getMonth() &&
|
||||||
|
holidayDate.getDate() === dateObj.getDate()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = holiday || null
|
||||||
|
holidayCache.set(cacheKey, result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get holiday for date', date, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a date is a holiday
|
||||||
|
* @param {string|Date} date - Date in YYYY-MM-DD format or Date object
|
||||||
|
* @returns {boolean} True if the date is a holiday
|
||||||
|
*/
|
||||||
|
export function isHoliday(date) {
|
||||||
|
return getHolidayForDate(date) !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available countries for holidays
|
||||||
|
* @returns {Array} Array of country codes
|
||||||
|
*/
|
||||||
|
export function getAvailableCountries() {
|
||||||
|
try {
|
||||||
|
const holidays = new Holidays()
|
||||||
|
const countries = holidays.getCountries()
|
||||||
|
|
||||||
|
// The getCountries method might return an object, convert to array of keys
|
||||||
|
if (countries && typeof countries === 'object') {
|
||||||
|
return Array.isArray(countries) ? countries : Object.keys(countries)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['US', 'GB', 'DE', 'FR', 'CA', 'AU'] // Fallback
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get available countries', error)
|
||||||
|
return ['US', 'GB', 'DE', 'FR', 'CA', 'AU'] // Fallback to common countries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available states/regions for a country
|
||||||
|
* @param {string} country - Country code
|
||||||
|
* @returns {Array} Array of state/region codes
|
||||||
|
*/
|
||||||
|
export function getAvailableStates(country) {
|
||||||
|
try {
|
||||||
|
if (!country) return []
|
||||||
|
|
||||||
|
const holidays = new Holidays()
|
||||||
|
const states = holidays.getStates(country)
|
||||||
|
|
||||||
|
// The getStates method might return an object, convert to array of keys
|
||||||
|
if (states && typeof states === 'object') {
|
||||||
|
return Array.isArray(states) ? states : Object.keys(states)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get available states for', country, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get holiday configuration info
|
||||||
|
* @returns {Object} Current holiday configuration
|
||||||
|
*/
|
||||||
|
export function getHolidayConfig() {
|
||||||
|
return {
|
||||||
|
country: currentCountry,
|
||||||
|
state: currentState,
|
||||||
|
region: currentRegion,
|
||||||
|
initialized: !!holidaysInstance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize with US holidays by default
|
||||||
|
initializeHolidays('US')
|
Loading…
x
Reference in New Issue
Block a user