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 :
+}