Rozbudowa kalendarza w HTML, CSS i jQuery o dynamiczne tłumaczenia z API za pomocą AJAXa w formacie JSONa

W poprzednich wpisach opisaliśmy budowę kalendarza w JavaScript oraz jego rozbudowę o dodatkowe funkcje. W pierwszym artykule, Kalendarz w HTML, CSS i jQuery, przedstawiliśmy krok po kroku, jak stworzyć podstawowy kalendarz. Następnie, w Refaktoryzacja i rozbudowa kalendarza w HTML, CSS i jQuery, omówiliśmy, jak ulepszyć ten kalendarz, dodając nowe funkcje i poprawiając jego strukturę.

Teraz zajmiemy się kolejnym etapem rozbudowy kalendarza – dodaniem tłumaczeń pobieranych dynamicznie za pomocą AJAX z API zwracającego dane w formacie JSON. Dzięki temu nasz kalendarz będzie mógł automatycznie dostosowywać się do wybranego przez użytkownika języka, co znacznie zwiększy jego funkcjonalność i dostępność.

Czym jest AJAX, JSON i API?

Przed przystąpieniem do implementacji, warto dokładniej zrozumieć działanie technologii, które będziemy wykorzystywać. AJAX umożliwia nam wykonywanie zapytań do serwera w tle, bez konieczności przeładowywania strony. To sprawia, że aplikacje webowe mogą działać szybciej i bardziej responsywnie.

JSON jest idealnym formatem do przesyłania danych, ponieważ jest zwięzły i łatwy do parsowania. Struktura JSON przypomina obiekty w JavaScript, co ułatwia jego integrację z istniejącym kodem.

API pozwala na komunikację między różnymi aplikacjami, umożliwiając przesyłanie danych i wykonywanie operacji na serwerze. W naszym projekcie, API zwróci odpowiednie tłumaczenia w formacie JSON, które nasz kalendarz pobierze i wykorzysta za pomocą AJAX.

Dzięki wykorzystaniu AJAX, JSON i API, nasz kalendarz będzie mógł pobierać tłumaczenia w locie, co umożliwi użytkownikom natychmiastową zmianę języka bez konieczności odświeżania strony.

Dodanie elementu do zmiany języka

Wprowadzenie dynamicznych tłumaczeń jest kluczowym ulepszeniem w naszym projekcie kalendarza, które znacznie zwiększa jego dostępność i użyteczność dla użytkowników z różnych regionów świata. Do realizacji tego celu, niezbędne jest wprowadzenie elementu interfejsu użytkownika, który umożliwi wybór języka.

Kalendarz z panelem do zmiany języka.

W naszym przykładzie, na samym początku pliku script.js, definiujemy zmienną lang, która przechowuje aktualnie wybrany język. Domyślnie ustawiona jest na 'pl' (polski), co odzwierciedla domyślne ustawienie dla naszych użytkowników:

var lang = 'pl'; // Domyślny język

Następnie, w sekcji tworzenia fragmentu dokumentu, dodajemy nowy element div z identyfikatorem language-panel. Wewnątrz tego elementu umieszczamy element select z identyfikatorem change-lang, który pozwala użytkownikom na wybór języka. Do tego select dodajemy opcje wyboru języka polskiego oraz angielskiego:

// Wyświetlenie panelu do zmiany języka
fragment.append($('<div>').attr('id', 'language-panel')
    .append($('<select>').attr('id', 'change-lang')
        .append($('<option>').val('pl').text('Polski'))
        .append($('<option>').val('en').text('English'))
    .val(lang)));

Element select nie tylko umożliwia wybór języka, ale także jest kluczowy w obsłudze zmiany języka bez konieczności przeładowania strony. Za pomocą jQuery, możemy dodać obsługę zdarzenia zmiany wybranego elementu, co pozwoli na dynamiczne pobranie odpowiednich tłumaczeń z serwera.

Stylizacja panelu zmiany języka

Oprócz dodania funkcjonalności umożliwiającej zmianę języka, kluczowe jest także zadbanie o estetykę i ergonomię elementu interfejsu. W sekcji CSS projektu dodaliśmy stylizacje, które nie tylko poprawiają wygląd, ale także funkcjonalność elementu do zmiany języka.

#language-panel {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    grid-column: span 7;
    background-color: darkgrey;
}

#change-lang {
    height: 25px;
    padding: 5px;
    font-size: 12px;
    background-color: lightgrey;
    border-radius: 5px;
    margin-right: 10px;
}

Definicja stylów dla #language-panel ma na celu umieszczenie go na początku kontenera gridowego kalendarza, zachowując przy tym jego funkcjonalność i dostępność. Ustawienia takie jak display: flex;, justify-content: flex-end; oraz align-items: center; pozwalają na wygodne umiejscowienie elementu w prawym górnym rogu kalendarza, co jest intuicyjne dla użytkownika.

Element #change-lang, czyli rozwijana lista wyboru języka, została zaprojektowana tak, aby była wygodna w użyciu i jednocześnie estetycznie dopasowana do reszty interfejsu. Wysokość (height: 25px;), padding (padding: 5px;), rozmiar czcionki (font-size: 12px;) oraz kolor tła (background-color: lightgrey;) są dobrze zbalansowane, co zapewnia wygodę użytkowania, niezależnie od rozmiaru urządzenia. Zaokrąglenie rogów (border-radius: 5px;) dodaje nowoczesności i łagodzi kształt elementu. Ostatni styl, margin-right: 10px;, zapewnia odpowiednią przestrzeń od krawędzi kontenera, co ułatwia dostęp do listy rozwijanej.

Zobacz commit na GitHub.

Implementacja początkowej funkcji zmiany języka bez użycia AJAX

W dalszej części rozbudowy naszego kalendarza dodajemy funkcjonalność dynamicznej zmiany języka. W tej sekcji, zanim użyjemy AJAX-a i stworzymy kod API, wprowadzamy tłumaczenia bezpośrednio w skrypcie JavaScript. Pozwoli to nam zrozumieć mechanizm zmiany języka, zanim zamienimy te dane na pobierane z zewnętrznego źródła.

Definiujemy obiekt translations, który będzie przechowywać tłumaczenia dla różnych elementów kalendarza, takich jak nazwy miesięcy i dni tygodnia:

var translations = {};

Następnie implementujemy funkcję changeTranslations(lang), która będzie ustawiać odpowiednie tłumaczenia w zależności od wybranego języka:

    function changeTranslations(lang) {
        if (lang === 'pl') {
            translations = {
                'month_january': 'Styczeń', 'month_february': 'Luty', 'month_march': 'Marzec',
                'month_april': 'Kwiecień', 'month_may': 'Maj', 'month_june': 'Czerwiec',
                'month_july': 'Lipiec', 'month_august': 'Sierpień', 'month_september': 'Wrzesień',
                'month_october': 'Październik', 'month_november': 'Listopad', 'month_december': 'Grudzień',
                'day_monday': 'Pon', 'day_tuesday': 'Wto', 'day_wednesday': 'Śro',
                'day_thursday': 'Czw', 'day_friday': 'Pią', 'day_saturday': 'Sob', 'day_sunday': 'Nie'
            };
        } else if (lang === 'en') {
            translations = {
                'month_january': 'January', 'month_february': 'February', 'month_march': 'March',
                'month_april': 'April', 'month_may': 'May', 'month_june': 'June',
                'month_july': 'July', 'month_august': 'August', 'month_september': 'September',
                'month_october': 'October', 'month_november': 'November', 'month_december': 'December',
                'day_monday': 'Mon', 'day_tuesday': 'Tue', 'day_wednesday': 'Wed',
                'day_thursday': 'Thu', 'day_friday': 'Fri', 'day_saturday': 'Sat', 'day_sunday': 'Sun'
            };
        }
        generateCalendar(month, year);
    }

Przy ładowaniu strony, zamiast bezpośrednio wywoływać generateCalendar(month, year);, uruchamiamy changeTranslations(lang);, co pozwala na wczytanie odpowiednich tłumaczeń i wygenerowanie kalendarza:

changeTranslations(lang);

Podmieniamy sztywne nazwy miesięcy i dni tygodnia na dynamiczne tłumaczenia, wykorzystując nowo utworzoną zmienną translations:

// Tablica z nazwami miesięcy
var monthName = [
    translations['month_january'], translations['month_february'], translations['month_march'],
    translations['month_april'], translations['month_may'], translations['month_june'],
    translations['month_july'], translations['month_august'], translations['month_september'],
    translations['month_october'], translations['month_november'], translations['month_december']
];

// Tablica z nazwami dni tygodnia
var dayName = [
    translations['day_monday'], translations['day_tuesday'], translations['day_wednesday'],
    translations['day_thursday'], translations['day_friday'], translations['day_saturday'],
    translations['day_sunday']
];

Dodajemy zdarzenie onchange dla elementu #change-lang, które wywołuje funkcję changeTranslations(lang); przy każdej zmianie języka:

// Onchange do zmiany tłumaczenia
$(document).on('change', '#change-lang', function() {
    lang = $(this).val();
    changeTranslations(lang);
});

Implementacja dynamicznych tłumaczeń w naszym kalendarzu przebiega etapami. Na początku wprowadziliśmy tłumaczenia bezpośrednio w kodzie, aby zrozumieć mechanizm działania. Następnie dodaliśmy funkcję changeTranslations, która umożliwia aktualizację kalendarza na podstawie wybranego języka.

Zobacz commit na GitHub.

Tworzenie API do obsługi tłumaczeń

Po początkowej implementacji funkcji zmiany języka bezpośrednio w kodzie JavaScript, następnym krokiem w rozbudowie naszego kalendarza jest przeniesienie tłumaczeń do bazy danych i stworzenie API, które pozwoli na ich dynamiczne ładowanie za pomocą AJAX. Umożliwi to łatwiejszą aktualizację i zarządzanie tłumaczeniami.

Najpierw musimy stworzyć bazę danych oraz tabelę, która będzie przechowywać tłumaczenia. Oto kwerendy SQL do stworzenia bazy danych o nazwie calendar oraz tabeli translations i wypełnienie jej tłumaczeniami polskimi i angielskimi:

CREATE DATABASE calendar;

USE calendar;

CREATE TABLE translations (
    id INT AUTO_INCREMENT PRIMARY KEY,
    key_name VARCHAR(50) NOT NULL,
    lang VARCHAR(10) NOT NULL,
    value VARCHAR(255) NOT NULL
);

INSERT INTO translations (key_name, lang, value) VALUES
('month_january', 'pl', 'Styczeń'),
('month_february', 'pl', 'Luty'),
('month_march', 'pl', 'Marzec'),
('month_april', 'pl', 'Kwiecień'),
('month_may', 'pl', 'Maj'),
('month_june', 'pl', 'Czerwiec'),
('month_july', 'pl', 'Lipiec'),
('month_august', 'pl', 'Sierpień'),
('month_september', 'pl', 'Wrzesień'),
('month_october', 'pl', 'Październik'),
('month_november', 'pl', 'Listopad'),
('month_december', 'pl', 'Grudzień'),
('month_january', 'en', 'January'),
('month_february', 'en', 'February'),
('month_march', 'en', 'March'),
('month_april', 'en', 'April'),
('month_may', 'en', 'May'),
('month_june', 'en', 'June'),
('month_july', 'en', 'July'),
('month_august', 'en', 'August'),
('month_september', 'en', 'September'),
('month_october', 'en', 'October'),
('month_november', 'en', 'November'),
('month_december', 'en', 'December'),
('day_monday', 'pl', 'Pon'),
('day_tuesday', 'pl', 'Wto'),
('day_wednesday', 'pl', 'Śro'),
('day_thursday', 'pl', 'Czw'),
('day_friday', 'pl', 'Pią'),
('day_saturday', 'pl', 'Sob'),
('day_sunday', 'pl', 'Nie'),
('day_monday', 'en', 'Mon'),
('day_tuesday', 'en', 'Tue'),
('day_wednesday', 'en', 'Wed'),
('day_thursday', 'en', 'Thu'),
('day_friday', 'en', 'Fri'),
('day_saturday', 'en', 'Sat'),
('day_sunday', 'en', 'Sun');

Następnie tworzymy skrypt PHP, który będzie obsługiwał zapytania do naszego API. Skrypt będzie pobierał tłumaczenia z bazy danych i zwracał je w formacie JSON.

<?php
header("Access-Control-Allow-Origin: *");

$servername = "localhost";
$username = "root";
$password = "";
$dbname = "calendar";

// Połączenie z bazą danych
$conn = new mysqli($servername, $username, $password, $dbname);

// Sprawdzenie połączenia
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}

// Pobranie języka z żądania
$lang = isset($_GET['lang']) ? $_GET['lang'] : 'pl';

// Przygotowanie  i wysłanie zapytania do bazy
$stmt = $conn->prepare("SELECT key_name, value FROM translations WHERE lang = ?");
$stmt->bind_param("s", $lang);
$stmt->execute();
$result = $stmt->get_result();

// Wypełnienie tablicy tłumaczeniami
$translations = array();
while($row = $result->fetch_assoc()) {
    $translations[$row['key_name']] = $row['value'];
}

// Zwrot tłumaczeń JSONem z opcją JSON_UNESCAPED_UNICODE
echo json_encode($translations, JSON_UNESCAPED_UNICODE);

$stmt->close();
$conn->close();
?>

Ten skrypt PHP łączy się z bazą danych MySQL przy użyciu podanych parametrów: nazwy serwera ($servername), nazwy użytkownika ($username), hasła ($password) i nazwy bazy danych ($dbname). Po nawiązaniu połączenia, skrypt sprawdza, czy nie wystąpił błąd podczas łączenia się z bazą danych. Jeśli połączenie się nie powiedzie, skrypt zwraca komunikat o błędzie i przerywa działanie.

Następnie skrypt pobiera parametr lang z żądania GET, który określa, w jakim języku mają być zwrócone tłumaczenia. Jeśli parametr lang nie jest podany, domyślnie ustawiany jest język polski ('pl').

Kolejnym krokiem jest przygotowanie zapytania SQL, które selekcjonuje odpowiednie tłumaczenia z tabeli translations na podstawie podanego języka. Skrypt przygotowuje zapytanie SQL (SELECT key_name, value FROM translations WHERE lang = ?) i wiąże parametr języka ($lang) z zapytaniem przy użyciu metody bind_param. Następnie wykonuje zapytanie za pomocą metody execute.

Po wykonaniu zapytania skrypt pobiera wyniki i zapisuje je w tablicy asocjacyjnej $translations, gdzie klucze to nazwy kluczy tłumaczeń (key_name), a wartości to odpowiednie tłumaczenia (value).

Na końcu skrypt koduje tablicę $translations do formatu JSON i zwraca ją jako odpowiedź. Skrypt zamyka połączenie z bazą danych, wywołując metody close dla zapytania ($stmt) i połączenia ($conn).

Zobacz commit na GitHub.

Testowanie API

Najlepszym sposobem na testowanie tego projektu jest użycie lokalnego serwera, takiego jak XAMPP. XAMPP umożliwia uruchamianie serwera Apache z obsługą PHP i MySQL, co jest idealne do testowania aplikacji webowych.

  1. Pobierz i zainstaluj XAMPP: Możesz pobrać XAMPP ze strony apachefriends.org.
  2. Skonfiguruj XAMPP:
    • Umieść pliki projektu w katalogu htdocs w folderze instalacyjnym XAMPP (zazwyczaj C:\xampp\htdocs).
    • Uruchom serwer Apache i MySQL za pomocą panelu kontrolnego XAMPP.
  3. Stwórz bazę danych:
    • Otwórz phpMyAdmin (dostępny pod http://localhost/phpmyadmin).
    • Wykonaj kwerendę SQL do stworzenia bazy danych i tabeli z tłumaczeniami, jak opisano powyżej.

Jeśli używasz XAMPPa możesz przetestować działanie naszego API wpisując w przeglądarce adres: http://localhost/lang.php powinieneś otrzymać JSONa z polskim tłumaczeniem (dostępne także w taki sposób: http://localhost/lang.php?lang=pl):

{"month_january":"Styczeń","month_february":"Luty","month_march":"Marzec","month_april":"Kwiecień","month_may":"Maj","month_june":"Czerwiec","month_july":"Lipiec","month_august":"Sierpień","month_september":"Wrzesień","month_october":"Październik","month_november":"Listopad","month_december":"Grudzień","day_monday":"Pon","day_tuesday":"Wto","day_wednesday":"Śro","day_thursday":"Czw","day_friday":"Pią","day_saturday":"Sob","day_sunday":"Nie"}

Natomiast angielskie tłumaczenie uzyskamy pod tym samym adresem ale z parametrem lang=en: http://localhost/lang.php?lang=en

Opisaliśmy, jak stworzyć API w PHP, które dynamicznie zwraca tłumaczenia z bazy danych MySQL. Dzięki temu nasz kalendarz za chwilę będzie mógł dynamicznie pobierać tłumaczenia za pomocą AJAX, co znacznie zwiększa jego elastyczność i dostępność.

Implementacja dynamicznego ładowania tłumaczeń za pomocą AJAX

Po stworzeniu API w PHP, które zwraca tłumaczenia z bazy danych w formacie JSON, możemy teraz zmodyfikować nasz skrypt JavaScript, aby korzystał z tego API. Zastąpimy statyczne tłumaczenia w funkcji changeTranslations na dynamiczne ładowane za pomocą AJAX.

Zamiast przechowywać tłumaczenia bezpośrednio w kodzie JavaScript, będziemy teraz ładować je dynamicznie z naszego API za pomocą AJAX. Dodamy również obsługę błędów, aby informować użytkownika w przypadku problemów z pobieraniem tłumaczeń.

Zaktualizowana funkcja changeTranslations:

function changeTranslations(lang) {
    $.ajax({
        url: 'http://localhost/lang.php',
        data: { lang: lang },
        dataType: 'json',
        success: function(data) {
            translations = data;
            generateCalendar(month, year);
        },
        error: function(jqXHR, textStatus, errorThrown) {
            alert("Wystąpił błąd podczas pobierania tłumaczeń: " + textStatus);
            console.error("Error details:", errorThrown);
        }
    });
}

Funkcja changeTranslations(lang) pobiera tłumaczenia w zależności od wybranego języka za pomocą zapytania AJAX. Wewnątrz funkcji wywoływana jest metoda $.ajax, która wysyła żądanie GET na adres http://localhost/lang.php z parametrem lang, określającym język tłumaczeń. Parametr dataType jest ustawiony na json, co oznacza, że oczekujemy odpowiedzi w formacie JSON.

Jeśli żądanie zakończy się sukcesem, funkcja anonimowa przypisana do klucza success zostanie wywołana z pobranymi danymi. Dane te są przypisywane do zmiennej translations, a następnie wywoływana jest funkcja generateCalendar(month, year), aby zaktualizować kalendarz z nowymi tłumaczeniami.

W przypadku, gdy zapytanie nie powiedzie się, funkcja anonimowa przypisana do klucza error zostanie wywołana. W tej funkcji błąd jest logowany w konsoli za pomocą console.error, a użytkownikowi wyświetlany jest komunikat alertujący o niepowodzeniu ładowania tłumaczeń i sugerujący ponowną próbę później.

Teraz możemy przetestować naszą aplikację, aby upewnić się, że tłumaczenia poprawnie ładują się z API i wyświetlają w kalendarzu. Poniżej znajduje się pełny kod JavaScript z aktualizacją funkcji changeTranslations.

$(document).ready(function() {
    // Dane startowe
    var date = new Date();
    var month = date.getMonth() + 1; // Pobranie aktualnego miesiąca i korekta indeksu (getMonth zwraca miesiące od 0 do 11)
    var year = date.getFullYear(); // Pobranie aktualnego roku w formacie czterocyfrowym
    var lang = 'pl'; // Domyślny język
    var translations = {};

    changeTranslations(lang);

    // Funkcja do pobierania tłumaczeń z api i ich zmiany
    function changeTranslations(lang) {
        $.ajax({
            url: 'http://localhost/lang.php',
            data: { lang: lang },
            dataType: 'json',
            success: function(data) {
                translations = data;
                generateCalendar(month, year);
            },
            error: function(xhr, status, error) {
                console.error('AJAX Error:', status, error);
                alert('Nie udało się załadować tłumaczeń. Spróbuj ponownie później.');
            }
        });
    }

    function generateCalendar(month, year) {
        var day = date.getDate(); // Pobranie aktualnego dnia miesiąca

        // Obliczenie pierwszego dnia miesiąca
        var startDay = new Date(year, month - 1, 1).getDay(); // .getDay() zwraca indeksy dni tygodnia 0-6, gdzie 0 to niedziela
        if (startDay === 0) { startDay = 7; } // Zamiana numeru dla niedzieli na format 1-7

        // Pobranie maksymalnego dnia w miesiącu
        var maxDay = new Date(year, month, 0).getDate();

        // Tablica z nazwami miesięcy
        var monthName = [
            translations['month_january'], translations['month_february'], translations['month_march'],
            translations['month_april'], translations['month_may'], translations['month_june'],
            translations['month_july'], translations['month_august'], translations['month_september'],
            translations['month_october'], translations['month_november'], translations['month_december']
        ];

        // Tworzenie fragmentu dokumentu
        var fragment = $(document.createDocumentFragment());

        // Wyświetlenie panelu do zmiany języka
        fragment.append($('<div>').attr('id', 'language-panel')
            .append($('<select>').attr('id', 'change-lang')
                .append($('<option>').val('pl').text('Polski'))
                .append($('<option>').val('en').text('English'))
            .val(lang)));

        // Wyświetlanie nazwy miesiąca i roku oraz elementów nawigacyjnych
        fragment.append($('<div>').text("<").addClass('nav prev'));
        fragment.append($('<div>').text(monthName[month - 1] + " " + year).addClass('month-year'));
        fragment.append($('<div>').text(">").addClass('nav next'));

        // Tablica z nazwami dni tygodnia
        var dayName = [
            translations['day_monday'], translations['day_tuesday'], translations['day_wednesday'],
            translations['day_thursday'], translations['day_friday'], translations['day_saturday'],
            translations['day_sunday']
        ];

        // Wyświetlenie nazw dni tygodnia z odpowiednią klasą dla weekendu
        for (let i = 0; i <= 6; i++) {
            divDay = $('<div>').text(dayName[i]).addClass('day-name');
            if (i === 5) { divDay.addClass('saturday'); }
            if (i === 6) { divDay.addClass('sunday'); }
            fragment.append(divDay);
        }

        // Wyświetlenie pustych dni na początku miesiąca
        for (let i = 1; i < startDay; i++) {
            divDay = $('<div>').addClass('none');
            fragment.append(divDay);
        }

        // Wyświetlenie dni miesiąca
        for (let i = 1; i <= maxDay; i++) {
            var ii = i + startDay - 1;
            divDay = $('<div>').text(i);
            if (i === day && month === (new Date()).getMonth() + 1 && year === (new Date()).getFullYear()) {
                divDay.addClass('today');
            } else {
                if (ii % 7 === 0) { divDay.addClass('sunday'); }
                if (ii % 7 === 6) { divDay.addClass('saturday'); }
            }
            fragment.append(divDay);
        }

        // Dodanie całego fragmentu do elementu #calendar
        $("#calendar").empty().append(fragment);
    }

    // Onclick dla przycisków nawigujących kalendarzem
    $(document).on('click', '.nav', function() {
        if ($(this).hasClass('prev')) {
            month--;
            if (month < 1) {
                month = 12;
                year--;
            }
        } else if ($(this).hasClass('next')) {
            month++;
            if (month > 12) {
                month = 1;
                year++;
            }
        }
        generateCalendar(month, year);
    });

    // Onchange do zmiany tłumaczenia
    $(document).on('change', '#change-lang', function() {
        lang = $(this).val();
        changeTranslations(lang);
    });
});

Zobacz commit na GitHub.

Zakończenie

Implementacja dynamicznego ładowania tłumaczeń za pomocą AJAX znacząco podnosi funkcjonalność i dostępność naszego kalendarza. Dzięki wykorzystaniu technologii takich jak AJAX, JSON i API, nasza aplikacja staje się bardziej responsywna i przyjazna dla użytkownika, pozwalając na łatwe dostosowanie języka interfejsu w locie.

Podsumowując, dzięki integracji dynamicznych tłumaczeń nasza aplikacja kalendarza staje się bardziej wszechstronna i dostępna dla użytkowników z różnych części świata. Kontynuując rozwój projektu, możemy dodać jeszcze więcej funkcji i usprawnień, które sprawią, że nasz kalendarz będzie jeszcze bardziej funkcjonalny i przyjazny dla użytkownika.

Zobacz commit na GitHub i zapoznaj się z pełnym kodem projektu, aby dokładniej zrozumieć, jak wszystkie elementy współpracują ze sobą. Dziękuję za śledzenie cyklu artykułów o rozbudowie kalendarza w HTML, CSS i jQuery.

Linki do poprzednich wpisów:

Zapraszam do śledzenia kolejnych wpisów, w których będę omawiać kolejne zaawansowane techniki i narzędzia do tworzenia nowoczesnych aplikacji webowych.

Leave a Comment

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *


Scroll to Top