Temat 28 · 2 × 45 min
Czas, daty i animacja
Do tej pory twój kod wykonywał się „natychmiast”: kliknięcie – reakcja. Prawdziwe interfejsy żyją w czasie: komunikat znika po trzech sekundach, zegar tyka, slider sam przewija zdjęcia, pasek postępu płynnie rośnie. Do tego dochodzą daty – „ile dni do egzaminu?”, „który to dzień tygodnia?” – z obiektem Date, który ma kilka historycznych dziwactw. Ta lekcja uczy trzech narzędzi: timerów, dat i requestAnimationFrame, a przy okazji otwiera drzwi do asynchroniczności z lekcji 29.
setTimeout – zrób to później
setTimeout(funkcja, ms, ...argumenty) planuje jednorazowe wywołanie funkcji po co najmniej ms milisekundach i natychmiast zwraca identyfikator timera. Reszta skryptu nie czeka.
console.log('1. start');
const id = setTimeout(() => {
console.log('3. minęło ok. 500 ms');
}, 500);
// dodatkowe argumenty trafiają do funkcji
setTimeout((kto, ile) => console.log(`4. ${kto} czekał ${ile} ms`), 800, 'Bartek', 800);
console.log('2. koniec skryptu – a timer dopiero się odliczy'); // wypisze się PRZED „3.”
// `id` to w przeglądarce liczba – przyda się do clearTimeout(id)Kolejność w konsoli: 1, 2, potem 3 i 4. setTimeout nie zatrzymuje programu – zleca przeglądarce „przypomnij mi za chwilę” i idzie dalej. Nawet setTimeout(f, 0) wykona się po całym bieżącym skrypcie, bo funkcja trafia do kolejki zadań i czeka, aż stos będzie pusty (szczegóły w lekcji 29).
Co wypisze ten kod?
console.log('A');
setTimeout(() => console.log('B'), 0);
console.log('C');Zaplanowane wywołanie odwołujesz przez clearTimeout(id) – klasyczny przykład to komunikat, który znika po 3 s, chyba że użytkownik zamknie go wcześniej:
let przypomnienie = setTimeout(() => console.log('To się NIE wypisze'), 300);
clearTimeout(przypomnienie); // odwołane, zanim zdążyło się uruchomić
// wzorzec „zresetuj odliczanie”: każde wywołanie kasuje poprzednie
let timerUkrycia = null;
function pokazKomunikat(tekst) {
console.log('pokazuję:', tekst);
clearTimeout(timerUkrycia); // clearTimeout(null) jest bezpieczne – nic nie robi
timerUkrycia = setTimeout(() => console.log('ukrywam komunikat'), 400);
}
pokazKomunikat('Zapisano');
setTimeout(() => pokazKomunikat('Zapisano ponownie'), 200); // resetuje odliczanie – „ukrywam” pojawi się tylko raz, po ~600 mssetInterval – powtarzaj co…
setInterval(funkcja, ms) wywołuje funkcję cyklicznie, co ms milisekund, aż do clearInterval(id). Bez zatrzymania działa, dopóki strona jest otwarta.
let sekunda = 3;
const id = setInterval(() => {
console.log(`Start za ${sekunda}…`);
sekunda--;
if (sekunda === 0) {
clearInterval(id); // ZAWSZE musi być warunek stopu
console.log('Start!');
}
}, 300);W przeglądarce zobaczysz trzy odliczenia i „Start!”. Interwał to najczęstsze źródło „wycieków” w aplikacjach: jeśli element znika z ekranu, a interwał dalej go aktualizuje, kod działa w pustkę i zużywa procesor. Zasady bezpieczeństwa:
- Jeden identyfikator, jedno miejsce startu. Przed
setIntervalzawszeclearIntervalpoprzedniego – inaczej dwa kliknięcia „Start” dadzą dwa interwały i zegar będzie „tykał” dwa razy na sekundę. - Zatrzymuj przy końcu: osiągnięty cel, zamknięcie okna, usunięcie elementu.
- Przy długich obliczeniach rozważ rekurencyjny
setTimeout– kolejny krok planujesz dopiero po zakończeniu bieżącego, więc odstępy się nie nakładają, a opóźnienie możesz zmieniać w locie:
let krok = 1;
function tik() {
if (krok === 4) {
console.log('tik 4 – koniec');
return;
}
const odstep = (krok + 1) * 100; // odstęp rośnie: 200, 300, 400 ms
console.log(`tik ${krok}, następny za ${odstep} ms`);
krok++;
setTimeout(tik, odstep); // kolejny krok planujemy dopiero teraz
}
tik();Zegar cyfrowy
<p id="zegar" class="zegar">--:--:--</p>
<button id="start" type="button">Start</button>
<button id="stop" type="button">Stop</button>.zegar { font-family: monospace; font-size: 40px; margin: 0 0 8px; }const zegar = document.getElementById('zegar');
let idZegara = null; // null = zegar nie chodzi
function pokazCzas() {
// toLocaleTimeString daje gotowy format HH:MM:SS zgodny z ustawieniami 'pl-PL'
zegar.textContent = new Date().toLocaleTimeString('pl-PL');
}
document.getElementById('start').addEventListener('click', () => {
if (idZegara !== null) return; // już chodzi – drugi klik nic nie robi
pokazCzas(); // od razu, nie dopiero po sekundzie
idZegara = setInterval(pokazCzas, 1000);
});
document.getElementById('stop').addEventListener('click', () => {
clearInterval(idZegara);
idZegara = null;
});
document.getElementById('start').click(); // uruchom na starcieZwróć uwagę na dwa drobiazgi, które odróżniają działający zegar od irytującego: pierwsze wyświetlenie natychmiast (nie po sekundzie czekania) i strażnik if (idZegara !== null) return.
Obiekt Date
Date reprezentuje jeden punkt w czasie: liczbę milisekund od 1 stycznia 1970 (UTC), zwaną znacznikiem czasu (timestamp). Z tego punktu umie wyliczyć rok, miesiąc, dzień tygodnia w lokalnej strefie czasowej użytkownika.
Tworzenie
const teraz = new Date(); // bieżąca chwila
console.log(typeof teraz, teraz instanceof Date); // object true
// rok, miesiąc (OD ZERA!), dzień, godz, min, sek – lokalnie
const egzamin = new Date(2027, 0, 12, 9, 0); // 12 stycznia 2027, 9:00
console.log(egzamin.getDate(), egzamin.getMonth(), egzamin.getFullYear()); // 12 0 2027
// napis ISO: samo "RRRR-MM-DD" jest traktowane jako UTC, z godziną – jako czas lokalny
const zIso = new Date('2026-10-20T08:30:00');
console.log(zIso.getHours()); // 8
// liczba ms od 1970
console.log(new Date(0).toISOString()); // 1970-01-01T00:00:00.000Z
console.log(typeof Date.now()); // number – bieżący znacznik czasu, bez tworzenia obiektu
// bez `new` Date() zwraca NAPIS z bieżącą datą – dialekt egzaminacyjny: document.write(Date())
console.log(typeof Date()); // stringCo wypisze ten kod?
const d = new Date(2026, 2, 15);
console.log(d.getMonth() + '/' + d.getDate());Odczyt i zmiana składników
| Odczyt | Zakres | Zmiana | Uwagi |
|---|---|---|---|
getFullYear() | 4 cyfry | setFullYear(r) | nie używaj getYear() (przestarzałe) |
getMonth() | 0–11 | setMonth(m) | 0 = styczeń |
getDate() | 1–31 | setDate(d) | dzień miesiąca; setDate(0) = ostatni dzień poprzedniego miesiąca |
getDay() | 0–6 | – | 0 = niedziela, 1 = poniedziałek… nie ma setDay |
getHours(), getMinutes(), getSeconds(), getMilliseconds() | 0–23, 0–59, 0–59, 0–999 | setHours(…) itd. | czas lokalny |
getTime(), Date.now(), +data | ms od 1970 | setTime(ms) | do porównań i różnic |
getTimezoneOffset() | minuty | – | Polska: −60 (zima) lub −120 (lato) |
getUTC…() | jak wyżej, ale w UTC | setUTC…() | do komunikacji z serwerem |
Settery „autokorygują” datę – to cecha, nie błąd, i daje wygodne sztuczki:
const d = new Date(2026, 0, 31); // 31 stycznia
d.setDate(d.getDate() + 1); // +1 dzień
console.log(d.getMonth(), d.getDate()); // 1 1 → 1 lutego – Date samo przeszło do następnego miesiąca
const ostatni = new Date(2026, 2, 0); // „dzień 0 marca” = ostatni dzień lutego
console.log(ostatni.getDate()); // 28 – ile dni ma luty 2026
const zaTydzien = new Date();
zaTydzien.setDate(zaTydzien.getDate() + 7); // +7 dni, przeskok miesiąca/roku załatwia Date
console.log(zaTydzien > new Date()); // true – porównania działają przez wartość liczbowąRóżnice między datami
Odejmowanie dwu dat daje różnicę w milisekundach (- zamienia obiekty na liczby). Dalej to arytmetyka:
const MS_DNIA = 24 * 60 * 60 * 1000;
function dniDo(dataDocelowa, od = new Date()) {
// zerujemy godziny, żeby liczyć pełne dni kalendarzowe, nie 24-godzinne odcinki
const a = new Date(od.getFullYear(), od.getMonth(), od.getDate());
const b = new Date(dataDocelowa.getFullYear(), dataDocelowa.getMonth(), dataDocelowa.getDate());
return Math.round((b - a) / MS_DNIA);
}
const dzis = new Date(2026, 9, 20); // 20 października 2026 (stała data, żeby wynik był przewidywalny)
console.log(dniDo(new Date(2027, 0, 12), dzis)); // 84 – do sesji styczniowej
console.log(dniDo(new Date(2026, 11, 24), dzis)); // 65 – do Wigilii
function wiek(urodziny, dzis = new Date()) {
let lata = dzis.getFullYear() - urodziny.getFullYear();
const przedUrodzinami =
dzis.getMonth() < urodziny.getMonth() || (dzis.getMonth() === urodziny.getMonth() && dzis.getDate() < urodziny.getDate());
if (przedUrodzinami) lata--; // w tym roku jeszcze nie było urodzin
return lata;
}
console.log(wiek(new Date(2008, 10, 5), dzis)); // 17 – urodziny dopiero w listopadzie
console.log(wiek(new Date(2008, 5, 5), dzis)); // 18Dlaczego Math.round, a nie dzielenie całkowite? Przy zmianie czasu letni/zimowy jeden dzień ma 23 lub 25 godzin; (b - a) / MS_DNIA daje wtedy 83,96 albo 84,04 i zaokrąglenie ratuje wynik. Przy liczeniu wieku nie dziel w ogóle – porównuj składniki.
Formatowanie: toLocale… i Intl
Ręczne sklejanie getDate() + '.' + (getMonth() + 1) jest podatne na błędy (brak zera wiodącego, miesiąc od zera). Przeglądarka ma wbudowany formater zgodny z polskimi zwyczajami:
const d = new Date(2026, 9, 20, 8, 5); // 20 października 2026, 8:05
console.log(d.toLocaleDateString('pl-PL')); // 20.10.2026
console.log(d.toLocaleTimeString('pl-PL')); // 08:05:00
console.log(d.toLocaleString('pl-PL')); // 20.10.2026, 08:05:00
console.log(d.toLocaleDateString('pl-PL', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }));
// wtorek, 20 października 2026
console.log(d.toLocaleDateString('pl-PL', { month: 'short', day: '2-digit' })); // 20 paź
console.log(d.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' })); // 08:05
// ten sam format wielokrotnie – utwórz formater raz
const format = new Intl.DateTimeFormat('pl-PL', { dateStyle: 'full', timeStyle: 'short' });
console.log(format.format(d)); // wtorek, 20 października 2026 08:05
// „3 dni temu”, „za 2 godziny”
const wzgledny = new Intl.RelativeTimeFormat('pl', { numeric: 'auto' });
console.log(wzgledny.format(-3, 'day')); // 3 dni temu
console.log(wzgledny.format(1, 'day')); // jutro
console.log(wzgledny.format(2, 'hour')); // za 2 godziny
// gdy mimo wszystko składasz ręcznie – padStart dla zer wiodących
const hh = String(d.getHours()).padStart(2, '0');
const mm = String(d.getMinutes()).padStart(2, '0');
console.log(`${hh}:${mm}`); // 08:05Odliczanie do egzaminu
<label>Data docelowa: <input type="datetime-local" id="cel"></label>
<p id="odliczanie" class="duze"></p>.duze { font-family: monospace; font-size: 26px; }const pole = document.getElementById('cel');
const wyjscie = document.getElementById('odliczanie');
// domyślnie: najbliższy 12 stycznia, 9:00 (sesja zimowa)
const teraz = new Date();
let cel = new Date(teraz.getFullYear(), 0, 12, 9);
if (cel < teraz) cel.setFullYear(cel.getFullYear() + 1);
// input datetime-local chce "RRRR-MM-DDTHH:MM" w czasie lokalnym
const pad = (n) => String(n).padStart(2, '0');
pole.value = `${cel.getFullYear()}-${pad(cel.getMonth() + 1)}-${pad(cel.getDate())}T${pad(cel.getHours())}:${pad(cel.getMinutes())}`;
let id = null;
pole.addEventListener('change', () => {
if (!pole.value) return; // wyczyszczone pole – zostaw poprzedni cel
cel = new Date(pole.value); // napis z godziną → czas lokalny
clearInterval(id); // interwał mógł już stanąć na „Już!” – startujemy od nowa
odlicz();
id = setInterval(odlicz, 1000);
});
function odlicz() {
const roznica = cel - Date.now();
if (roznica <= 0) {
wyjscie.textContent = 'Już!';
clearInterval(id);
return;
}
const s = Math.floor(roznica / 1000);
const dni = Math.floor(s / 86400);
const godz = Math.floor((s % 86400) / 3600);
const min = Math.floor((s % 3600) / 60);
const sek = s % 60;
wyjscie.textContent = `${dni} dni ${pad(godz)}:${pad(min)}:${pad(sek)}`;
}
odlicz();
id = setInterval(odlicz, 1000);Slider z autoodtwarzaniem i pauzą
Łączymy wszystko: stan (indeks z zawijaniem, lekcja 17), interwał ze strażnikiem i zdarzenia mouseenter/mouseleave (lekcja 27).
<div id="slider" class="slider">
<button class="strzalka" data-krok="-1" type="button" aria-label="poprzedni">‹</button>
<div id="slajd" class="slajd"></div>
<button class="strzalka" data-krok="1" type="button" aria-label="następny">›</button>
</div>
<div id="kropki" class="kropki"></div>
<p id="stan">odtwarzanie</p>.slider { display: flex; align-items: center; gap: 8px; }
.slajd { flex: 1; height: 120px; display: grid; place-items: center; color: #fff; font-size: 22px; font-weight: 700; border-radius: 12px; transition: background 0.4s; }
.strzalka { font-size: 24px; width: 40px; height: 40px; border-radius: 50%; border: 1px solid #15171c; background: #fff; cursor: pointer; }
.kropki { display: flex; gap: 6px; justify-content: center; margin-top: 8px; }
.kropki button { width: 12px; height: 12px; border-radius: 50%; border: 1px solid #15171c; background: #fff; cursor: pointer; padding: 0; }
.kropki button.aktywna { background: #15171c; }
#stan { text-align: center; font-size: 13px; color: #555; }const slajdy = [
{ tekst: 'Zakopane', kolor: '#2563eb' },
{ tekst: 'Gdańsk', kolor: '#0891b2' },
{ tekst: 'Kraków', kolor: '#dc2626' },
{ tekst: 'Wrocław', kolor: '#16a34a' },
];
const CO_ILE = 2000;
const slider = document.getElementById('slider');
const slajd = document.getElementById('slajd');
const kropki = document.getElementById('kropki');
const stan = document.getElementById('stan');
let indeks = 0;
let idAuto = null;
// kropki generowane z danych – jedna na slajd
slajdy.forEach((_, i) => {
const b = document.createElement('button');
b.type = 'button';
b.dataset.indeks = i;
b.setAttribute('aria-label', `slajd ${i + 1}`);
kropki.append(b);
});
function pokaz(nowy) {
indeks = (nowy + slajdy.length) % slajdy.length;
slajd.textContent = slajdy[indeks].tekst;
slajd.style.background = slajdy[indeks].kolor;
kropki.querySelectorAll('button').forEach((b, i) => b.classList.toggle('aktywna', i === indeks));
}
function start() {
if (idAuto !== null) return; // strażnik: nigdy dwa interwały
idAuto = setInterval(() => pokaz(indeks + 1), CO_ILE);
stan.textContent = 'odtwarzanie';
}
function stop() {
clearInterval(idAuto);
idAuto = null;
stan.textContent = 'pauza (kursor nad sliderem)';
}
// strzałki – delegacja; po ręcznej zmianie restartujemy odliczanie, żeby slajd nie przeskoczył od razu
slider.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-krok]');
if (!btn) return;
pokaz(indeks + Number(btn.dataset.krok));
if (idAuto !== null) {
stop();
start();
}
});
kropki.addEventListener('click', (e) => {
const b = e.target.closest('button[data-indeks]');
if (b) pokaz(Number(b.dataset.indeks));
});
// mouseenter/mouseleave nie bąbelkują i nie „migają” przy przejściu na przyciski w środku
slider.addEventListener('mouseenter', stop);
slider.addEventListener('mouseleave', start);
// gdy karta przeglądarki jest w tle, nie ma sensu przewijać
document.addEventListener('visibilitychange', () => (document.hidden ? stop() : start()));
pokaz(0);
start();Dwie decyzje projektowe warte zapamiętania: restart odliczania po ręcznym kliknięciu (inaczej slajd zmienia się pół sekundy po tym, jak użytkownik wybrał inny) i pauza, gdy karta jest niewidoczna (visibilitychange). Przeglądarki i tak spowalniają timery w kartach w tle, ale jawne zatrzymanie jest czystsze.
requestAnimationFrame – płynna animacja
setInterval(rysuj, 16) jako animacja to zły pomysł: ekran odświeża się 60 (lub 120) razy na sekundę, a interwał nie jest z tym zsynchronizowany – klatki się gubią lub dublują, animacja „szarpie”. requestAnimationFrame(callback) prosi przeglądarkę: wywołaj moją funkcję tuż przed następnym odmalowaniem ekranu. Callback dostaje znacznik czasu w ms (performance.now()), a żeby animować dalej, w jego środku prosisz o kolejną klatkę.
let idKlatki;
function klatka(czas) {
// czas – ms od załadowania strony, z dokładnością do ułamków
rysuj(czas);
idKlatki = requestAnimationFrame(klatka); // kolejna klatka
}
idKlatki = requestAnimationFrame(klatka);
// zatrzymanie
cancelAnimationFrame(idKlatki);Najważniejsza zasada: animuj na podstawie upływu czasu, nie liczby klatek. Jeśli w każdej klatce przesuwasz o 2 px, na ekranie 120 Hz animacja jest dwa razy szybsza niż na 60 Hz. Zamiast tego oblicz, jaka część animacji powinna już minąć: postep = (teraz - start) / czasTrwania i ustaw położenie z tego ułamka.
<p>setInterval(…, 16) – przesuwa o stałą liczbę pikseli na klatkę</p>
<div class="tor"><div id="kulkaA" class="kulka a"></div></div>
<p>requestAnimationFrame – położenie z upływu czasu (2 s, z wygładzeniem)</p>
<div class="tor"><div id="kulkaB" class="kulka b"></div></div>
<button id="start" type="button">Start</button> <span id="info"></span>.tor { position: relative; height: 32px; background: #eee; border-radius: 16px; margin: 4px 0 12px; }
.kulka { position: absolute; top: 2px; left: 0; width: 28px; height: 28px; border-radius: 50%; }
.a { background: #dc2626; } .b { background: #2563eb; }
p { margin: 0; font-size: 13px; color: #555; }const kulkaA = document.getElementById('kulkaA');
const kulkaB = document.getElementById('kulkaB');
const info = document.getElementById('info');
const CZAS = 2000; // ms
let idInterval = null;
let idRaf = null;
// wygładzenie (easing): start powoli, koniec powoli
const easeInOut = (t) => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2);
function start() {
clearInterval(idInterval);
cancelAnimationFrame(idRaf);
const dystans = kulkaA.parentElement.clientWidth - kulkaA.offsetWidth;
// A: naiwnie – stały krok na klatkę; prędkość zależy od tego, jak często odpala się interwał
let x = 0;
kulkaA.style.left = '0px';
idInterval = setInterval(() => {
x += dystans / 125; // „125 klatek po 16 ms = 2 s” – ale interwał nie trzyma 16 ms dokładnie
if (x >= dystans) {
x = dystans;
clearInterval(idInterval);
}
kulkaA.style.left = `${x}px`;
}, 16);
// B: poprawnie – położenie wynika z czasu, więc trwa 2 s na każdym ekranie
const startCzas = performance.now();
let klatki = 0;
function klatka(teraz) {
klatki++;
const postep = Math.min((teraz - startCzas) / CZAS, 1); // 0 → 1, nigdy powyżej
kulkaB.style.left = `${easeInOut(postep) * dystans}px`;
if (postep < 1) {
idRaf = requestAnimationFrame(klatka);
} else {
info.textContent = `rAF: ${klatki} klatek w ${Math.round(teraz - startCzas)} ms`;
}
}
idRaf = requestAnimationFrame(klatka);
}
document.getElementById('start').addEventListener('click', start);
start();Na większości komputerów obie kulki dojadą w podobnym czasie – ale czerwona szarpie i spóźnia się, gdy przeglądarka ma dużo pracy, a niebieska zawsze kończy dokładnie po 2 s. requestAnimationFrame dodatkowo zatrzymuje się w kartach w tle, więc nie marnuje baterii.
Ściąga
| Składnia | Co robi | Przykład |
|---|---|---|
const id = setTimeout(fn, ms, ...args) | wywołaj fn raz po ms; zwraca id | setTimeout(() => ukryj(), 3000) |
clearTimeout(id) | odwołaj zaplanowane wywołanie | wzorzec „resetuj odliczanie” |
const id = setInterval(fn, ms) | wywołuj co ms aż do clearInterval | zegar, odliczanie, slider |
clearInterval(id) | zatrzymaj; przed setInterval wyczyść poprzedni | if (id !== null) return |
rekurencyjny setTimeout | kolejny krok po zakończeniu bieżącego; zmienny odstęp | function tik() { …; setTimeout(tik, ms); } |
new Date() / Date.now() | teraz jako obiekt / jako liczba ms | const start = Date.now() |
new Date(r, m, d, h, min) | data lokalna, miesiąc 0–11 | new Date(2027, 0, 12) = 12 I 2027 |
new Date('RRRR-MM-DDTHH:MM') | z napisu ISO (z godziną – lokalnie) | new Date('2026-10-20T08:30') |
getFullYear/Month/Date/Day/Hours… | składniki; getDay() 0 = niedziela | dni[d.getDay()] |
setDate(d.getDate() + n) | przesuń o n dni z autokorektą | setDate(0) → ostatni dzień poprz. miesiąca |
b - a | różnica w ms | Math.round((b - a) / 86400000) dni |
toLocaleDateString('pl-PL', opcje) | 20.10.2026; opcje weekday, month: 'long'… | toLocaleTimeString('pl-PL') → 08:05:00 |
new Intl.DateTimeFormat(…), Intl.RelativeTimeFormat | formater wielokrotnego użytku; „3 dni temu” | format(d), format(-3, 'day') |
toISOString() | 2026-10-19T22:00:00.000Z – UTC! | do serwera; nie do „daty kalendarzowej” |
String(n).padStart(2, '0') | zero wiodące | 08, 05 |
requestAnimationFrame(fn) / cancelAnimationFrame(id) | klatka przed odmalowaniem; fn(czas) | postep = (teraz - start) / czas |
performance.now() | precyzyjny czas od załadowania strony (ms) | pomiar, animacja |
document.hidden, visibilitychange | karta w tle | zatrzymaj slider/zegar |
Zadania
Powiadomienie, które samo znika
Przycisk „Zapisz” pokazuje <div id="toast">Zapisano!</div> (klasa widoczny), który znika po 3 sekundach. Wymagania: wielokrotne kliknięcie resetuje odliczanie (powiadomienie nie znika po 3 s od pierwszego kliknięcia, lecz od ostatniego); przycisk „×” w powiadomieniu zamyka je od razu i odwołuje timer. Pokaż w powiadomieniu, które to z kolei zapisanie.
Pokaż rozwiązanie
<button id="zapisz" type="button">Zapisz</button>
<div id="toast" class="toast"><span id="toast-tekst"></span> <button id="zamknij" type="button" aria-label="zamknij">×</button></div>.toast { position: fixed; bottom: 16px; right: 16px; background: #15171c; color: #fff; padding: 10px 14px; border-radius: 8px; opacity: 0; transform: translateY(10px); transition: opacity 0.3s, transform 0.3s; pointer-events: none; }
.toast.widoczny { opacity: 1; transform: none; pointer-events: auto; }
.toast button { background: none; border: 0; color: #fff; font-size: 18px; cursor: pointer; margin-left: 8px; }const toast = document.getElementById('toast');
const tekst = document.getElementById('toast-tekst');
let timer = null;
let licznik = 0;
function pokazToast(komunikat) {
tekst.textContent = komunikat;
toast.classList.add('widoczny');
clearTimeout(timer); // reset – liczymy 3 s od OSTATNIEGO pokazania
timer = setTimeout(ukryjToast, 3000);
}
function ukryjToast() {
toast.classList.remove('widoczny');
clearTimeout(timer);
timer = null;
}
document.getElementById('zapisz').addEventListener('click', () => {
licznik++;
pokazToast(`Zapisano! (${licznik}. raz)`);
});
document.getElementById('zamknij').addEventListener('click', ukryjToast);Stoper z międzyczasami
Stoper w formacie mm:ss.d (dziesiąte sekundy) z przyciskami Start/Stop (jeden przycisk zmieniający etykietę), „Międzyczas” (dopisuje aktualny czas do listy <ol>) i „Reset”. Czas licz z różnicy Date.now() od chwili startu (plus czas zgromadzony przed pauzą), nie przez dodawanie 100 ms w każdym ticku – interwały nie są dokładne. Odświeżaj co 50 ms. Reset ma być możliwy tylko po zatrzymaniu.
Pokaż rozwiązanie
<p id="czas" class="czas">00:00.0</p>
<button id="startstop" type="button">Start</button>
<button id="miedzyczas" type="button" disabled>Międzyczas</button>
<button id="reset" type="button" disabled>Reset</button>
<ol id="lista"></ol>.czas { font-family: monospace; font-size: 40px; margin: 0 0 8px; }const czasEl = document.getElementById('czas');
const btnStartStop = document.getElementById('startstop');
const btnMiedzy = document.getElementById('miedzyczas');
const btnReset = document.getElementById('reset');
const lista = document.getElementById('lista');
let zgromadzone = 0; // ms zliczone przed ostatnią pauzą
let startOd = null; // Date.now() ostatniego startu, null = stoi
let id = null;
const pad = (n, len = 2) => String(n).padStart(len, '0');
function aktualny() {
return zgromadzone + (startOd === null ? 0 : Date.now() - startOd);
}
function formatuj(ms) {
const min = Math.floor(ms / 60000);
const sek = Math.floor((ms % 60000) / 1000);
const dziesiate = Math.floor((ms % 1000) / 100);
return `${pad(min)}:${pad(sek)}.${dziesiate}`;
}
function odswiez() {
czasEl.textContent = formatuj(aktualny());
}
btnStartStop.addEventListener('click', () => {
if (startOd === null) {
startOd = Date.now();
id = setInterval(odswiez, 50);
btnStartStop.textContent = 'Stop';
btnMiedzy.disabled = false;
btnReset.disabled = true;
} else {
zgromadzone += Date.now() - startOd; // dolicz odcinek, który właśnie się skończył
startOd = null;
clearInterval(id);
odswiez();
btnStartStop.textContent = 'Start';
btnMiedzy.disabled = true;
btnReset.disabled = false;
}
});
btnMiedzy.addEventListener('click', () => {
const li = document.createElement('li');
li.textContent = formatuj(aktualny());
lista.append(li);
});
btnReset.addEventListener('click', () => {
zgromadzone = 0;
lista.innerHTML = '';
odswiez();
btnReset.disabled = true;
});Kalkulator dat
Formularz z polem <input type="date" id="data"> i trzema wynikami w akapitach: a) dzień tygodnia po polsku i czy to weekend, b) liczba dni od dziś do wybranej daty (ujemna, gdy w przeszłości; tekst: Za 84 dni / 12 dni temu / To dziś), c) data za 30 dni od wybranej, sformatowana wtorek, 20 października 2026. Reaguj na change i policz od razu dla dzisiejszej daty. Pamiętaj, że pole type="date" zwraca napis RRRR-MM-DD – zbuduj z niego datę lokalną (nie new Date('2026-10-20'), które jest w UTC).
Pokaż rozwiązanie
<label>Data: <input type="date" id="data"></label>
<p id="a"></p>
<p id="b"></p>
<p id="c"></p>const pole = document.getElementById('data');
const DNI = ['niedziela', 'poniedziałek', 'wtorek', 'środa', 'czwartek', 'piątek', 'sobota'];
const MS_DNIA = 86400000;
const pad = (n) => String(n).padStart(2, '0');
// napis "RRRR-MM-DD" → data LOKALNA o północy (split zamiast new Date(napis), które dałoby UTC)
function zPola(napis) {
const [r, m, d] = napis.split('-').map(Number);
return new Date(r, m - 1, d);
}
function naPole(data) {
return `${data.getFullYear()}-${pad(data.getMonth() + 1)}-${pad(data.getDate())}`;
}
function polnoc(data) {
return new Date(data.getFullYear(), data.getMonth(), data.getDate());
}
function policz() {
const data = zPola(pole.value);
const dzis = polnoc(new Date());
const dzien = data.getDay();
const weekend = dzien === 0 || dzien === 6;
document.getElementById('a').textContent = `To ${DNI[dzien]}${weekend ? ' – weekend!' : ''}.`;
const roznica = Math.round((data - dzis) / MS_DNIA);
const b = roznica === 0 ? 'To dziś.' : roznica > 0 ? `Za ${roznica} dni.` : `${-roznica} dni temu.`;
document.getElementById('b').textContent = b;
const za30 = new Date(data);
za30.setDate(za30.getDate() + 30);
document.getElementById('c').textContent =
'30 dni później: ' + za30.toLocaleDateString('pl-PL', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
}
pole.value = naPole(new Date());
pole.addEventListener('change', policz);
policz();Kalendarz miesiąca
Wyświetl siatkę kalendarza dla dowolnego miesiąca: nagłówek październik 2026 z przyciskami „‹” / „›”, wiersz skrótów dni od poniedziałku (Pn Wt Śr Cz Pt So Nd), potem dni miesiąca w siatce 7 kolumn (CSS Grid). Pierwszy dzień musi trafić do właściwej kolumny (uwaga: getDay() daje 0 dla niedzieli, a ty zaczynasz od poniedziałku). Dzisiejszy dzień ma klasę dzis, weekendy – klasę weekend. Liczbę dni w miesiącu wylicz sztuczką new Date(rok, miesiac + 1, 0).getDate().
Pokaż rozwiązanie
<div class="naglowek">
<button id="poprz" type="button">‹</button>
<strong id="tytul"></strong>
<button id="nast" type="button">›</button>
</div>
<div id="siatka" class="siatka"></div>.naglowek { display: flex; justify-content: space-between; align-items: center; max-width: 340px; margin-bottom: 8px; }
.naglowek button { width: 32px; height: 32px; border-radius: 50%; border: 1px solid #15171c; background: #fff; cursor: pointer; }
.siatka { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; max-width: 340px; }
.siatka .dzien-tyg { text-align: center; font-size: 12px; color: #555; }
.siatka .dzien { text-align: center; padding: 6px 0; border-radius: 6px; background: #f3f3f3; }
.siatka .weekend { background: #fff6c2; }
.siatka .dzis { background: #15171c; color: #fff; font-weight: 700; }const tytul = document.getElementById('tytul');
const siatka = document.getElementById('siatka');
const SKROTY = ['Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So', 'Nd'];
const dzis = new Date();
let rok = dzis.getFullYear();
let miesiac = dzis.getMonth();
function rysuj() {
const pierwszy = new Date(rok, miesiac, 1);
tytul.textContent = pierwszy.toLocaleDateString('pl-PL', { month: 'long', year: 'numeric' });
const dniWMiesiacu = new Date(rok, miesiac + 1, 0).getDate(); // „dzień 0” następnego miesiąca
// getDay(): 0 = niedziela … 6 = sobota → przesuwamy, by 0 = poniedziałek … 6 = niedziela
const przesuniecie = (pierwszy.getDay() + 6) % 7;
siatka.innerHTML = '';
for (const s of SKROTY) {
const el = document.createElement('div');
el.className = 'dzien-tyg';
el.textContent = s;
siatka.append(el);
}
for (let i = 0; i < przesuniecie; i++) siatka.append(document.createElement('div')); // puste pola przed 1.
for (let d = 1; d <= dniWMiesiacu; d++) {
const el = document.createElement('div');
el.className = 'dzien';
el.textContent = d;
const kolumna = (przesuniecie + d - 1) % 7; // 5 = sobota, 6 = niedziela
if (kolumna >= 5) el.classList.add('weekend');
if (d === dzis.getDate() && miesiac === dzis.getMonth() && rok === dzis.getFullYear()) el.classList.add('dzis');
siatka.append(el);
}
}
function przesun(oIle) {
// Date sam skoryguje miesiąc 12 → styczeń następnego roku i -1 → grudzień poprzedniego
const nowa = new Date(rok, miesiac + oIle, 1);
rok = nowa.getFullYear();
miesiac = nowa.getMonth();
rysuj();
}
document.getElementById('poprz').addEventListener('click', () => przesun(-1));
document.getElementById('nast').addEventListener('click', () => przesun(1));
rysuj();Licznik rosnący do wartości (rAF)
Na stronach „o nas” liczby (1250 uczniów, 98 % zdawalności) animują się od 0 do wartości docelowej. Zbuduj to: trzy elementy <span class="licznik" data-cel="1250">0</span> itd. Po kliknięciu „Start” każdy licznik rośnie do swojego data-cel w ciągu 1,5 s z wygładzeniem (easeOut: 1 - (1 - t) ** 3), używając requestAnimationFrame i upływu czasu. Liczby formatuj przez toLocaleString('pl-PL') (separator tysięcy). Kliknięcie „Start” w trakcie animacji ma ją zrestartować, nie uruchomić drugiej.
Pokaż rozwiązanie
<p><span class="licznik" data-cel="1250">0</span> uczniów</p>
<p><span class="licznik" data-cel="98">0</span> % zdawalności</p>
<p><span class="licznik" data-cel="37">0</span> lekcji</p>
<button id="start" type="button">Start</button>.licznik { font-size: 32px; font-weight: 700; font-variant-numeric: tabular-nums; }
p { margin: 4px 0; }const liczniki = [...document.querySelectorAll('.licznik')];
const CZAS = 1500;
const easeOut = (t) => 1 - (1 - t) ** 3;
let idKlatki = null;
function start() {
cancelAnimationFrame(idKlatki); // restart zamiast drugiej animacji
const startCzas = performance.now();
function klatka(teraz) {
const t = Math.min((teraz - startCzas) / CZAS, 1);
const postep = easeOut(t);
for (const el of liczniki) {
const cel = Number(el.dataset.cel);
el.textContent = Math.round(cel * postep).toLocaleString('pl-PL');
}
if (t < 1) idKlatki = requestAnimationFrame(klatka);
}
idKlatki = requestAnimationFrame(klatka);
}
document.getElementById('start').addEventListener('click', start);
start();Test powtórkowy
Test powtórkowy
Timery, Date, formatowanie i requestAnimationFrame. Jedna poprawna odpowiedź.
Co wypisze ten kod?
setTimeout(() => console.log('A'), 100);
setTimeout(() => console.log('B'), 0);
console.log('C');Dlaczego komunikat nigdy się nie pokaże?
function pokaz() { console.log('Czas minął'); }
setTimeout(pokaz(), 2000);Użytkownik klika „Start” trzy razy. Co się stanie?
btn.addEventListener('click', () => {
setInterval(tik, 1000);
});Jaką datę reprezentuje new Date(2026, 11, 24)?
Co zwróci new Date(2026, 9, 25).getDay(), jeśli 25 października 2026 to niedziela?
Który zapis zwraca liczbę dni między dwiema datami a i b (obie o północy)?
Co wypisze console.log(typeof Date(), typeof new Date())?
Który zapis wypisze datę w formacie 20.10.2026 dla polskiego użytkownika bez ręcznego składania napisu?
Dlaczego do płynnej animacji zaleca się requestAnimationFrame zamiast setInterval(f, 16)?
Animacja z requestAnimationFrame przesuwa element o 3 px w każdej klatce. Co się stanie na monitorze 120 Hz w porównaniu z 60 Hz?