Wideokurs 15 komponentów na strony WWW od MMC School

Dzisiaj na tapecie leży wideokurs 15 komponentów na strony WWW od MMC School.

To nie jest dokładna recenzja kursu, raczej zbiór luźnych przemyśleń i propozycji.

Ogólne uwagi

Mamy 2021 rok i myślę, że to już odpowiedni moment, żeby w kursach posługiwać się modułami ES. Zwłaszcza, że na potrzeby tego kursu jedyną zmianą byłoby dostawienie atrybutu [type=module] do znacznika script. A moduły przy okazji rozwiązałyby jedną ze złych praktyk, jaka jest widoczna w tym kursie – czyli globalny zasięg.

Warto też zmienić wartość atrybutu [lang] w szablonie, bo obecnie jest ustawiony na język angielski, niemniej przykłady są po polsku.

W całej recenzji pomijam także wszelkie problemy z kontrastem, ponieważ są w miarę proste do wyłapania i poprawy (praktycznie każde narzędzie do automatycznych testów dostępności jest w stanie ten błąd wykryć).

W kursie też często pojawia się wzorzec warunkowego dodawania i usuwania klas w postaci:

if ( whatever ) {
    element.classList.add( 'klasa' );
} else {
    element.classList.remove( 'klasa' );
}

Kod ten można zdecydowanie uprościć, używając classList.toggle:

element.classList.toggle( 'klasa', whatever );

Drugi parametr tej metody określa, czy klasa ma być dodana, czy usunięta. Jeśli warunek whatever będzie prawdziwy, klasa zostanie dodana, jeśli będzie fałszywy – usunięta.

Nie można także nie wspomnieć o animacjach, które nie przestrzegają ustawień użytkownika odnośnie ograniczenia ruchu. Problem ten występuje niemal w każdym komponencie. Szkoda, bo jego poprawa nie jest specjalnie trudna i sprowadza się do otoczenia kodu dodającego animację w odpowiedni warunek.

Komponent #1 – Pulsujący przycisk

Na początek pochwalę: bardzo dobrze, że została zwrócona uwaga na ważność posiadania outline’u przez przyciski! Szkoda tylko, że i tak w tym przykładzie został on usunięty, przez co finalny przycisk nie ma absolutnie żadnych stylów, gdy jest sfocusowany.

Osobiście nie dodawałbym dodatkowego elementu tylko na potrzeby animacji. Raczej zrobiłbym to przy pomocy pseudoelementu, do którego przekazywałbym pozycję kursora myszy przy pomocy niestandardowych własności CSS (tzw. zmiennych). Natomiast setTimeout, które ma odpalać kod po zakończonej animacji, zamieniłbym na odpowiednie zdarzenie – animationend lub transitionend.

Wypada też zwrócić uwagę, że offsetTop i offsetLeft nie zadziałają, jeśli element będzie miał pozycjonowanego przodka. Wówczas odległości będą podane względem tego przodka, nie całej strony. Żeby dostać odległość od całej strony, należałoby dodać do siebie offsety danego elementu i wszystkich jego przodków. Alternatywnie zastosować można element.getClientBoundingRect – z tym, że wówczas musielibyśmy nieco zmodyfikować kod tak, aby liczyć względem viewportu.

Przy okazji: w obliczeniach w kursie jest błąd. Jeśli wykorzystuje się button.offsetTop i button.offsetLeft, należałoby równocześnie korzystać z event.pageY oraz event.pageX. W tym momencie pozycja przycisku jest pobierana względem strony, natomiast pozycja kursora – względem viewportu. A to oznacza, że animowane kółko będzie źle pozycjonowane w przypadku, gdy przycisk będzie znajdował się w odległości większej niż jeden ekran od początku strony.

No i wypadałoby dodać obsługę animacji także dla „kliknięcia” przy pomocy klawiatury. Wówczas animacja mogłaby odpalać się w jednym z rogów przycisku.

Po zaaplikowaniu wszystkich uwag (w tym też ogólnych, dotyczących animacji) uzyskujemy bardziej dostępny przycisk. Kolejną optymalizacją, na jaką można się pokusić, to zastosowanie event delegation zamiast przypinania obsługi zdarzeń do każdego przycisku oddzielnie.

Komponent #2 – Nawigacja z wysuwaną wyszukiwarką

Fajnie byłoby, gdyby nawigacja została zrobiona na liście.

Samemu polu wyszukiwania brakuje z kolei etykiety. Atrybut [placeholder] jej nie zastępuje. Można by się przy okazji pokusić o oznaczenie wyszukiwarki przy pomocy odpowiednich atrybutów. Formularz wyszukiwarki (którego tutaj w kodzie brakuje) mógłby mieć rolę search, a samo pole atrybut [type=search] (który jest równoznaczny z rolą searchbox).

Także sam fakt rozsuwania się pola wyszukiwania powinien być oznaczony w odpowiedni sposób, podobnie jak inne formy rozwijanej treści.

W przykładzie jest też problem ze sterowaniem focusem. Pole znajduje się bowiem w kodzie przed przyciskiem je rozwijającym, a to znaczy, że po kliknięciu przycisku z poziomu klawiatury, a następnie naciśnięciu Tab, użytkownik nie znajdzie się w polu wyszukiwania. Co więcej, sposób ukrycia pola w tym komponencie sprawia, że można do niego przenieść focus i pisać w nim nawet wtedy, gdy pole wciąż jest ukryte. Niemniej dla użytkownika, który widzi stronę, jest to bardzo mylące, ponieważ spodziewa się, że po ostatnim linku w nawigacji przeskoczy bezpośrednio do przycisku rozwijającego wyszukiwarkę. Tak się jednak nie dzieje, bo trafia na ukryty element.

Jest też jeszcze jeden problem, który w tym przykładzie jest całkowicie pominięty. Przycisk z lupą jedynie rozwija i zwija pole. Niemniej jego wygląd sugeruje, że jest przyciskiem służącym także do zatwierdzenia wyszukiwania. A takiego przycisku brakuje. Swoją drogą przycisk ten ma niewłaściwą etykietę. Emoji lupy nie niesie bowiem żadnych użytecznych informacji choćby użytkownikom czytników ekranowych. W razie wykorzystania tego emoji jako jedynej zawartości przycisku, należałoby mu dodać dodatkową etykietę przy pomocy [aria-label]. Zwłaszcza, że etykieta składająca się z samego emoji może także utrudniać pracę użytkownikom oprogramowania do sterowania głosem.

Komponent #3 – Zaawansowana kolorowa plansza

W sumie o tym komponencie nie mam nic do powiedzenia. Nie bardzo bowiem jestem w stanie sobie wyobrazić jego praktyczne wykorzystanie.

Komponent #4 – Wskaźnik postępu w formularzach

Największym problemem w tym komponencie bez wątpienia jest sterowanie focusem. Uważam, że w tym wypadku przejście do poprzedniego/następnego kroku w formularzu powinno przenosić użytkownika do pierwszego pola w danym kroku. W chwili, gdy focus zostaje na przyciskach zmieniających kroki, pojawiają się dwa problemy. Pierwszy jest taki, że użytkownik nawigujący klawiaturą musi przenieść focus ręcznie. A to oznacza, że musi przejść przez formularz od końca, by znaleźć się na jego początku. Drugi problem związany jest z tym, że przyciski mogą zostać zablokowane, gdy poprzedniego/następnego kroku nie ma. W tym celu wykorzystany jest atrybut [disabled], który sprawia, że focus jest usuwany z dokumentu. To może być mocno mylące dla użytkowników technologii asystującej. W przypadku, gdy focus byłby przenoszony do formularza, obydwa problemy by nie występowały.

Warto też zwrócić uwagę, że przyciski mają niedostępne etykiety. Strzałka w lewo (←) oraz w prawo (→) nie do końca oddają przeznaczenie tych elementów.

W przypadku tego komponentu aż się prosi o pokazanie, jak go zrobić w technice Progressive Enhancement. W przypadku, gdy JS u użytkownika by nie działał, dostałby on po prostu formularz w całości, niepodzielony na kroki. Dopiero w JS następowałoby to dzielenie (np. na podstawie znajdujących się w formularzu elementów fieldset) oraz dodanie reszty interfejsu kroków.

Element, który jest animowany, można zamienić na pseudoelement ::before. Zawsze to ciut bardziej elegancki kod.

W tłumaczeniu w odcinku pojawia się także wyjaśnienie, że w działaniu ((activeSteps.length - 1) / (steps.length - 1)) * 100 odejmowane jest 1, ponieważ liczenie w tablicy zaczyna się od 0. Nie mogę się zgodzić z tym wyjaśnieniem z dwóch powodów. Po pierwsze, odjęcie 1 wynika raczej z tego, że działanie jest używane do animowania kresek między krokami. A kresek tych jest zawsze o jedną mniej niż kroków. Stąd, jeśli mamy 5 kroków, to mamy 4 kreski. Po drugie, na długość tablicy nie wpływa numer początkowego indeksu. Tablica, która ma 5 elementów, wciąż będzie miała własność length równą 5. Zatem, gdyby obliczenie było wykonywane dla takiej tablicy, odjęcie 1 dałoby wynik dla innej tablicy, zawierającej 4 elementy.

Warto też zauważyć, że technologia asystująca nie dostaje żadnej sensownej informacji o tym, ile jest kroków i na którym obecnie użytkownik się znajduje. Obecny wskaźnik kroków jest bowiem czysto wizualny i dość ciężkostrawny choćby dla czytnika ekranu.

Inna rzecz, że osobiście jestem zdania, że jeśli Twój formularz jest tak skomplikowany, że musisz go dzielić na kroki, to coś poszło bardzo nie tak. I tak naprawdę to powinieneś przeprojektować formularz.

Komponent #5 – Akordeon

Bardzo fajnie, że został wykorzystany tutaj przycisk! Szkoda tylko, że brakuje odpowiednich atrybutów ARIA. Można podpatrzeć przykład akordeonu w ARIA Practices.

Nie rozumiem za bardzo użycia this w funkcji obsługującej klik. Rozumiem, że miała ona być zabezpieczeniem przed kliknięciem w ikonkę w przycisku. Ikonka jest bowiem elementem i, zatem jej kliknięcie spowodowałoby wskazanie innego elementu przez event.target. Faktycznie, this domyślnie wskazuje na element, do którego została przypięta obsługa danego zdarzenia. Problem w tym, że wartość this można łatwo zmienić, np. przy pomocy bind. O wiele pewniejszym rozwiązaniem jest skorzystanie z event.currentTarget, które zawsze będzie wskazywać na element, do którego przypięto obsługę zdarzenia.

Inna rzecz, że mimo wykorzystania sztuczki z this kliknięcie na ikonkę w przycisku nie rozwija danej sekcji. Winny temu jest warunek w funkcji clickOutsideAccordion, który sprawdza, czy kliknięty element ma odpowiednie klasy:

if (
    e.target.classList.contains('accordion-btn') ||
    e.target.classList.contains('accordion-info') ||
    e.target.classList.contains('accordion-info-text')
)
    return

Problem z tym warunkiem polega na tym, że nie sprawdza, czy kliknięty element nie jest dzieckiem jednego z wymienionych elementów. Bez takiego sprawdzenia kliknięcie np. w pogrubienie wewnątrz tekstu rozwiniętej sekcji również by ją zwinęło. Pogrubienie bowiem to osobny element, który nie zostałby wykryty przez ten warunek. Tutaj można wykorzystać element.closest, które przy okazji uprości kod:

if ( e.target.closest( '.accordion-btn, .accordion-info, .accordion-info-text' ) ) {
    return;
}

Jeśli nie chcielibyśmy sprawdzać potomków, to prostszy kod dałaby z kolei metoda element.matches:

if ( e.target.matches( '.accordion-btn, .accordion-info, .accordion-info-text' ) ) {
    return;
}

Nie rozumiem też zdziwienia, że zmiana klasy owinięta w console.log się wykonała. Ta metoda nie zmienia przecież działania przekazanego jej kodu. Wyświetla jedynie w konsoli wynik tego kodu.

Komponent #6 – Slider zdjęć

Po raz kolejny nie są respektowane ustawienia użytkownika odnośnie ograniczania ruchu. Tutaj jednak należałoby dodać także możliwość wyłączenia automatycznego przesuwania się slidera.

Zamiast operowania w JS na stylach bezpośrednio, użyłbym zmiennych CSS. Sprawiłoby to, że rozwiązanie byłoby nieco bardziej eleganckie.

W tym komponencie przyciski znowu mają nieopisowe etykiety.

Cały slider jest też zrobiony przy pomocy setInterval. Osobiście pewnie bym tutaj wykorzystał setTimeout, ale w tak prostym przykładzie jest to praktycznie bez znaczenia.

Komponent #7 – Rozwijane slajdy

Ten komponent jest nieużywalny dla użytkownika posługującego się klawiaturą, bo nie ma tutaj żadnych elementów focusowalnych. A wystarczyłoby dać linki do każdego nagłówka. Wykorzystanie linków daje także ulepszenie w postaci zmiany adresu URL i możliwość wykorzystania :target. Dzięki tym prostym zmianom cały komponent działałby z powodzeniem także bez JS, który byłby potrzebny głównie do zmiany tła całej strony oraz rozwinięcia pierwszej z kart (przynajmniej do czasu, aż :has() nie zacznie działać).

No i szkoda, że nie jest to responsywne. Mimo wszystko w 2021 roku powinno być to standardem. I uważam, że powinno się na to kłaść bardzo mocny nacisk w kursach. Dzisiaj niemal żadna strona nie może się obejść bez responsywności – zwłaszcza, że więcej jest użytkowników mobilnych niż desktopowych.

Komponent #8 – Animowany tekst

Tutaj główne problemy z dostępnością powiązane są z okienkiem, które pozwala wprowadzać tekst do animacji. Tego typu okienko powinno być odpowiednio oznaczone oraz wymaga odpowiedniego sterowania focusem. Po kliknięciu w przycisk otwierający takie okienko, focus powinien zostać automatycznie przeniesiony do otwartego okienka (na okienko jako całe lub do jego pierwszego elementu focusowalnego). Natomiast po zamknięciu okienka focus powinien zostać z powrotem przeniesiony na przycisk otwierający to okienko. W innym przypadku (co jest tutaj widoczne) focus zostanie całkowicie usunięty z obszaru strony. ARIA Practices zawiera odpowiedni przykład.

Okienko zawiera dodatkowo formularz, składający się z jednego pola i przycisku. Niestety, pole to nie ma etykiety. Dodatkowo, w razie niewpisania tekstu, pod polem pojawia się komunikat o błędzie. Nie jest on jednak poprawnie przypięty do pola przy pomocy atrybutu [aria-describedby]. Samo pole również nie zostało odpowiednio oznaczone jako niepoprawnie wypełnione – brakuje atrybutu [aria-invalid=true]. Równocześnie muszę przyznać, że przykładów dobrze i dostepnie zrobionych formularzy jest w Internecie wręcz podejrzanie mało. Na szczęście rząd UK ma naprawdę dobre zasoby o dostępności.

No i pytanie, czy czytnik ekranu poradzi sobie z przeczytaniem takiego animowanego tekstu? Być może warto byłoby go ukryć przy pomocy [aria-hidden] i czytnikom ekranowym dostarczyć tekst w innej formie.

Komponent #9 – Przeciągana lista prezentów

Tutaj główne zastrzeżenie mam takie, że nie da się użyć tego komponentu przy pomocy samej klawiatury. Oczywiście trudno byłoby zrobić przeciąganie i upuszczanie przy pomocy klawiatury. Można jednak zaproponować technikę zastępczą. Prosty przykład: zrobienie z prezentów checkboxów i dodanie przycisku Dodaj do listy prezentów. Po wybraniu odpowiednich checkboxów, użytkownik fokusowałby przycisk i po jego naciśnięciu prezenty zostałyby przeniesione do drugiej rubryki. Obecnie komponent nie jest w ogóle dostępny z poziomu klawiatury, ponieważ nie zawiera nawet elementów focusowalnych.

No i jest jeszcze słoń w składzie porcelany: przycisk w linku. Tutaj to po prostu powinien być odpowiednio ostylowany link. Nie robi się przycisków w linkach – po prostu.

Komponent #10 – Counter

Osobiście bym nie wybrał ikonek w formie fonta, ale jako SVG – bo jest lepsze niemal pod każdym względem.

Mam też zastrzeżenie podobne co w komponencie #8: czy nie lepiej byłoby animowany tekst ukryć przed czytnikami ekranu i dostarczyć im od razu gotowe wartości?

Jeśli chodzi o to zawiłe wytłumaczenie działania, jak dla mnie o wiele prościej byłoby to wyjaśnić po prostu jako proporcje.

Komponent #11 – Timeline

Tutaj nie mam się do czego przyczepić – jakby nie patrzeć, to po prostu ostylowana lista.

No ok, niech będzie, że w teorii powinna być tutaj lista uporządkowana zamiast listy nieuporządkowanej. Ten komponent prezentuje wydarzenia w kolejności chronologicznej i można to zaznaczyć właśnie przez zastosowanie ol.

Komponent #12 – Cookie alert + localStorage

To kolejny komponent, który nie respektuje ustawień użytkownika w zakresie ograniczania ruchu. Komunikat posiada nieblokowalną animację wjeżdżania na stronę.

Osobiście prawdopodobnie użyłbym tutaj ciasteczka do zapisania, że użytkownik już zaakceptował ciasteczka. Dzięki temu przy kolejnym odwiedzeniu przez użytkownika strony byłbym w stanie nie wysyłać mu niepotrzebnie komunikatu wraz z kodem JS do jego obsługi. Ot, prosta optymalizacja. I tak, obsługa ciasteczek w JS-ie jest mocno upierdliwa… ale to może się niedługo zmienić. Istnieje bowiem eksperymentalne API, Cookie Store, które w końcu pozwala na zabawę ciasteczkami w cywilizowany sposób.

Komponent #13 – Powiększanie zdjęć produktów

W sumie tutaj problem jest podobny, jak w przypadku komponentu #1: część obliczeń jest robiona dla viewportu, część – dla strony. Dodatkowo strony w działaniach powinny być raczej odwrotnie (od viewportu odejmujemy odległość obrazka od krawędzi ekranu), dzięki czemu wyeliminować można mnożenie przez -1.

Komponent #14 – Scrollspy

Po raz kolejny nawigacja nie jest na liście. A powinna być – to jednak już de facto standard.

W przykładzie wykorzystane jest scroll-behavior: smooth, ale znów jest wymuszane nawet wówczas, gdy użytkownik zażyczy sobie ograniczania ruchu. Co więcej, sposób, w jaki płynne przewijanie jest zaaplikowane, sprawia, że będzie ono przeszkadzać m.in. w wyszukiwaniu treści na stronie (przeglądarka do każdego szukanego słowa będzie płynnie przewijać).

Myślę też, że rozwiązanie oparte na IntersectionObserverze byłoby zdecydowanie prostsze a przy okazji – bardziej eleganckie. Zamiast stosować magiczne liczby i dodawać specjalną obsługę ostatniej sekcji, wystarczyłoby ustawić obserwatora tak, by wskazywał, która sekcja jest obecnie w pełni widoczna na ekranie.

No i tutaj aż się prosi, by wykorzystać klauzulę strażniczą w celu uproszczenia kodu, ponieważ jego całość jest wewnątrz warunku. Odwrócenie tego warunku pozwoliłoby usunąć jeden poziom zagłębienia kodu.

Można się też pokusić o oznaczenie aktualnego linku w nawigacji przy pomocy odpowiedniego atrybutu ARIA.

Komponent #15 – Scrollbar i przycisk powrotu na górę

Link do przejścia na samą górę strony powinien być linkiem. Tutaj zastosowany jest przycisk, wywołująćy przewijanie przy pomocy window.scroll. Oprócz problemów z semantyką (to linki służą do przechodzenia w inne miejsca na danej stronie, nie przyciski), tego typu przewijanie niesie także ze sobą problemy z dostępnością. W przypadku przycisku focus nie zostanie przeniesiony na górę strony. Jeśli wykorzystamy link i odeślemy do pierwszego elementu na stronie (przy pomocy kotwicy), wówczas focus zostanie przeniesiony do tego elementu.

Samo umiejscowienie przycisku powrotnego na górę strony także jest nie najlepsze. Obecnie przycisk ten znajduje się w kodzie przed treścią strony, zatem osoba nawigująca po stronie przy pomocy klawiatury odwiedzi ten przycisk na samym początku. Czyli wtedy, gdy nie musi go używać. Ten przycisk powinien być na samym końcu kodu, by zachowana została sensowna kolejność focusu.

Z kolei sam pasek postępu można zrobić przy pomocy pseudoelementu ::before głównego nagłówka strony. Nie trzeba używać w tym celu dodatkowego elementu.

Warto też dodać, że obliczenie pozostałej do przewijania treści obliczyć można obecnie przy pomocy pobrania wysokości wizualnego viewportu oraz przewijalnej wysokości elementu. Natomiast jako element powinien być użyty element wskazywany przez document.scrollingElement – czyli element służący do przewijania strony (do którego przyczepiony jest pasek przewijania przeglądarki). Trzeba przy tym pamiętać, że Firefox wciąż nie ma wsparcia dla API wizualnego viewportu – i jest ostatnią przeglądarką bez niego. Dlatego na chwilę obecną wykorzystać można używany w komponencie window.innerHeight.

Podsumowanie

Niestety, mimo że dobór komponentów w kursie jest bardzo ciekawy, to po raz kolejny jest to kurs skupiający się przede wszystkim na wizualiach. Cierpi na tym dostępność i semantyka. A szkoda, bo nisza kursów traktujących dostępność jako integralną część webdevelopmentu jest niemal nieistniejąca.

6 myśli w temacie “Wideokurs 15 komponentów na strony WWW od MMC School”

  1. „No i jest jeszcze słoń w składzie porcelany: przycisk w linku.”
    Macie pomysł, skąd są te problemy? Przecież linki i przyciski nie są nowymi elementami; dlaczego po kilkunastu latach ludzie ciągle mają problem ze zrozumieniem, do czego służą?

    Ostatnio w płatnym(!) plugienie do WP trafiłam na taki przypadek, oczywiście jedyna możliwość poprawy to zmiana bezpośrednio w plikach (więc zostanie nadpisane przy aktualizacjach)…

    1. Tak jak podejrzewałem, jest to kolejny niskiej jakości kurs z agresywną kampanią reklamową. Jest to niestety żerowanie na ludziach, którzy dopiero stawiają swoje pierwsze kroki w świecie web devu i nie są w stanie obiektywnie ocenić, ile warta jest zdobyta przez nich wiedza.
      Miałem w przeciągu ostatnich dwóch lat okazję recenzować kod kilkunastu aspirujących juniorów. Większość z nich odpadła już na etapie sprawdzania poprawności HTMLa. Brak labeli dla inputów w formularzach (często w ogóle brak znacznika „form”!), stosowanie divów tam, gdzie powinien znaleźć sie odpowiedni do sytuacji element (header, nav, main, section, article, etc.), albo po prostu dziwaczne kombinacje jak ul > a > li to kilka z „grzechów”, które popełniał prawie każdy kandydat.
      Do tego często widywałem pozycjonowanie elementów przy pomocy floatów w CSS albo podpinanie eventów poprzez artybut „onclick”. Zupełnie jakby w materiałach do nauki czas stanąl w miejscu kilka lat temu.

      1. Na szczęście są ludzie, którzy nic nie wiedzą, ale zawsze chętnie się wypowiadają. 🙂

        Gdybyś tylko zajrzał do wymagań kursu, zobaczyłbyś, że jest on częścią pewnej ścieżki – kilku kursów tworzenia stron www.

        Gdybyś obejrzał chociaż pierwszą część, wiedziałbyś, że jest w niej cały odcinek poświęcony narzekaniu na układanie elementów za pomocą float. Nie wspominając o reszcie rzeczy, o których piszesz.

        No ale wtedy musiałbyś zrezygnować z zasady „nie znam się, wypowiem się”.

        1. >”Gdybyś obejrzał chociaż pierwszą część, wiedziałbyś, że jest w niej cały odcinek poświęcony narzekaniu na układanie elementów za pomocą float. Nie wspominając o reszcie rzeczy, o których piszesz.”
          Ale pan Franek Zamek nie pisał o tym konkretnym kursie tylko o tych z którymi miał do czynienia, a pan to przyjął do siebie

          1. „Tak jak podejrzewałem, jest to kolejny niskiej jakości kurs z agresywną kampanią reklamową.”

            Pan Franek konkretnie wskazał na ten kurs i na „agresywną kampanię reklamową” (pomijam fakt, że żadnej kampanii nie prowadziłem ^^).

  2. To chyba koniec Webkrytyka, cenię pracę Comandeera, ale przez żadkość pojawiania się nowych wpisów, stał się nudny. Ja już się żegnam z tym miejscem.
    Powodzenia w dalszym rozwoju strony, niech Bóg się zlituje nad nią!

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.