Krok 11: notatki przy dniach
Kalendarz ze świętami, notatkami i językami - Kalendarz w JavaScript krok po kroku (HTML/CSS/JS)
Stan dostaje zaznaczony dzień (`selected`, klucz daty) i notatki (`notes`: obiekt klucz daty -> tekst). Kliknięcie dnia i przycisk Zapisz zmieniają tylko stan; `render` zaznacza dzień, podkreśla dni z notatką i dokleja notatkę do podpowiedzi obok nazwy święta. Notatka zostaje po przejściu do innego miesiąca i z powrotem. Nowy obiekt notatek (`{ ...state.notes }`) zamiast zmiany starego to zwyczaj, który ułatwia późniejsze porównywanie stanów.
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="utf-8">
<title>Kalendarz</title>
<style>
body { font-family: sans-serif; max-width: 36em; line-height: 1.4; }
input, select, button, textarea { font: inherit; padding: 0.2em 0.4em; }
table { border-collapse: collapse; }
th, td { border: 1px solid #94a3b8; padding: 0.2em 0.6em; }
.grid { display: grid; grid-template-columns: repeat(7, 40px); gap: 6px; }
.grid div { height: 40px; line-height: 40px; text-align: center; background: lightgrey; }
.grid .day-name { background: darkgrey; color: white; }
.grid .weekend { background: lightblue; color: white; }
.grid .none { background: white; }
.grid .holiday { background: #fecaca; color: #b91c1c; font-weight: bold; }
.grid .today { background: crimson; color: white; }
.grid .day { cursor: pointer; }
.grid .selected { outline: 3px solid #f59e0b; }
.grid .has-note { text-decoration: underline; }
.bar { display: flex; align-items: center; gap: 0.5em; width: 316px; }
.bar strong { flex: 1; text-align: center; }
</style>
</head>
<body>
<p class="bar">
<button id="prev"><</button>
<strong id="title"></strong>
<button id="next">></button>
</p>
<div id="calendar" class="grid"></div>
<p><span id="chosen">Kliknij dzień</span> <input id="note"> <button id="save">Zapisz</button></p>
<ul id="holidays"></ul>
<script>
const TEXTS = {
pl: {
months: ['styczeń', 'luty', 'marzec', 'kwiecień', 'maj', 'czerwiec',
'lipiec', 'sierpień', 'wrzesień', 'październik', 'listopad', 'grudzień'],
days: ['Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So', 'Nd'],
holidays: {
newYear: 'Nowy Rok', epiphany: 'Trzech Króli', labour: 'Święto Pracy', constitution: 'Święto Konstytucji 3 Maja',
assumption: 'Wniebowzięcie NMP', allSaints: 'Wszystkich Świętych', independence: 'Święto Niepodległości',
christmasEve: 'Wigilia', christmas: 'Boże Narodzenie', boxing: 'Drugi dzień Bożego Narodzenia',
easter: 'Wielkanoc', easterMonday: 'Poniedziałek Wielkanocny', pentecost: 'Zielone Świątki', corpusChristi: 'Boże Ciało',
},
},
};
// algorytm Meeusa (Jonesa-Butchera): Wielkanoc dla dowolnego roku kalendarza gregoriańskiego
function easter(year) {
const a = year % 19;
const b = Math.floor(year / 100);
const c = year % 100;
const d = Math.floor(b / 4);
const e = b % 4;
const f = Math.floor((b + 8) / 25);
const g = Math.floor((b - f + 1) / 3);
const h = (19 * a + b - d - g + 15) % 30;
const i = Math.floor(c / 4);
const k = c % 4;
const l = (32 + 2 * e + 2 * i - h - k) % 7;
const m = Math.floor((a + 11 * h + 22 * l) / 451);
const month = Math.floor((h + l - 7 * m + 114) / 31);
const day = ((h + l - 7 * m + 114) % 31) + 1;
return new Date(year, month - 1, day);
}
// Date sam przenosi nadmiar dni do kolejnego miesiąca
const addDays = (date, n) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + n);
const FIXED_HOLIDAYS = [
{ month: 1, day: 1, id: 'newYear' }, { month: 1, day: 6, id: 'epiphany' },
{ month: 5, day: 1, id: 'labour' }, { month: 5, day: 3, id: 'constitution' },
{ month: 8, day: 15, id: 'assumption' }, { month: 11, day: 1, id: 'allSaints' },
{ month: 11, day: 11, id: 'independence' }, { month: 12, day: 24, id: 'christmasEve', since: 2025 },
{ month: 12, day: 25, id: 'christmas' }, { month: 12, day: 26, id: 'boxing' },
];
const MOVABLE_HOLIDAYS = [
{ offset: 0, id: 'easter' }, { offset: 1, id: 'easterMonday' },
{ offset: 49, id: 'pentecost' }, { offset: 60, id: 'corpusChristi' },
];
const pad = n => String(n).padStart(2, '0');
const key = date => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
function holidaysOf(year) {
const fixed = FIXED_HOLIDAYS.filter(h => !h.since || year >= h.since)
.map(h => ({ date: new Date(year, h.month - 1, h.day), id: h.id }));
const sunday = easter(year);
const movable = MOVABLE_HOLIDAYS.map(h => ({ date: addDays(sunday, h.offset), id: h.id }));
return [...fixed, ...movable].sort((x, y) => x.date - y.date);
}
// model: tygodnie miesiąca, dzień = { day, key, weekend, today, holiday }
function monthWeeks(year, month, today) {
const holidays = new Map(holidaysOf(year).map(h => [key(h.date), h.id]));
const firstDay = (new Date(year, month - 1, 1).getDay() + 6) % 7;
const days = new Date(year, month, 0).getDate();
const cells = Array(firstDay).fill(null);
for (let d = 1; d <= days; d++) {
const date = new Date(year, month - 1, d);
cells.push({
day: d,
key: key(date),
weekend: (firstDay + d - 1) % 7 >= 5,
today: key(date) === key(today),
holiday: holidays.get(key(date)) ?? null,
});
}
while (cells.length % 7 !== 0) cells.push(null);
const weeks = [];
for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
return weeks;
}
function cell(text, ...classes) {
const div = document.createElement('div');
div.textContent = text;
div.classList.add(...classes.filter(Boolean));
return div;
}
function shift(year, month, delta) {
const index = year * 12 + (month - 1) + delta;
return { year: Math.floor(index / 12), month: (index % 12) + 1 };
}
function renderGrid(root, weeks, t, selected, notes) {
const fragment = document.createDocumentFragment();
t.days.forEach((name, k) => fragment.append(cell(name, 'day-name', k >= 5 ? 'weekend' : '')));
for (const c of weeks.flat()) {
if (c === null) {
fragment.append(cell('', 'none'));
continue;
}
const div = cell(c.day, 'day', c.today ? 'today' : c.weekend ? 'weekend' : '', c.holiday ? 'holiday' : '',
c.key === selected ? 'selected' : '', notes[c.key] ? 'has-note' : '');
div.dataset.day = c.day;
div.dataset.key = c.key;
div.title = [c.holiday && t.holidays[c.holiday], notes[c.key]].filter(Boolean).join(' - ');
fragment.append(div);
}
root.replaceChildren(fragment);
}
function renderHolidays(list, year, month, t) {
list.replaceChildren(...holidaysOf(year)
.filter(h => h.date.getMonth() + 1 === month)
.map(h => {
const li = document.createElement('li');
li.textContent = `${pad(h.date.getDate())}.${pad(month)} ${t.holidays[h.id]}`;
return li;
}));
}
const today = new Date();
const state = { year: today.getFullYear(), month: today.getMonth() + 1, selected: null, notes: {} };
function setState(changes) {
Object.assign(state, changes);
render();
}
function render() {
const t = TEXTS.pl;
document.getElementById('title').textContent = `${t.months[state.month - 1]} ${state.year}`;
renderGrid(document.getElementById('calendar'), monthWeeks(state.year, state.month, today), t, state.selected, state.notes);
renderHolidays(document.getElementById('holidays'), state.year, state.month, t);
document.getElementById('chosen').textContent = state.selected ? `Dzień ${state.selected}:` : 'Kliknij dzień';
document.getElementById('note').value = state.notes[state.selected] ?? '';
}
document.getElementById('prev').addEventListener('click', () => setState(shift(state.year, state.month, -1)));
document.getElementById('next').addEventListener('click', () => setState(shift(state.year, state.month, 1)));
document.getElementById('calendar').addEventListener('click', e => {
const div = e.target.closest('.day');
if (div) setState({ selected: div.dataset.key });
});
document.getElementById('save').addEventListener('click', () => {
if (!state.selected) return;
const notes = { ...state.notes };
const text = document.getElementById('note').value.trim();
if (text) notes[state.selected] = text;
else delete notes[state.selected];
setState({ notes });
});
render();
</script>
</body>
</html>Ładowanie interaktywnego edytora... Jeśli się nie pojawi, włącz JavaScript.
Wszystkie lekcje kursu Kalendarz w JavaScript krok po kroku·Zaloguj się, aby zapisać postęp na koncie.