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

:

+}