From 06097be8298dc8afc8c247c4be8b15458a4122f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Zatorski?= Date: Sat, 5 Sep 2026 07:40:28 +0200 Subject: [PATCH] Licznik odliczajacy na stronie zapisu Sama data w naglowku mowi KIEDY, ale nie mowi ZA ILE: "14 wrzesnia, godz. 10:00" wymaga od odwiedzajacego przeliczenia w glowie. Licznik odpowiada na pytanie, ktore naprawde sobie zadaje: czy to juz, czy jeszcze daleko. Przy wydarzeniu za dwie godziny jest przy okazji sygnalem, ze warto zapisac sie teraz, a nie wrocic pozniej. Miejsce: pod trescia strony, nad formularzem zapisu. ZNIKA PO TERMINIE Odliczanie do czegos, co juz sie zaczelo, wprowadza w blad. Po `startsAt` komponent nie renderuje sie wcale, wiec przy webinarze zakonczonym nie pokazuje zer obok komunikatu o zamknieciu zapisow. CZAS Z ZEGARA, NIE Z ODEJMOWANIA Wartosc bierze sie z roznicy dat przy kazdym tyknieciu. Karta uspiona przez przegladarke wstrzymuje `setInterval`, wiec licznik odliczajacy "o jeden" rozjechalby sie z zegarem tym bardziej, im dluzej strona byla w tle. STREFA CZASOWA Backend zwraca `OffsetDateTime`, czyli moment z przesunieciem, wiec odliczanie jest poprawne takze dla odwiedzajacego z innej strefy niz webinar. Zgodnie z regula, ze strefa zostaje jawna tam, gdzie ma znaczenie. `tabular-nums` (klasa `hx-data`) nie jest tu ozdoba: bez niego cyfry maja rozne szerokosci i licznik drga przy kazdej zmianie sekundy. Czytnik ekranu dostaje jedno zdanie z `aria-live`, a nie cztery liczby odczytane po kolei: same cyfry z podpisami sa dla oka, a odczytane brzmialyby jak zbior przypadkowych wartosci. Co-Authored-By: Claude Opus 5 --- .../src/pages/public/PublicWebinarPage.tsx | 9 ++ .../src/pages/public/WebinarCountdown.tsx | 107 ++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 frontend/src/pages/public/WebinarCountdown.tsx diff --git a/frontend/src/pages/public/PublicWebinarPage.tsx b/frontend/src/pages/public/PublicWebinarPage.tsx index 58ac5d0..1012dc8 100644 --- a/frontend/src/pages/public/PublicWebinarPage.tsx +++ b/frontend/src/pages/public/PublicWebinarPage.tsx @@ -8,6 +8,7 @@ import { PUBLIC_CLOSED_MESSAGE } from '../../lib/labels' import { useDocumentMeta } from '../../lib/useDocumentMeta' import { PublicLayout, PublicNotice } from './PublicLayout' import { RegistrationForm } from './RegistrationForm' +import { WebinarCountdown } from './WebinarCountdown' import { Icon } from '../../components/Icon' export function PublicWebinarPage() { @@ -169,6 +170,14 @@ export function PublicWebinarPage() { )} + {/* + Licznik nad formularzem, pod opisem: odpowiada na pytanie "za ile", + ktore sama data w naglowku zostawia bez odpowiedzi. Sam znika po + terminie rozpoczecia, wiec przy webinarze zakonczonym nie pokazuje + zer obok komunikatu o zamknieciu. + */} + + {/* formularz jest ukryty w kazdym stanie zamknietym, nie tylko zablokowany */} {closed && notice ? ( diff --git a/frontend/src/pages/public/WebinarCountdown.tsx b/frontend/src/pages/public/WebinarCountdown.tsx new file mode 100644 index 0000000..1cf7557 --- /dev/null +++ b/frontend/src/pages/public/WebinarCountdown.tsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from 'react' + +/** + * Odliczanie do rozpoczecia webinaru. + * + * DLACZEGO W OGOLE + * ------------------------------------------------------------------------ + * Sama data w naglowku mowi KIEDY, ale nie mowi ZA ILE. "14 wrzesnia, godz. + * 10:00" wymaga od odwiedzajacego przeliczenia w glowie, a licznik odpowiada + * na pytanie, ktore naprawde sobie zadaje: czy to juz, czy jeszcze daleko. + * Przy wydarzeniu za dwie godziny jest to takze delikatny sygnal, ze warto + * zapisac sie teraz, a nie wrocic pozniej. + * + * ZNIKA, GDY PRZESTAJE MIEC SENS + * ------------------------------------------------------------------------ + * Odliczanie do czegos, co juz sie zaczelo albo skonczylo, wprowadza w blad. + * Po terminie rozpoczecia komponent nie renderuje sie wcale: strona zamknietego + * webinaru ma wtedy wlasny komunikat, a licznik pokazujacy zera obok niego + * wygladalby na zepsuty. + * + * ODSWIEZANIE CO SEKUNDE, LICZONE OD ZEGARA + * ------------------------------------------------------------------------ + * Wartosc bierze sie z roznicy dat przy kazdym tyknieciu, a nie z odejmowania + * jedynki od poprzedniego stanu. Karta uspiona przez przegladarke wstrzymuje + * `setInterval`, wiec licznik odliczajacy "o jeden" rozjechalby sie z zegarem + * tym bardziej, im dluzej strona byla w tle. + */ + +type Remaining = { days: number; hours: number; minutes: number; seconds: number } + +function remaining(target: Date): Remaining | null { + const diff = target.getTime() - Date.now() + if (diff <= 0) return null + + const total = Math.floor(diff / 1000) + return { + days: Math.floor(total / 86400), + hours: Math.floor((total % 86400) / 3600), + minutes: Math.floor((total % 3600) / 60), + seconds: total % 60, + } +} + +export function WebinarCountdown({ startsAt }: { startsAt: string }) { + const target = new Date(startsAt) + const [left, setLeft] = useState(() => remaining(target)) + + useEffect(() => { + const timer = window.setInterval(() => setLeft(remaining(new Date(startsAt))), 1000) + return () => window.clearInterval(timer) + }, [startsAt]) + + // webinar juz sie zaczal albo termin jest nieczytelny: nie ma czego odliczac + if (!left || Number.isNaN(target.getTime())) return null + + return ( +
+

+ Webinar rozpocznie się za: +

+ + {/* + Wartosc czytana przez czytnik ekranu jako jedno zdanie: cztery liczby + z podpisami pod spodem sa dla oka, ale odczytane po kolei brzmialyby + jak zbior przypadkowych cyfr. + */} +

+ Pozostało {left.days} dni, {left.hours} godzin, {left.minutes} minut i {left.seconds} sekund. +

+ + +
+ ) +} + +/** + * Jedna wartosc z podpisem. + * + * `tabular-nums` (klasa `hx-data`) jest tu konieczne, a nie ozdobne: bez niego + * cyfry maja rozne szerokosci i licznik drga przy kazdej zmianie sekundy. + */ +function Unit({ value, label }: { value: number; label: string }) { + return ( +
+

+ {String(value).padStart(2, '0')} +

+

{label}

+
+ ) +} + +/** Dwukropek na wysokosci cyfr, nie podpisow. */ +function Separator() { + return

:

+}