kenat
Version:
A JavaScript library for the Ethiopian calendar with date and time support.
397 lines (343 loc) • 15.2 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kenat Calendar Example</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
text-align: center;
background-color: #f9f9f9;
padding: 1rem;
}
table {
border-collapse: collapse;
margin: 1rem auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
th,
td {
border: 1px solid #e0e0e0;
padding: 0.5rem;
width: 85px;
height: 85px;
vertical-align: top;
position: relative;
}
th {
background-color: #f5f5f5;
font-weight: 600;
}
.today {
background-color: #fffde7;
border: 2px solid #ffc107;
font-weight: bold;
}
td.event-day {
cursor: pointer;
}
.official-holiday {
background-color: #e3f2fd;
}
.christian-holiday {
background-color: #ebf5ff;
}
.jummah-day {
background-color: #e8f5e9;
}
.holiday-labels {
font-size: 0.75rem;
color: #333;
margin-top: 5px;
line-height: 1.2;
text-align: left;
max-height: 3.6em;
overflow: hidden;
}
.holiday-labels .is-nigs {
font-weight: bold;
color: #b58c00;
}
.holiday-labels .jummah-label {
font-weight: bold;
color: #388e3c;
}
.header-controls,
.controls,
.filter-controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin: 1rem 0;
align-items: center;
}
.nav-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.controls {
gap: 20px;
}
.filter-controls {
margin-bottom: 1.5rem;
background-color: #fff;
padding: 1rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.filter-controls label,
.controls label {
font-size: 0.9rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.filter-controls label.disabled {
color: #aaa;
cursor: not-allowed;
}
button,
select {
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
cursor: pointer;
border-radius: 6px;
border: 1px solid #ccc;
background-color: #fff;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 25px;
border: 1px solid #888;
width: 90%;
max-width: 500px;
border-radius: 10px;
position: relative;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
text-align: left;
}
.modal-close {
color: #aaa;
position: absolute;
top: 10px;
right: 20px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.modal-content h3 {
margin-top: 0;
}
.modal-content .is-nigs-title {
color: #b58c00;
}
.modal-content hr {
border: 0;
border-top: 1px solid #eee;
margin: 15px 0;
}
</style>
</head>
<body>
<h1>Kenat Calendar</h1>
<div id="calendar">Loading...</div>
<div id="holidayModal" class="modal">
<div class="modal-content"><span class="modal-close">×</span>
<div id="modalBody"></div>
</div>
</div>
<script type="module">
import Kenat, { MonthGrid, HolidayTags, toArabic } from '../../src/index.js';
import { monthNames } from '../../src/constants.js';
let monthGridInstance;
let useGeez = false;
let weekdayLang = 'amharic';
let weekStart = 1;
let holidayFilter = null;
let activeMode = 'public';
let showAllSaints = false;
const initialDate = new Kenat().getEthiopian();
let currentYear = initialDate.year;
let currentMonth = initialDate.month;
const modal = document.getElementById('holidayModal');
const modalBody = document.getElementById('modalBody');
const closeModal = document.querySelector('.modal-close');
function showHolidayModal(holidays) {
let content = '';
holidays.forEach((h, index) => {
const nigsClass = h.isNigs ? 'is-nigs-title' : '';
const description = h.description ? `<p>${h.description}</p>` : '';
content += `<h3 class="${nigsClass}">${h.name}</h3>${description}`;
if (index < holidays.length - 1) content += '<hr>';
});
modalBody.innerHTML = content;
modal.style.display = 'flex';
}
closeModal.onclick = () => { modal.style.display = 'none'; }
window.onclick = (event) => { if (event.target == modal) modal.style.display = 'none'; }
function renderCalendar(gridData) {
let { headers, days, year, month } = gridData;
const holidaysForDay = (item) => {
return item.holidays.map(h => {
let specialClass = '';
if (h.isNigs) specialClass = 'is-nigs';
if (h.key === 'jummah') specialClass = 'jummah-label';
return `<div class="${specialClass}">${h.name}</div>`;
}).join('');
};
const yearForComparison = typeof year === 'string' ? toArabic(year) : year;
const yearOptions = Array.from({ length: 201 }, (_, i) => 1900 + i).map(y => `<option value="${y}" ${y === yearForComparison ? 'selected' : ''}>${y}</option>`).join('');
const monthOptions = monthNames.amharic.map((name, i) => `<option value="${i + 1}" ${i + 1 === month ? 'selected' : ''}>${name}</option>`).join('');
let html = `<div class="header-controls"><button id="prevMonth">⬅️</button><div class="nav-controls"><select id="monthSelector">${monthOptions}</select><select id="yearSelector">${yearOptions}</select></div><button id="nextMonth">➡️</button></div><div class="controls" id="topControls"></div><div class="filter-controls" id="filterControls"></div><table><thead><tr>${headers.map(day => `<th>${day}</th>`).join('')}</tr></thead><tbody>`;
for (let i = 0; i < days.length; i += 7) {
html += '<tr>';
for (let j = 0; j < 7; j++) {
const item = days[i + j];
if (!item) {
html += '<td></td>';
} else {
const hasEvents = item.holidays && item.holidays.length > 0;
let dayClasses = [item.isToday ? 'today' : ''];
if (hasEvents) {
dayClasses.push('event-day');
const isOfficial = item.holidays.some(h => h.tags.includes(HolidayTags.PUBLIC) || h.tags.includes(HolidayTags.STATE));
const isChristian = item.holidays.some(h => h.tags.includes(HolidayTags.CHRISTIAN));
const isJummah = item.holidays.some(h => h.key === 'jummah');
if (isOfficial) dayClasses.push('official-holiday');
if (isChristian && !isOfficial) dayClasses.push('christian-holiday');
if (isJummah) dayClasses.push('jummah-day');
}
const holidaysText = hasEvents ? holidaysForDay(item) : '';
const eventAttr = hasEvents ? `data-day-index="${i + j}"` : '';
html += `<td class="${dayClasses.join(' ')}" ${eventAttr}><strong>${item.ethiopian.day}</strong><br/><small>${item.gregorian.month}/${item.gregorian.day}</small>${holidaysText ? `<div class="holiday-labels">${holidaysText}</div>` : ''}</td>`;
}
}
html += '</tr>';
}
html += '</tbody></table>';
document.getElementById('calendar').innerHTML = html;
renderControls();
attachEventListeners();
}
function renderControls() {
const topControlsContainer = document.getElementById('topControls');
const filterControlsContainer = document.getElementById('filterControls');
topControlsContainer.innerHTML = `<label for="modeSelector">Mode:</label><select id="modeSelector"><option value="none" ${activeMode === 'none' ? 'selected' : ''}>All</option><option value="christian" ${activeMode === 'christian' ? 'selected' : ''}>Christian</option><option value="muslim" ${activeMode === 'muslim' ? 'selected' : ''}>Muslim</option></select><button id="toggleGeez">Geez (${useGeez ? 'ON' : 'OFF'})</button><button id="toggleLang">Lang (${weekdayLang})</button><button id="toggleWeekStart">Start (${weekStart === 1 ? 'Mon' : 'Sun'})</button>`;
let filterHtml = '';
if (activeMode === 'christian') {
filterHtml += `<label><input type="checkbox" id="showAllSaintsToggle" ${showAllSaints ? 'checked' : ''}> Show All Saints</label>`;
}
const isDisabled = activeMode !== 'none';
const disabledAttr = isDisabled ? 'disabled' : '';
filterHtml += `<label class="${isDisabled ? 'disabled' : ''}"><input type="checkbox" name="holidayTag" value="all" ${!holidayFilter || isDisabled ? 'checked' : ''} ${disabledAttr}> All Holidays</label>`;
for (const key in HolidayTags) {
const value = HolidayTags[key];
const isChecked = !isDisabled && holidayFilter && holidayFilter.includes(value);
filterHtml += `<label class="${isDisabled ? 'disabled' : ''}"><input type="checkbox" name="holidayTag" value="${value}" ${isChecked ? 'checked' : ''} ${disabledAttr}> ${value}</label>`;
}
filterControlsContainer.innerHTML = filterHtml;
}
function attachEventListeners() {
document.getElementById('prevMonth').onclick = () => {
const newGrid = monthGridInstance.down().generate();
currentYear = (typeof newGrid.year === 'string') ? toArabic(newGrid.year) : newGrid.year;
currentMonth = newGrid.month;
renderCalendar(newGrid);
};
document.getElementById('nextMonth').onclick = () => {
const newGrid = monthGridInstance.up().generate();
currentYear = (typeof newGrid.year === 'string') ? toArabic(newGrid.year) : newGrid.year;
currentMonth = newGrid.month;
renderCalendar(newGrid);
};
document.getElementById('monthSelector').onchange = (e) => { currentMonth = parseInt(e.target.value); rerender(); };
document.getElementById('yearSelector').onchange = (e) => { currentYear = parseInt(e.target.value); rerender(); };
document.getElementById('toggleGeez').onclick = () => { useGeez = !useGeez; rerender(); };
document.getElementById('toggleLang').onclick = () => { weekdayLang = weekdayLang === 'amharic' ? 'english' : 'amharic'; rerender(); };
document.getElementById('toggleWeekStart').onclick = () => { weekStart = weekStart === 1 ? 0 : 1; rerender(); };
document.getElementById('modeSelector').onchange = (e) => {
activeMode = e.target.value;
if (activeMode !== 'none') {
holidayFilter = null;
}
showAllSaints = false;
rerender();
};
const saintsToggle = document.getElementById('showAllSaintsToggle');
if (saintsToggle) {
saintsToggle.onchange = (e) => {
showAllSaints = e.target.checked;
rerender();
};
}
document.querySelectorAll('.event-day').forEach(cell => {
cell.onclick = () => {
const dayIndex = parseInt(cell.getAttribute('data-day-index'));
const dayData = monthGridInstance.generate().days[dayIndex];
if (dayData && dayData.holidays.length > 0) {
showHolidayModal(dayData.holidays);
}
};
});
const allCheckbox = document.querySelector('input[name="holidayTag"][value="all"]');
const otherCheckboxes = document.querySelectorAll('input[name="holidayTag"]:not([value="all"])');
const updateFilter = () => {
const checkedValues = Array.from(otherCheckboxes).filter(i => i.checked).map(i => i.value);
if (checkedValues.length === 0) {
allCheckbox.checked = true;
holidayFilter = null;
} else {
allCheckbox.checked = false;
holidayFilter = checkedValues;
}
rerender();
};
if (allCheckbox) {
allCheckbox.onchange = () => {
if (allCheckbox.checked) {
otherCheckboxes.forEach(cb => cb.checked = false);
holidayFilter = null;
rerender();
} else {
allCheckbox.checked = true;
}
};
}
otherCheckboxes.forEach(checkbox => {
if (checkbox) checkbox.onchange = updateFilter;
});
}
function rerender() {
monthGridInstance = new MonthGrid({
year: currentYear,
month: currentMonth,
useGeez, weekdayLang, weekStart,
holidayFilter,
mode: activeMode,
showAllSaints: showAllSaints,
});
renderCalendar(monthGridInstance.generate());
}
rerender();
</script>
</body>
</html>