Kalendarz Miesięczny
Poznaj prosty, elegancki i niezwykle wygodny kalendarz, który działa w całości w Twojej przeglądarce — bez logowania, bez internetu, bez utraty danych. Aplikacja pozwala szybko planować dzień, przeglądać najbliższe wydarzenia, sprawdzać polskie święta i dni wolne, a także tworzyć własne notatki i zapisywać je lokalnie na urządzeniu.
To idealne narzędzie dla osób, które chcą mieć przejrzysty kalendarz zawsze pod ręką — lekki, bezpieczny i w pełni prywatny. Zacznij korzystać już teraz i zobacz, jak wygodne może być planowanie!
Opis działania kalendarza
Aplikacja wyświetla kalendarz miesięczny z możliwością przechodzenia między miesiącami oraz szybkiego powrotu do bieżącej daty. Najważniejsze elementy działania:
1. Kalendarz miesięczny
- Aktualny miesiąc wyświetlany jest z podziałem na tygodnie.
- Niedziele i święta oznaczone są czerwonym kolorem, aby były lepiej widoczne.
- Dzisiejszy dzień jest dodatkowo wyróżniony, dzięki czemu z łatwością go odnajdziesz.
- Możesz swobodnie przełączać miesiące i przeglądać rok wstecz lub naprzód.
2. Automatyczne zaznaczanie świąt
Kalendarz rozpoznaje wszystkie najważniejsze polskie święta, zarówno stałe, jak i ruchome (np. Wielkanoc, Boże Ciało). Po wskazaniu święta wyświetlany jest jego opis.
3. Dodawanie i edycja wydarzeń
- Kliknięcie dowolnego dnia otwiera panel z listą wydarzeń.
- Możesz szybko dodać nowe zadanie, notatkę lub przypomnienie.
- Każde wydarzenie można łatwo edytować lub usunąć.
- Wydarzenia zapisywane są automatycznie w localStorage, dzięki czemu pozostają na urządzeniu nawet po zamknięciu przeglądarki.
4. Import i eksport danych
- Eksport wszystkich wydarzeń do pliku
.jsonjednym kliknięciem. - Import pozwala przywrócić lub przenieść dane na inne urządzenie.
5. Prywatność
Wszystkie dane pozostają wyłącznie u Ciebie — nic nie jest wysyłane na zewnętrzne serwery.
Kalendarz
| Pn | Wt | Śr | Cz | Pt | So | Nd |
|---|
Szczegóły dnia
Wybierz dzień
Dane astronomiczne
Zdarzenia
Funkcje
- Podświetla niedziele i święta na czerwono.
- Dodawanie / edycja / usuwanie zdarzeń (LocalStorage).
- Eksport i import JSON z zachowaniem zdarzeń.
- Szczegółowa lista świąt (stałe i ruchome dla Polski).
Kod po stronie przeglądarki
<link type="text/css" href="http://www.dariuszrorat.ugu.pl/assets/css/bootstrap/wcag-outline.min.css" rel="stylesheet">
<style>
/* Drobne poprawki wyglądu */
.calendar-table td { height: 84px; vertical-align: top; padding: .35rem; cursor: pointer; }
.day-number { font-size: .95rem; }
.events-list { font-size: .85rem; max-height: 136px; overflow:auto; }
.table-active { outline: 2px solid rgba(13,110,253,0.12); }
[data-bs-theme=golden] .text-danger {color: #d13241 !important;}
[data-bs-theme=twilight] .text-danger {color: #ff4c65 !important;}
[data-bs-theme=dark] .text-danger {color: #ff4359 !important;}
</style>
<div id="app">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>
Kalendarz
</h2>
<div>
<div class="btn-group me-2" role="group">
<button id="prevMonth" class="btn btn-outline-secondary btn-sm"><</button>
<button id="todayBtn" class="btn btn-outline-secondary btn-sm">Dziś</button>
<button id="nextMonth" class="btn btn-outline-secondary btn-sm">></button>
</div>
<div class="btn-group ms-2" role="group">
<button id="exportBtn" class="btn btn-sm btn-outline-primary">Eksportuj</button>
<button id="importBtn" class="btn btn-sm btn-outline-secondary">Importuj</button>
<input id="importFile" type="file" accept="application/json" class="d-none" aria-label="Wybierz plik">
</div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-8">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<div id="monthTitle" class="fw-bold text-capitalize"></div>
<div id="yearTitle" class="small text-muted"></div>
</div>
<div>
<button id="addEventGlobal" class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#eventModal">Dodaj zdarzenie</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-bordered text-center calendar-table mb-0">
<thead>
<tr class="table-light">
<th>Pn</th><th>Wt</th><th>Śr</th><th>Cz</th><th>Pt</th><th>So</th><th>Nd</th>
</tr>
</thead>
<tbody id="calendarBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-body">
<h3 class="h5 card-title">Szczegóły dnia</h3>
<p id="selectedDateText" class="mb-1">Wybierz dzień</p>
<div id="holidaysList" class="mb-2 small"></div>
<h4 class="h6 mb-1">Dane astronomiczne</h4>
<div class="small text-muted">Warszawa (52.2297° N, 21.0122° E)</div>
<div id="suninfo" class="mb-3"></div>
<h4 class="h6 mb-1">Zdarzenia</h4>
<ul id="eventsList" class="list-group mb-2 events-list"></ul>
<div class="d-flex gap-2">
<button id="addEventBtn" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#eventModal">Dodaj</button>
<button id="clearStorageBtn" class="btn btn-outline-danger btn-sm">Wyczyść lokalne</button>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-body small">
<h4 class="h6">Funkcje</h4>
<ul>
<li>Podświetla niedziele i święta na czerwono.</li>
<li>Dodawanie / edycja / usuwanie zdarzeń (LocalStorage).</li>
<li>Eksport i import JSON z zachowaniem zdarzeń.</li>
<li>Szczegółowa lista świąt (stałe i ruchome dla Polski).</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Event modal -->
<div class="modal fade" id="eventModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form id="eventForm" class="modal-content">
<div class="modal-header">
<h3 class="h5 modal-title">Zdarzenie</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body">
<input type="hidden" id="eventId">
<div class="mb-3">
<label for="eventDate" class="form-label">Data</label>
<input type="date" id="eventDate" class="form-control" required>
</div>
<div class="mb-3">
<label for="eventTime" class="form-label">Godzina (opcjonalnie)</label>
<input type="time" id="eventTime" class="form-control">
</div>
<div class="mb-3">
<label for="eventTitle" class="form-label">Tytuł</label>
<input type="text" id="eventTitle" class="form-control" required>
</div>
<div class="mb-3">
<label for="eventDesc" class="form-label">Opis</label>
<textarea id="eventDesc" rows="3" class="form-control"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" id="deleteEventBtn" class="btn btn-outline-danger me-auto d-none">Usuń</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
<button type="submit" class="btn btn-primary">Zapisz</button>
</div>
</form>
</div>
</div>
</div>
<script>
// =====================
// Prosty kalendarz z obsługą świąt i LocalStorage
// =====================
// --- Stan aplikacji ---
const state = {
cursor: new Date(), // aktywny miesiąc
selectedDate: null, // YYYY-MM-DD
eventsKey: 'calendarEvents_v1'
};
// --- Utility ---
const fmtDate = (d) => d.toISOString().slice(0,10);
const pad = (n)=> n<10? '0'+n: ''+n;
// --- LocalStorage events ---
function loadEvents(){
try{
const raw = localStorage.getItem(state.eventsKey);
return raw ? JSON.parse(raw) : [];
}catch(e){ console.error(e); return []; }
}
function saveEvents(list){ localStorage.setItem(state.eventsKey, JSON.stringify(list)); }
function addEvent(evt){
const list = loadEvents();
list.push(evt);
saveEvents(list);
}
function updateEvent(updated){
const list = loadEvents().map(e=> e.id===updated.id? updated: e);
saveEvents(list);
}
function removeEvent(id){
const list = loadEvents().filter(e=> e.id!==id);
saveEvents(list);
}
function eventsForDate(dateStr){
return loadEvents().filter(e=> e.date===dateStr).sort((a,b)=> (a.time||'')> (b.time||'')?1:-1);
}
// --- Holiday calculations (Poland) ---
function getEasterDate(y){
// Gauss algorithm
const a = y % 19;
const b = Math.floor(y/100);
const c = y % 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) -1;
const day = ((h + l -7*m +114) % 31) +1;
return new Date(y, month, day);
}
function holidaysForYear(y){
const fixed = [
{m:0,d:1, name: 'Nowy Rok'}, // 01.01
{m:0,d:6, name: 'Trzech Króli'}, // 06.01
{m:4,d:1, name: 'Święto Pracy'}, // 01.05
{m:4,d:3, name: 'Święto Konstytucji 3 Maja'}, // 03.05
{m:7,d:15, name: 'Wniebowzięcie Najświętszej Maryi Panny'}, // 15.08
{m:10,d:1, name: 'Wszystkich Świętych'}, // 01.11 (poprawione)
{m:10,d:11, name: 'Święto Niepodległości'}, // 11.11 (poprawione)
{m:11,d:25, name: 'Boże Narodzenie (1)'}, // 25.12
{m:11,d:26, name: 'Boże Narodzenie (2)'} // 26.12
];
const easter = getEasterDate(y);
const easterMon = new Date(easter); easterMon.setDate(easter.getDate()+1);
const pentecost = new Date(easter); pentecost.setDate(easter.getDate()+49);
const corpus = new Date(easter); corpus.setDate(easter.getDate()+60);
const mov = [
{d: easter.getDate(), m: easter.getMonth(), name: 'Wielkanoc'},
{d: easterMon.getDate(), m: easterMon.getMonth(), name: 'Poniedziałek Wielkanocny'},
{d: pentecost.getDate(), m: pentecost.getMonth(), name: 'Zielone Świątki'},
{d: corpus.getDate(), m: corpus.getMonth(), name: 'Boże Ciało'}
];
const combined = fixed.concat(mov);
// map to date string keys
return combined.map(h=> ({ date: y+'-'+pad(h.m+1)+'-'+pad(h.d), name: h.name }));
}
// cache holidays per year
const holidaysCache = {};
function getHolidaysForYear(y){ if(!holidaysCache[y]) holidaysCache[y]= holidaysForYear(y); return holidaysCache[y]; }
// Dokładna funkcja Sunrise / Sunset (algorytm NOAA)
function noaaGetSunTimes(date, lat, lon) {
const rad = Math.PI / 180;
const deg = 180 / Math.PI;
// Helpers
function toJulian(d) {
return d.valueOf() / 86400000 + 2440587.5; // JS Date -> Julian day
}
function fromJulian(j) {
return new Date((j - 2440587.5) * 86400000);
}
function julianCentury(jd) {
return (jd - 2451545.0) / 36525.0;
}
function geomMeanLongSun(T) {
let L = 280.46646 + T * (36000.76983 + T * 0.0003032);
L = L % 360;
return (L < 0) ? L + 360 : L;
}
function geomMeanAnomalySun(T) {
return 357.52911 + T * (35999.05029 - 0.0001537 * T);
}
function eccentEarthOrbit(T) {
return 0.016708634 - T * (0.000042037 + 0.0000001267 * T);
}
function sunEqOfCenter(T, M) {
const Mrad = M * rad;
return Math.sin(Mrad) * (1.914602 - T * (0.004817 + 0.000014 * T)) +
Math.sin(2 * Mrad) * (0.019993 - 0.000101 * T) +
Math.sin(3 * Mrad) * 0.000289;
}
function sunTrueLong(L0, C) {
return L0 + C;
}
function sunApparentLong(T, trueLong) {
const omega = 125.04 - 1934.136 * T;
return trueLong - 0.00569 - 0.00478 * Math.sin(omega * rad);
}
function meanObliqEcliptic(T) {
return 23 + (26 + ((21.448 - T * (46.815 + T * (0.00059 - T * 0.001813)))) / 60) / 60;
}
function obliqCorr(T, eps0) {
const omega = 125.04 - 1934.136 * T;
return eps0 + 0.00256 * Math.cos(omega * rad);
}
function sunDeclination(eps, lambda) {
return Math.asin(Math.sin(eps * rad) * Math.sin(lambda * rad)) * deg;
}
function equationOfTime(T, L0, e, M, eps) {
// in minutes
const y = Math.tan((eps / 2) * rad);
const y2 = y * y;
const Mrad = M * rad;
const L0rad = L0 * rad;
const Etime = 4 * deg * (y2 * Math.sin(2 * L0rad) -
2 * e * Math.sin(Mrad) +
4 * e * y2 * Math.sin(Mrad) * Math.cos(2 * L0rad) -
0.5 * y2 * y2 * Math.sin(4 * L0rad) -
1.25 * e * e * Math.sin(2 * Mrad));
return Etime; // minutes
}
// ---- main calculation ----
const jd = toJulian(new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0)));
const T = julianCentury(jd);
const L0 = geomMeanLongSun(T); // degrees
const M = geomMeanAnomalySun(T); // degrees
const e = eccentEarthOrbit(T);
const C = sunEqOfCenter(T, M);
const trueLong = sunTrueLong(L0, C);
const apparentLong = sunApparentLong(T, trueLong);
const eps0 = meanObliqEcliptic(T);
const eps = obliqCorr(T, eps0);
const decl = sunDeclination(eps, apparentLong);
const eqTime = equationOfTime(T, L0, e, M, eps); // minutes
// solar noon (in Julian days)
// longitude to minutes: lon * 4 (deg -> minutes)
const timezoneOffsetMinutes = new Date().getTimezoneOffset(); // in minutes (local)
// solarNoon in minutes relative to UTC midnight = 720 - 4*lon - eqTime
const solarNoonMinutesUTC = 720 - (4 * lon) - eqTime;
// Hour angle for the sun altitude -0.83 deg for sunrise/sunset (refraction + sun radius)
const ha = Math.acos(
(Math.cos((90.833) * rad) / (Math.cos(lat * rad) * Math.cos(decl * rad)) - Math.tan(lat * rad) * Math.tan(decl * rad))
) * deg; // degrees
// sunrise and sunset in minutes UTC
const sunriseMinutesUTC = solarNoonMinutesUTC - (ha * 4);
const sunsetMinutesUTC = solarNoonMinutesUTC + (ha * 4);
// Build Date objects (convert minutes UTC of the day to Date in local timezone)
function minutesUTCToLocalDate(minutesUTC) {
// minutesUTC is minutes since 00:00 UTC of that date
const ms = Math.round(minutesUTC * 60000);
// base: UTC midnight of the given date
const utcMidnight = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0);
// UTC timestamp
const utcTimestamp = utcMidnight + ms;
return new Date(utcTimestamp); // JS Date will represent local time when formatted
}
const sunrise = minutesUTCToLocalDate(sunriseMinutesUTC);
const sunset = minutesUTCToLocalDate(sunsetMinutesUTC);
return {
sunrise,
sunset
};
}
// --- Rendering calendar ---
function buildCalendar(){
const now = state.cursor;
const year = now.getFullYear();
const month = now.getMonth();
const firstDay = new Date(year, month, 1).getDay() || 7; // Monday=1 .. Sunday=7
const daysInMonth = new Date(year, month+1, 0).getDate();
document.getElementById('monthTitle').textContent = now.toLocaleDateString('pl-PL',{ month:'long'});
document.getElementById('yearTitle').textContent = year;
const holidays = getHolidaysForYear(year).reduce((acc,h)=>{ acc[h.date]=h.name; return acc; }, {});
let html = '';
let day = 1;
for(let row=0; row<6; row++){
html += '<tr>';
for(let col=1; col<=7; col++){
if((row===0 && col<firstDay) || day>daysInMonth){ html += '<td></td>'; }
else{
const dateStr = `${year}-${pad(month+1)}-${pad(day)}`;
const isToday = dateStr === fmtDate(new Date());
const isSunday = col===7;
const isHoliday = !!holidays[dateStr];
let classes = [];
if(isToday) classes.push('border border-primary border-2 fw-bold');
if(isHoliday || isSunday) classes.push('text-danger fw-bold');
const evs = eventsForDate(dateStr);
html += `<td class="${classes.join(' ')}" data-date="${dateStr}">`;
html += `<div class="d-flex justify-content-between">`;
html += `<div class="day-number">${day}</div>`;
if(evs.length) html += `<span class="badge bg-primary rounded-pill">${evs.length}</span>`;
html += `</div>`;
if(isHoliday) html += `<div class="small">${holidays[dateStr]}</div>`;
// show up to 2 events
html += '<div class="events-list mt-1">';
evs.slice(0,2).forEach(e=>{
html += `<div class="small text-truncate">${escapeHtml(e.time? e.time + ' ' : '')}${escapeHtml(e.title)}</div>`;
});
html += '</div>';
html += '</td>';
day++;
}
}
html += '</tr>';
}
document.getElementById('calendarBody').innerHTML = html;
// attach click handlers
document.querySelectorAll('[data-date]').forEach(td=> td.addEventListener('click', ()=> selectDate(td.getAttribute('data-date'))));
}
function escapeHtml(s){ return (s||'').replace(/[&<>\\\"]/g, c=> ({'&':'&','<':'<','>':'>','\\':'\\\\','"':'"'}[c])); }
function parseYMD(ymd) {
const [y, m, d] = ymd.split('-').map(Number);
return new Date(y, m - 1, d, 12); // południe = brak przesunięć
}
function selectDate(dateStr){
state.selectedDate = dateStr;
document.getElementById('selectedDateText').textContent = new Date(dateStr).toLocaleDateString('pl-PL', { weekday:'long', year:'numeric', month:'long', day:'numeric' });
const d = parseYMD(dateStr);
const LAT = 52.2297;
const LNG = 21.0122;
const times = noaaGetSunTimes(d, LAT, LNG);
const diffMs = times.sunset - times.sunrise;
const hrs = Math.floor(diffMs / 3600000);
const mins = Math.floor((diffMs % 3600000) / 60000);
const html = `
<div class="small text-muted mt-2">
<div>Wschód słońca: <strong>${times.sunrise.toLocaleTimeString("pl-PL") ?? "n/a"}</strong></div>
<div>Zachód słońca: <strong>${times.sunset.toLocaleTimeString("pl-PL") ?? "n/a"}</strong></div>
<div>Długość dnia: <strong>${hrs}h ${mins}min</strong></div>
</div>
`;
const suninfo = document.getElementById('suninfo');
suninfo.innerHTML = html;
const holidays = getHolidaysForYear(new Date(dateStr).getFullYear()).filter(h=> h.date===dateStr);
const holEl = document.getElementById('holidaysList');
holEl.innerHTML = holidays.map(h=> `<div class="badge bg-danger me-1">${h.name}</div>`).join('') || '<span class="small-quiet">Brak świąt</span>';
renderEventsList();
// highlight selected cell
document.querySelectorAll('[data-date]').forEach(td=> td.classList.remove('table-active'));
const sel = document.querySelector(`[data-date="${dateStr}"]`);
if(sel) sel.classList.add('table-active');
}
function renderEventsList(){
const ul = document.getElementById('eventsList'); ul.innerHTML='';
if(!state.selectedDate) return;
const evs = eventsForDate(state.selectedDate);
if(!evs.length){ ul.innerHTML = '<li class="list-group-item small-quiet">Brak zdarzeń</li>'; return; }
evs.forEach(e=>{
const li = document.createElement('li'); li.className='list-group-item d-flex justify-content-between align-items-start';
const div = document.createElement('div');
div.innerHTML = `<div class="fw-bold">${escapeHtml(e.title)}</div><div class="small">${escapeHtml(e.time||'')} ${escapeHtml(e.description||'')}</div>`;
const btns = document.createElement('div');
btns.innerHTML = `<button class="btn btn-sm btn-outline-secondary me-1 edit">Edytuj</button><button class="btn btn-sm btn-outline-danger delete">Usuń</button>`;
li.appendChild(div); li.appendChild(btns);
ul.appendChild(li);
btns.querySelector('.edit').addEventListener('click', ()=> openEditModal(e));
btns.querySelector('.delete').addEventListener('click', ()=>{ if(confirm('Usuń zdarzenie?')){ removeEvent(e.id); buildCalendar(); renderEventsList(); }});
});
}
// --- Modal handling (add/edit) ---
const eventModalEl = document.getElementById('eventModal');
let eventModal = null;
document.addEventListener('DOMContentLoaded', function() {
eventModal = new bootstrap.Modal(eventModalEl);
}) ;
const eventForm = document.getElementById('eventForm');
document.getElementById('addEventBtn').addEventListener('click', ()=> openAddModal());
document.getElementById('addEventGlobal').addEventListener('click', ()=> openAddModal());
function openAddModal(){
document.getElementById('eventId').value = '';
document.getElementById('eventDate').value = state.selectedDate || fmtDate(new Date());
document.getElementById('eventTime').value = '';
document.getElementById('eventTitle').value = '';
document.getElementById('eventDesc').value = '';
document.getElementById('deleteEventBtn').classList.add('d-none');
eventModal.show();
}
function openEditModal(e){
document.getElementById('eventId').value = e.id;
document.getElementById('eventDate').value = e.date;
document.getElementById('eventTime').value = e.time || '';
document.getElementById('eventTitle').value = e.title;
document.getElementById('eventDesc').value = e.description || '';
document.getElementById('deleteEventBtn').classList.remove('d-none');
eventModal.show();
}
document.getElementById('deleteEventBtn').addEventListener('click', ()=>{
const id = document.getElementById('eventId').value;
if(!id) return;
if(confirm('Na pewno usunąć zdarzenie?')){ removeEvent(id); eventModal.hide(); buildCalendar(); renderEventsList(); }
});
eventForm.addEventListener('submit', (ev)=>{
ev.preventDefault();
const id = document.getElementById('eventId').value || ('e_'+Date.now());
const data = {
id,
date: document.getElementById('eventDate').value,
time: document.getElementById('eventTime').value,
title: document.getElementById('eventTitle').value.trim(),
description: document.getElementById('eventDesc').value.trim()
};
if(document.getElementById('eventId').value) updateEvent(data);
else addEvent(data);
eventModal.hide();
buildCalendar();
if(state.selectedDate === data.date) renderEventsList();
});
// --- Navigation ---
document.getElementById('prevMonth').addEventListener('click', ()=>{ state.cursor.setMonth(state.cursor.getMonth()-1); buildCalendar(); });
document.getElementById('nextMonth').addEventListener('click', ()=>{ state.cursor.setMonth(state.cursor.getMonth()+1); buildCalendar(); });
document.getElementById('todayBtn').addEventListener('click', ()=>{ state.cursor = new Date(); state.selectedDate = fmtDate(state.cursor); selectDate(state.selectedDate); buildCalendar(); });
// --- Export / Import ---
document.getElementById('exportBtn').addEventListener('click', ()=>{
const data = { exportedAt: new Date().toISOString(), events: loadEvents() };
const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = 'calendar-events.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
});
document.getElementById('importBtn').addEventListener('click', ()=> document.getElementById('importFile').click());
document.getElementById('importFile').addEventListener('change', (e)=>{
const f = e.target.files[0]; if(!f) return; const reader = new FileReader();
reader.onload = function(){
try{
const parsed = JSON.parse(reader.result);
if(!parsed.events) throw new Error('Brak pola events w pliku');
// merge uniquely by id
const existing = loadEvents();
const map = new Map(existing.map(x=> [x.id,x]));
parsed.events.forEach(x=> map.set(x.id,x));
saveEvents(Array.from(map.values()));
alert('Import zakończony');
buildCalendar(); if(state.selectedDate) renderEventsList();
}catch(err){ alert('Błąd importu: '+ err.message); }
};
reader.readAsText(f);
e.target.value = '';
});
document.getElementById('clearStorageBtn').addEventListener('click', ()=>{
if(confirm('Usunąć wszystkie zdarzenia z LocalStorage?')){ localStorage.removeItem(state.eventsKey); buildCalendar(); renderEventsList(); }
});
// --- Init ---
(function init(){
// select today by default
state.selectedDate = fmtDate(new Date());
selectDate(state.selectedDate);
buildCalendar();
})();
</script>
Kod po stronie serwera
Brak kodu serwera
Ta aplikacja działa wyłącznie w przeglądarce i nie korzysta z kodu po stronie serwera.