Przejdź do głównej treści
INF.03-01-24.06-SGCzerwiec 2024 · 32 kroki · ok. 68 min · 35 kryteriów z klucza

Rozwiązanie krok po kroku: Witryna o motocyklach (Nowoczesny kod (grid, PDO))

To drugi sposób rozwiązania tego samego arkusza. Wynik jest ten sam - baza motory z czterema zapytaniami, przeskalowane zdjęcie i strona motor.php z listą definicji wycieczek, licznikiem i motocyklem prześwitującym przez półprzezroczyste bloki - i tak samo spełnia każde kryterium klucza. Inna jest droga:

  • siatka CSS na całej stronie: grid-template-areas stawia blok lewy na dwa wiersze, a oba bloki prawe jeden pod drugim obok niego - wprost, a nie dzięki temu, że drugi blok „nie mieści się” w linii i opada; szerokości 75% i 15% stoją w grid-template-columns (klucz R.4.4 przyjmuje je wprost),
  • kolory w zmiennych CSS: dwa półprzezroczyste tła i kolor cienia zapisane raz, w :root,
  • PHP na PDO: oba zapytania na górze pliku, połączenie zamknięte, zanim zacznie się HTML, lista wypisywana pętlą foreach w szablonie,
  • bezpieczeństwo: escape() przy każdej wartości z bazy, rawurlencode() dla nazwy pliku w adresie, try/catch przy awarii bazy,
  • zapytanie 2 z aliasami tabel (w, z) - przy każdej kolumnie widać, z której tabeli pochodzi.

Na końcu krótkie porównanie z wersją podstawową (zakładka obok). Baza i grafiki pochodzą z prawdziwego archiwum pliki1, zapytania zostały wykonane na tej bazie w MariaDB z XAMPP-a, a podgląd strony od etapu PHP to wynik prawdziwego uruchomienia kodu z danego kroku w PHP 8.2.

Jak liczą się punkty. Klucz ma 35 kryteriów (R.1.1 - R.5.8), każde za 1 punkt. Próg zdania to 75%, czyli 27 punktów. Przy każdym kroku jest napisane, które kryteria spełnia.

  1. Etap 1: Baza danych

    Kroki 1-7 · po tym etapie masz 6 z 35 kryteriów (pkt)

    Krok 1: Baza danych motory

    ok. 2 min
    Polecenie z arkusza

    Utwórz bazę danych o nazwie motory, z zestawem polskich znaków (np. utf8_unicode_ci).

    Archiwum pliki1.zip rozpakuj hasłem ^moTocyKle^ (daszki na początku i na końcu, wielkość liter ma znaczenie) do folderu z numerem zdającego. W phpMyAdmin Nowa → motory, porównywanie utf8_unicode_ci, albo zapytaniem:

    CREATE DATABASE motory CHARACTER SET utf8 COLLATE utf8_unicode_ci;
    
    Typowe błędy

    Nazwa bazy to motory, nie motocykle - skrypty łączą się z bazą po nazwie (R.5.1).

  2. Krok 2: Import tabel z pliku baza.sql

    ok. 2 min
    Polecenie z arkusza

    Do bazy zaimportuj tabele z pliku baza.sql z rozpakowanego archiwum.

    Import → baza.sql. Dwie tabele w relacji 1..n (obraz 1): zdjecia (5 zdjęć; zrodlo to nazwa pliku bez rozszerzenia, np. droga1) i wycieczki (10 tras z kluczem obcym zdjecia_id). Jedno zdjęcie może ilustrować kilka wycieczek - stąd „jeden do wielu”.

  3. Krok 3: Zrzut ekranu import.png

    Kryteria:

    R.1.1 Wykonano import tabel do bazy danych motory czynność udokumentowano plikiem o nazwie import w formacie PNG

    ok. 2 min
    Polecenie z arkusza

    Wykonaj zrzut ekranu po imporcie. Zrzut zapisz w formacie PNG i nazwij import. Nie kadruj zrzutu. Powinien on obejmować cały ekran monitora, z widocznym paskiem zadań. Na zrzucie powinny być widoczne elementy wskazujące na poprawnie wykonany import tabel.

    Cały ekran z paskiem zadań. Uwaga na formaty w tym arkuszu: import w PNG (import.png), zrzuty zapytań w JPEG (kw1.jpg - kw4.jpg).

    Typowe błędy

    import.jpg zamiast import.png to strata R.1.1 - klucz podaje format wprost.

  4. Krok 4: Zapytanie 1: wycieczki z Muszyny i Wieliczki

    Kryteria: ,

    R.1.2 Zapisano plik kwerendy.txt zawierający co najmniej jedno zapytanie SQL, wynikające z treści zadania

    R.1.3 Wykonano zapytanie 1 wybierające jedynie nazwy wycieczek, których początek jest w Muszynie, Wieliczce. W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT nazwa FROM wycieczki WHERE poczatek="Muszyna" OR poczatek="Wieliczka"; oraz wynik działania udokumentowano zrzutem, na którym są widoczne dokładnie dwa rekordy: Dolina Popradu i Alter-Zakopianka

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 1: wybierające jedynie nazwy wycieczek, których początek jest w Muszynie, Wieliczce.

    IN (...) sprawdza, czy wartość jest na liście - to krótszy zapis dwóch warunków z OR (z przykładu w kluczu) i łatwiej go rozbudować o trzecie miasto. W bazie miasta są w mianowniku („Muszyna”), choć arkusz odmienia je w zdaniu. Wynik: Dolina Popradu i Alter-Zakopianka, jak w kluczu. Zapytanie zapisz w kwerendy.txt, zrzut jako kw1.jpg.

    Typowe błędy

    AND zamiast OR (albo zamiast IN) szuka trasy, która zaczyna się w dwóch miastach naraz - wynik pusty.

    kwerendy.txt Tekst nowy plik
    -- Zapytanie 1
    SELECT nazwa
    FROM wycieczki
    WHERE poczatek IN ('Muszyna', 'Wieliczka');
    Podgląd strony po tym kroku
    Podgląd strony po kroku 4
  5. Krok 5: Zapytanie 2: wycieczki ze źródłem zdjęcia

    Kryteria:

    R.1.4 Wykonano zapytanie 2 wybierające jedynie pola nazwa, opis i poczatek z tabeli wycieczki oraz odpowiadające im pole zrodlo z tabeli zdjecia . W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT nazwa, opis, poczatek, zrodlo FROM wycieczki JOIN zdjecia ON zdjecia_id = zdjecia.id; (możliwe również INNER JOIN lub bez JOIN i porównanie kluczy po WHERE) oraz wynik działania udokumentowano zrzutem, na którym jest widocznych dokładnie 10 rekordów, jedynie pola nazwa, opis, poczatek, zrodlo

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 2: wybierające jedynie pola nazwa, opis i poczatek z tabeli wycieczki oraz odpowiadające im pole zrodlo z tabeli zdjecia. Należy posłużyć się relacją.

    Aliasy tabel w (wycieczki) i z (zdjęcia): przy każdej kolumnie widać, skąd pochodzi, a ON z.id = w.zdjecia_id pokazuje parę kluczy relacji - klucz główny zdjęcia i klucz obcy wycieczki. Kolumna id jest w obu tabelach, więc bez przedrostka byłaby niejednoznaczna. Wynik: 10 wycieczek z czterema kolumnami. Zrzut: kw2.jpg. To zapytanie wyśle skrypt 1.

    kwerendy.txt Tekst zmienione: 5 linii
    -- Zapytanie 1
    SELECT nazwa
    FROM wycieczki
    WHERE poczatek IN ('Muszyna', 'Wieliczka');
    
    -- Zapytanie 2
    SELECT w.nazwa, w.opis, w.poczatek, z.zrodlo
    FROM wycieczki AS w
    JOIN zdjecia AS z ON z.id = w.zdjecia_id;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 5
  6. Krok 6: Zapytanie 3: liczba wycieczek

    Kryteria:

    R.1.5 Wykonano zapytanie 3 zliczające liczbę wycieczek wpisanych do tabeli wycieczki . W pliku z kwerendami lub na zrzucie istnieje zapis np.: SELECT COUNT(*) FROM wycieczki; (lub dowolne pole zamiast *) oraz wynik działania udokumentowano zrzutem, na którym jest widoczna wartość 10

    ok. 1 min
    Polecenie z arkusza

    Zapytanie 3: zliczające liczbę wycieczek wpisanych do tabeli wycieczki.

    COUNT(*) liczy wiersze tabeli; alias liczba_wycieczek nadaje kolumnie wyniku czytelną nazwę zamiast COUNT(*). Wynik: 10. Zrzut: kw3.jpg. To zapytanie wyśle skrypt 2.

    kwerendy.txt Tekst zmienione: 4 linie
    -- Zapytanie 1
    SELECT nazwa
    FROM wycieczki
    WHERE poczatek IN ('Muszyna', 'Wieliczka');
    
    -- Zapytanie 2
    SELECT w.nazwa, w.opis, w.poczatek, z.zrodlo
    FROM wycieczki AS w
    JOIN zdjecia AS z ON z.id = w.zdjecia_id;
    
    -- Zapytanie 3
    SELECT COUNT(*) AS liczba_wycieczek
    FROM wycieczki;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 6
  7. Krok 7: Zapytanie 4: nowa kolumna ocena

    Kryteria:

    R.1.6 Wykonano zapytanie 4 modyfikujące strukturę tabeli wycieczki . Dodana jest kolumna ocena typu całkowitego. W pliku z kwerendami lub na zrzucie istnieje zapis np.: ALTER TABLE wycieczki ADD COLUMN ocena INT; (słowo COLUMN jest opcjonalne, dowolny typ całkowity) oraz wynik działania udokumentowano zrzutem, na którym jest widoczny komunikat o poprawnym wykonaniu zapytania lub struktura tabeli z dodanym polem ocena typu całkowitego

    ok. 2 min
    Polecenie z arkusza

    Zapytanie 4: modyfikujące strukturę tabeli wycieczki, poprzez dodanie kolumny ocena typu całkowitego.

    Strukturę zmienia ALTER TABLE ... ADD COLUMN (UPDATE zmienia tylko dane). Typ TINYINT UNSIGNED - liczba całkowita od 0 do 255 w jednym bajcie: na ocenę w rodzaju 1-5 wystarczy z zapasem, a klucz przyjmuje dowolny typ całkowity. Zapytanie nie zwraca wierszy; zrzut kw4.jpg pokazuje komunikat albo strukturę tabeli z nową kolumną.

    Typowe błędy

    Drugie wykonanie kończy się błędem „Duplicate column name” - kolumna już istnieje.

    kwerendy.txt Tekst zmienione: 4 linie
    -- Zapytanie 1
    SELECT nazwa
    FROM wycieczki
    WHERE poczatek IN ('Muszyna', 'Wieliczka');
    
    -- Zapytanie 2
    SELECT w.nazwa, w.opis, w.poczatek, z.zrodlo
    FROM wycieczki AS w
    JOIN zdjecia AS z ON z.id = w.zdjecia_id;
    
    -- Zapytanie 3
    SELECT COUNT(*) AS liczba_wycieczek
    FROM wycieczki;
    
    -- Zapytanie 4
    ALTER TABLE wycieczki
    ADD COLUMN ocena TINYINT UNSIGNED;
    Podgląd strony po tym kroku
    Podgląd strony po kroku 7
  8. Etap 2: Grafika i strona HTML

    Kroki 8-15 · po tym etapie masz 14 z 35 kryteriów (pkt)

    Krok 8: droga1.jpg: szerokość 640 px z zachowaniem proporcji

    Kryteria:

    R.2.1 Plik droga1.jpg przeskalowano z zachowaniem proporcji do szerokości 640 px, odpowiadająca temu wysokość wynosi 480 px (± 5 px)

    ok. 2 min
    Polecenie z arkusza

    Przygotowanie grafiki: Plik droga1.jpg, wypakowany z archiwum, należy przeskalować z zachowaniem proporcji tak, aby jego szerokość wynosiła dokładnie 640 px.

    Zdjęcie ma 1000 × 750 px. W GIMP-ie Obraz → Skaluj obraz, łańcuch proporcji zamknięty, szerokość 640 - wysokość 750 × 640 / 1000 = 480 px (R.2.1 sprawdza 480 ± 5). Eksportuj jako droga1.jpg, nadpisując plik.

    Typowe błędy

    Otwarty łańcuch i 640 × 640 zniekształca zdjęcie.

    Podgląd strony po tym kroku
    Podgląd strony po kroku 8
  9. Krok 9: Plik motor.php: HTML5, język, kodowanie

    Kryteria:

    R.2.2 Witrynę internetową zapisano w pliku motor.php, oraz zapisano jawnie standard kodowania polskich znaków i deklarację HTML5 <!DOCTYPE HTML> oraz zadeklarowano język witryny polski np.<HTML lang="pl">. Strona zawiera sekcje head i body

    ok. 2 min
    Polecenie z arkusza

    Cechy witryny: Składa się ze strony o nazwie motor.php zapisanej w języku HTML5. Ustawiony język zawartości strony na polski. Jawnie zastosowany właściwy standard kodowania polskich znaków.

    Plik motor.php w folderze z rozpakowanymi plikami: <!DOCTYPE html>, <html lang="pl">, <head> z <meta charset="UTF-8"> jako pierwszym elementem i <body> (R.2.2 wymaga obu sekcji).

    motor.php PHP nowy plik
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  10. Krok 10: Tytuł strony i arkusz stylów

    Kryteria: ,

    R.2.3 Nadano tytuł stronie: "Motocykle"

    R.3.3 Strona zawiera działające połączenie z zewnętrznym arkuszem stylów o nazwie styl.css , formatowanie CSS pochodzi jedynie z tego arkusza Uwaga: kryterium to nie jest spełnione również, gdy zdający źle przypisał co najmniej jeden selektor do bloku (np. kropka zamiast #)

    ok. 1 min
    Polecenie z arkusza

    Tytuł strony widoczny na karcie przeglądarki: „Motocykle”. Arkusz stylów w pliku o nazwie styl.css prawidłowo połączony z kodem strony.

    <title> (R.2.3), <link> do styl.css (R.3.3) i viewport, żeby telefon nie pomniejszał strony.

    motor.php PHP zmienione: 3 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  11. Krok 11: Obraz motor.png poza blokami

    Kryteria:

    R.2.8 W sekcji body, poza blokami umieszczono obraz moto r.png wraz z tekstem alternatywnym „motocykl”, sprawdzić w kodzie źródłowym

    ok. 1 min
    Polecenie z arkusza

    Umieszczony poza blokami obraz motor.png o tekście alternatywnym „motocykl”.

    Obraz stoi bezpośrednio w <body>, przed blokami - nie należy do żadnego z nich (R.2.8). Identyfikator bike pozwoli nadać styl „jedynie dla obrazu motor.png”. Na razie zajmuje pełne 1280 px; pod bloki przeniesie go CSS.

    motor.php PHP zmienione: 1 linia
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  12. Krok 12: Bloki semantyczne

    Kryteria:

    R.2.4 Układ strony zdefiniowano dzieląc ją na bloki: baner, lewy, dwa prawe oraz stopkę. Zastosowano semantyczne znaczniki sekcji HTML5 zgodnie z ich znaczeniem, w tym do części nagłówkowej - header, stopki - footer, dopuszcza się zagnieżdżenie bloków div w znacznikach semantycznych

    ok. 2 min
    Polecenie z arkusza

    Podział strony na bloki: na górze baner, poniżej trzy bloki: lewy, i dwa prawe, na dole blok stopki. Podział zrealizowany wyłącznie za pomocą semantycznych znaczników sekcji języka HTML5 tak, aby po uruchomieniu w przeglądarce wygląd układu bloków był zgodny z obrazem 2.

    Baner to <header>, blok lewy z główną treścią (lista wycieczek) - <main>, dwa bloki prawe z dodatkami - dwa <aside>, stopka - <footer> (R.2.4). Identyfikatory buy i stats (od treści, nie od położenia) rozróżnią bloki prawe w siatce.

    motor.php PHP zmienione: 10 linii
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
        <header>
        </header>
        <main>
        </main>
        <aside id="buy">
        </aside>
        <aside id="stats">
        </aside>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  13. Krok 13: Baner i blok lewy: nagłówki, lista definicji

    ok. 2 min
    Polecenie z arkusza

    Zawartość banera: nagłówek pierwszego stopnia o treści „Motocykle - moja pasja”. Zawartość bloku lewego: Nagłówek drugiego stopnia o treści: „Gdzie pojechać?”. Lista definicji (Description List) wypełniona przez skrypt 1.

    <h1> w banerze, <h2> i pusta lista definicji <dl> w bloku lewym. W niej staną pary: termin <dt> (nazwa i początek trasy) i opis <dd> - wypisze je skrypt 1.

    motor.php PHP zmienione: 4 linie
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
        <header>
            <h1>Motocykle - moja pasja</h1>
        </header>
        <main>
            <h2>Gdzie pojechać?</h2>
            <dl>
            </dl>
        </main>
        <aside id="buy">
        </aside>
        <aside id="stats">
        </aside>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  14. Krok 14: Pierwszy blok prawy: lista numerowana

    Kryteria:

    R.2.7 W pierwszym bloku prawym umieszczono jedną listę numerowaną z pięcioma elementami, zastosowano znaczniki <ol>, <li>

    ok. 2 min
    Polecenie z arkusza

    Zawartość pierwszego bloku prawego: Nagłówek drugiego stopnia o treści: „Co kupić?”. Lista numerowana (uporządkowana) o 5 elementach: „Honda CBR125R”, „Yamaha YBR125”, „Honda VFR800i”, „Honda CBR1100XX”, „BMW R1200GS LC”.

    Numerowana = <ol>, pięć <li> w kolejności z arkusza (R.2.7).

    motor.php PHP zmienione: 8 linii
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
        <header>
            <h1>Motocykle - moja pasja</h1>
        </header>
        <main>
            <h2>Gdzie pojechać?</h2>
            <dl>
            </dl>
        </main>
        <aside id="buy">
            <h2>Co kupić?</h2>
            <ol>
                <li>Honda CBR125R</li>
                <li>Yamaha YBR125</li>
                <li>Honda VFR800i</li>
                <li>Honda CBR1100XX</li>
                <li>BMW R1200GS LC</li>
            </ol>
        </aside>
        <aside id="stats">
        </aside>
        <footer>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  15. Krok 15: Drugi blok prawy i stopka

    Kryteria:

    R.2.5 W banerze zapisano nagłówek h1: "Motocykle - moja pasja", w bloku lewym h2: "Gdzie pojechać?", w blokach prawych h2: "Co kupić?" i "Statystyki" oraz zapisano w drugim bloku prawym trzy paragrafy i w stopce paragraf z numerem zdającego

    ok. 2 min
    Polecenie z arkusza

    Zawartość drugiego bloku prawego: Nagłówek drugiego stopnia o treści: „Statystyki”, paragraf (akapit) o treści „Wpisanych wycieczek: ”, dalej efekt działania skryptu 2, paragraf o treści „Użytkowników forum: 200”, paragraf o treści „Przesłanych zdjęć: 1300”. Zawartość stopki: akapit o treści „Stronę wykonał: ”, dalej wstawiony numer zdającego.

    Trzy akapity; w pierwszym po dwukropku liczbę wstawi skrypt 2. W stopce akapit: na egzaminie po „Stronę wykonał:” wpisujesz swój numer zdającego (PESEL), tu stoi autor rozwiązania. Komplet nagłówków i akapitów to R.2.5.

    motor.php PHP zmienione: 5 linii
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
        <header>
            <h1>Motocykle - moja pasja</h1>
        </header>
        <main>
            <h2>Gdzie pojechać?</h2>
            <dl>
            </dl>
        </main>
        <aside id="buy">
            <h2>Co kupić?</h2>
            <ol>
                <li>Honda CBR125R</li>
                <li>Yamaha YBR125</li>
                <li>Honda VFR800i</li>
                <li>Honda CBR1100XX</li>
                <li>BMW R1200GS LC</li>
            </ol>
        </aside>
        <aside id="stats">
            <h2>Statystyki</h2>
            <p>Wpisanych wycieczek: </p>
            <p>Użytkowników forum: 200</p>
            <p>Przesłanych zdjęć: 1300</p>
        </aside>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  16. Etap 3: Styl CSS

    Kroki 16-23 · po tym etapie masz 25 z 35 kryteriów (pkt)

    Krok 16: Plik styl.css: zmienne i krój czcionki

    Kryteria:

    R.4.1 Ustawiono domyślne formatowanie wszystkich selektorów (dla *): krój czcionki Verdana

    ok. 1 min
    Polecenie z arkusza

    Domyślne formatowanie wszystkich selektorów: krój czcionki Verdana.

    Dwa półprzezroczyste tła i kolor cienia z arkusza zapisane raz, w zmiennych CSS na :root - każde występuje w dwóch albo trzech blokach. Selektor * z Verdaną i zapasowym krojem bezszeryfowym (R.4.1).

    styl.css CSS nowy plik
    :root {
        --banner-bg: rgba(60, 120, 160, 0.9);
        --block-bg: rgba(0, 100, 0, 0.6);
        --shadow: DimGray;
    }
    
    * {
        font-family: Verdana, sans-serif;
    }
    Podgląd strony po tym kroku (motor.php)
  17. Krok 17: Obraz motocykla pod blokami

    Kryteria: ,

    R.4.4 Ustawiono szerokość: bloku lewego 75%, obu prawych 15% (width lub grid-template- columns lub flex) oraz jedynie dla obrazu motor.png 70%

    R.4.8 Dla obrazu ustawiono właściwości display: block; position: absolute; left: 15% oraz obraz znajduje się za blokami strony

    ok. 3 min
    Polecenie z arkusza

    Jedynie dla obrazu motor.png: wyświetlanie blokowe, pozycjonowanie na stronie: element przesunięty 15% od lewej strony względem swojego rodzica, szerokość 70%, element jest zakryty przez bloki strony (znajduje się za blokami strony).

    Tu nie ma wyboru techniki: klucz R.4.8 ocenia wprost display: block; position: absolute; left: 15%.

    • position: absolute wyjmuje obraz z układu - siatka, którą za chwilę dostanie <body>, nie zrobi mu miejsca ani komórki. left: 15% i width: 70% liczą się od bloku zawierającego: najbliższego przodka z ustawionym position, a gdy takiego nie ma - od okna. Arkusz upraszcza to do „względem rodzica”,
    • z-index: -1 kładzie obraz pod bloki (R.4.8).

    Selektor #bike dotyczy tylko tego obrazu (R.4.4 - szerokość 70% „jedynie dla obrazu”).

    styl.css CSS zmienione: 8 linii
    :root {
        --banner-bg: rgba(60, 120, 160, 0.9);
        --block-bg: rgba(0, 100, 0, 0.6);
        --shadow: DimGray;
    }
    
    * {
        font-family: Verdana, sans-serif;
    }
    
    #bike {
        display: block;
        position: absolute;
        left: 15%;
        width: 70%;
        z-index: -1;
    }
    Podgląd strony po tym kroku (motor.php)
  18. Krok 18: Układ bloków na siatce

    Kryteria:

    R.3.1 Układ bloków po uruchomieniu strony w przeglądarce jest zgodny z obrazem 2 w arkuszu (prawidłowo zastosowano właściwości CSS decydujące o układzie strony np. float i clear albo display flex albo grid)

    ok. 3 min
    Polecenie z arkusza

    Podział zrealizowany wyłącznie za pomocą semantycznych znaczników sekcji języka HTML5 tak, aby po uruchomieniu w przeglądarce wygląd układu bloków był zgodny z obrazem 2.

    <body> staje się siatką. grid-template-areas rysuje obraz 2 słowami: baner na dwie kolumny, potem dwa wiersze, w których main zajmuje lewą kolumnę w obu (blok lewy jest wysoki na dwa bloki prawe), a buy i stats stoją po prawej jeden pod drugim, na dole stopka (R.3.1). Każdy blok trafia w swoje pole przez grid-area.

    grid-template-columns: 75% 15% 1fr - pierwsze dwie kolumny to szerokości bloku lewego i prawych (klucz R.4.4 przyjmuje je w tej postaci), a trzecia, 1fr, zabiera resztę szerokości. Dzięki niej baner i stopka rozciągają się na całą stronę (w ich wierszach nazwa pola powtarza się trzy razy), a kropka w pozostałych wierszach oznacza puste pole. align-items: start - bloki nie rozciągają się do wysokości wiersza, tylko mają swoje 600 i 300 px. W wersji podstawowej drugi blok prawy trafiał pod pierwszy dlatego, że nie mieścił się w linii obok bloku lewego; tu jego miejsce jest zapisane wprost i nie zależy od sumy szerokości.

    styl.css CSS zmienione: 31 linii
    :root {
        --banner-bg: rgba(60, 120, 160, 0.9);
        --block-bg: rgba(0, 100, 0, 0.6);
        --shadow: DimGray;
    }
    
    * {
        font-family: Verdana, sans-serif;
    }
    
    #bike {
        display: block;
        position: absolute;
        left: 15%;
        width: 70%;
        z-index: -1;
    }
    
    body {
        display: grid;
        grid-template-columns: 75% 15% 1fr;
        grid-template-areas:
            "header header header"
            "main   buy    ."
            "main   stats  ."
            "footer footer footer";
        align-items: start;
    }
    
    header {
        grid-area: header;
    }
    
    main {
        grid-area: main;
    }
    
    #buy {
        grid-area: buy;
    }
    
    #stats {
        grid-area: stats;
    }
    
    footer {
        grid-area: footer;
    }
    Podgląd strony po tym kroku (motor.php)
  19. Krok 19: Baner: tło rgba, biały tekst, rozmiar, odstęp

    ok. 2 min
    Polecenie z arkusza

    Dla banera: kolor tła RGB z wartościami 60, 120, 160 i przezroczystością 0.9; biały kolor czcionki, rozmiar czcionki 150%, marginesy wewnętrzne 10 px.

    rgba(60, 120, 160, 0.9) - trzy składowe koloru i krycie 0.9: przez tło baneru prześwituje kawałek motocykla, a tekst zostaje w pełni biały (opacity zrobiłby przezroczystym cały blok razem z tekstem). Klucz R.4.2 podaje zapis rgba(...), więc zostaje on, choć nowa składnia CSS pozwala pisać rgb(60 120 160 / 0.9).

    styl.css CSS zmienione: 4 linie
    :root {
        --banner-bg: rgba(60, 120, 160, 0.9);
        --block-bg: rgba(0, 100, 0, 0.6);
        --shadow: DimGray;
    }
    
    * {
        font-family: Verdana, sans-serif;
    }
    
    #bike {
        display: block;
        position: absolute;
        left: 15%;
        width: 70%;
        z-index: -1;
    }
    
    body {
        display: grid;
        grid-template-columns: 75% 15% 1fr;
        grid-template-areas:
            "header header header"
            "main   buy    ."
            "main   stats  ."
            "footer footer footer";
        align-items: start;
    }
    
    header {
        grid-area: header;
        background-color: var(--banner-bg);
        color: white;
        font-size: 150%;
        padding: 10px;
    }
    
    main {
        grid-area: main;
    }
    
    #buy {
        grid-area: buy;
    }
    
    #stats {
        grid-area: stats;
    }
    
    footer {
        grid-area: footer;
    }
    Podgląd strony po tym kroku (motor.php)
  20. Krok 20: Blok lewy: tło, wymiary, przewijanie, cień

    Kryteria: , ,

    R.4.3 Ustawiono wysokość bloku lewego na 600px, obu prawych na 300px

    R.4.7 Ustawiono dla bloku lewego automatyczne paski przewijania (overflow: auto lub scroll) oraz rozmiar czcionki 150% dla banera i 120% dla bloku lewego

    R.4.9 Ustawiono przynajmniej dla jednego bloku (lewy, lub dowolny prawy) cień bloku o przesunięciu 10 px w obu osiach, rozmyciu 7 px i kolorze DimGray (box-shadow: 10px 10px 7px DimGray;)

    ok. 3 min
    Polecenie z arkusza

    Dla bloku lewego: kolor tła RGB z wartościami 0, 100, 0 i przezroczystością 0.6; marginesy zewnętrzne 2%, szerokość 75%, wysokość 600 px, rozmiar czcionki 120%, paski przewijania widoczne tylko gdy zawartość nie mieści się w bloku, cień bloku o przesunięciu 10 px w obu osiach, rozmyciu 7 px i kolorze DimGray.

    • overflow: auto - pasek przewijania tylko wtedy, gdy dziesięć opisów nie zmieści się w 600 px (R.4.7),
    • box-shadow: 10px 10px 7px - przesunięcie w poziomie, w pionie, rozmycie, kolor (R.4.9),
    • margin: 2% - uwaga na siatkę: procentowy margines elementu siatki liczy się od szerokości jego pola (tu kolumny 75%), a nie całej strony, więc odstęp wychodzi trochę mniejszy niż w wersji podstawowej. Zapis i kryterium R.4.6 się nie zmieniają.

    Szerokość 75% daje kolumna siatki - dodatkowe width: 75% liczyłoby się od kolumny i zwęziło blok do 3/4 z 3/4.

    styl.css CSS zmienione: 6 linii
    :root {
        --banner-bg: rgba(60, 120, 160, 0.9);
        --block-bg: rgba(0, 100, 0, 0.6);
        --shadow: DimGray;
    }
    
    * {
        font-family: Verdana, sans-serif;
    }
    
    #bike {
        display: block;
        position: absolute;
        left: 15%;
        width: 70%;
        z-index: -1;
    }
    
    body {
        display: grid;
        grid-template-columns: 75% 15% 1fr;
        grid-template-areas:
            "header header header"
            "main   buy    ."
            "main   stats  ."
            "footer footer footer";
        align-items: start;
    }
    
    header {
        grid-area: header;
        background-color: var(--banner-bg);
        color: white;
        font-size: 150%;
        padding: 10px;
    }
    
    main {
        grid-area: main;
        background-color: var(--block-bg);
        margin: 2%;
        height: 600px;
        font-size: 120%;
        overflow: auto;
        box-shadow: 10px 10px 7px var(--shadow);
    }
    
    #buy {
        grid-area: buy;
    }
    
    #stats {
        grid-area: stats;
    }
    
    footer {
        grid-area: footer;
    }
    Podgląd strony po tym kroku (motor.php)
  21. Krok 21: Bloki prawe: wspólny wygląd

    ok. 2 min
    Polecenie z arkusza

    Wspólne dla bloków prawych: kolor tła RGB z wartościami 0, 100, 0 i przezroczystością 0.6; marginesy zewnętrzne 1%, szerokość 15%, wysokość 300 px, cień bloku o przesunięciu 10 px w obu osiach, rozmyciu 7 px i kolorze DimGray.

    Selektor aside obejmuje oba bloki prawe naraz - tak „wspólne” przekłada się na CSS; identyfikatory służą tylko do wskazania pola siatki. Szerokość 15% daje druga kolumna siatki. Oba bloki mają 300 px, a z marginesami zajmują razem tyle, ile blok lewy obok - jak na obrazie 2.

    styl.css CSS zmienione: 7 linii
    :root {
        --banner-bg: rgba(60, 120, 160, 0.9);
        --block-bg: rgba(0, 100, 0, 0.6);
        --shadow: DimGray;
    }
    
    * {
        font-family: Verdana, sans-serif;
    }
    
    #bike {
        display: block;
        position: absolute;
        left: 15%;
        width: 70%;
        z-index: -1;
    }
    
    body {
        display: grid;
        grid-template-columns: 75% 15% 1fr;
        grid-template-areas:
            "header header header"
            "main   buy    ."
            "main   stats  ."
            "footer footer footer";
        align-items: start;
    }
    
    header {
        grid-area: header;
        background-color: var(--banner-bg);
        color: white;
        font-size: 150%;
        padding: 10px;
    }
    
    main {
        grid-area: main;
        background-color: var(--block-bg);
        margin: 2%;
        height: 600px;
        font-size: 120%;
        overflow: auto;
        box-shadow: 10px 10px 7px var(--shadow);
    }
    
    aside {
        background-color: var(--block-bg);
        margin: 1%;
        height: 300px;
        box-shadow: 10px 10px 7px var(--shadow);
    }
    
    #buy {
        grid-area: buy;
    }
    
    #stats {
        grid-area: stats;
    }
    
    footer {
        grid-area: footer;
    }
    Podgląd strony po tym kroku (motor.php)
  22. Krok 22: Stopka

    Kryteria: ,

    R.4.2 Ustawiono kolor tła rgba(60, 120, 160, 0.9) dla banera i stopki, rgba(0, 100, 0, 0.6) dla bloków lewego i obu prawych oraz biały kolor czcionki dla banera, stopki, odnośników, lub kolor tła bloków ustawiony za pomocą rgb i opacity

    R.4.6 Ustawiono marginesy wewnętrzne (padding) 10px dla banera i stopki oraz marginesy zewnętrzne (margin) 2% dla bloku lewego, 1% dla obu bloków prawych

    ok. 1 min
    Polecenie z arkusza

    Dla stopki: kolor tła RGB z wartościami 60, 120, 160 i przezroczystością 0.9; biały kolor czcionki, marginesy wewnętrzne 10 px.

    Te same zmienne co baner, bez powiększenia czcionki. Tła i biały tekst bloków to R.4.2, marginesy wewnętrzne baneru i stopki oraz zewnętrzne bloków - R.4.6, wysokości 600 i 300 px - R.4.3.

    styl.css CSS zmienione: 3 linie
    :root {
        --banner-bg: rgba(60, 120, 160, 0.9);
        --block-bg: rgba(0, 100, 0, 0.6);
        --shadow: DimGray;
    }
    
    * {
        font-family: Verdana, sans-serif;
    }
    
    #bike {
        display: block;
        position: absolute;
        left: 15%;
        width: 70%;
        z-index: -1;
    }
    
    body {
        display: grid;
        grid-template-columns: 75% 15% 1fr;
        grid-template-areas:
            "header header header"
            "main   buy    ."
            "main   stats  ."
            "footer footer footer";
        align-items: start;
    }
    
    header {
        grid-area: header;
        background-color: var(--banner-bg);
        color: white;
        font-size: 150%;
        padding: 10px;
    }
    
    main {
        grid-area: main;
        background-color: var(--block-bg);
        margin: 2%;
        height: 600px;
        font-size: 120%;
        overflow: auto;
        box-shadow: 10px 10px 7px var(--shadow);
    }
    
    aside {
        background-color: var(--block-bg);
        margin: 1%;
        height: 300px;
        box-shadow: 10px 10px 7px var(--shadow);
    }
    
    #buy {
        grid-area: buy;
    }
    
    #stats {
        grid-area: stats;
    }
    
    footer {
        grid-area: footer;
        background-color: var(--banner-bg);
        color: white;
        padding: 10px;
    }
    Podgląd strony po tym kroku (motor.php)
  23. Krok 23: Nagłówki i lista definicji

    Kryteria: ,

    R.4.5 Ustawiono dla selektorów h1 i h2 wyrównanie tekstu do środka i czcionkę pochyloną

    R.4.10 Ustawiono dla terminu w liście definicji biały kolor czcionki, padding: 1% dla elementu definicji w liście oraz gdy kursor znajdzie się na elemencie definicji, jego kolor tła zmienia się na LightGreen

    ok. 2 min
    Polecenie z arkusza

    Dla selektora nagłówka pierwszego i drugiego stopnia: wyrównanie tekstu do środka, czcionka pochylona. Wspólne dla odnośnika i terminu w liście definicji: biały kolor czcionki. Dla elementu definicji w liście: marginesy wewnętrzne 1%. Gdy kursor znajdzie się na elemencie definicji w liście, jego kolor tła zmienia się na LightGreen (widoczne na obrazie 2).

    Arkusz każe użyć selektorów znaczników h1, h2 (R.4.5 nie uzna klasy). a, dt - odnośniki i terminy białe, dd z marginesem wewnętrznym 1%, a dd:hover podświetla opis pod kursorem (R.4.10). Lista jest jeszcze pusta - wypełni ją skrypt 1.

    styl.css CSS zmienione: 19 linii
    :root {
        --banner-bg: rgba(60, 120, 160, 0.9);
        --block-bg: rgba(0, 100, 0, 0.6);
        --shadow: DimGray;
    }
    
    * {
        font-family: Verdana, sans-serif;
    }
    
    #bike {
        display: block;
        position: absolute;
        left: 15%;
        width: 70%;
        z-index: -1;
    }
    
    body {
        display: grid;
        grid-template-columns: 75% 15% 1fr;
        grid-template-areas:
            "header header header"
            "main   buy    ."
            "main   stats  ."
            "footer footer footer";
        align-items: start;
    }
    
    header {
        grid-area: header;
        background-color: var(--banner-bg);
        color: white;
        font-size: 150%;
        padding: 10px;
    }
    
    main {
        grid-area: main;
        background-color: var(--block-bg);
        margin: 2%;
        height: 600px;
        font-size: 120%;
        overflow: auto;
        box-shadow: 10px 10px 7px var(--shadow);
    }
    
    aside {
        background-color: var(--block-bg);
        margin: 1%;
        height: 300px;
        box-shadow: 10px 10px 7px var(--shadow);
    }
    
    #buy {
        grid-area: buy;
    }
    
    #stats {
        grid-area: stats;
    }
    
    footer {
        grid-area: footer;
        background-color: var(--banner-bg);
        color: white;
        padding: 10px;
    }
    
    h1,
    h2 {
        text-align: center;
        font-style: italic;
    }
    
    a,
    dt {
        color: white;
    }
    
    dd {
        padding: 1%;
    }
    
    dd:hover {
        background-color: LightGreen;
    }
    Podgląd strony po tym kroku (motor.php)
  24. Etap 4: Skrypty PHP

    Kroki 24-30 · po tym etapie masz 35 z 35 kryteriów (pkt)

    Krok 24: Połączenie PDO na górze pliku

    Kryteria: ,

    R.5.1 Skrypt zawiera instrukcje połączenia z serwerem bazy danych i wyboru bazy motory

    R.5.2 Ostatnią operacją na bazie jest jej zamknięcie

    ok. 4 min
    Polecenie z arkusza

    Napisane w języku PHP. Należy stosować znaczące nazewnictwo wszystkich zmiennych lub funkcji. Skrypty łączą się z serwerem bazodanowym na localhost, użytkownik root bez hasła, baza danych o nazwie motory. Na końcu jest zamykane połączenie z serwerem.

    Praca z bazą stoi przed <!DOCTYPE html>: najpierw dane, potem HTML, który je tylko wypisuje. Połączenie przez PDO (klucz R.5 przyjmuje MySQLi i PDO):

    • DSN 'mysql:host=localhost;dbname=motory;charset=utf8mb4', użytkownik root, puste hasło (R.5.1),
    • ERRMODE_EXCEPTION - błąd bazy rzuca wyjątek, FETCH_ASSOC - wiersze z nazwami kolumn,
    • $pdo = null zamyka połączenie; oba skrypty trafią przed tę linię, więc zamknięcie zostanie ostatnią operacją na bazie (R.5.2),
    • try / catch - przy awarii bazy szczegóły idą do dziennika serwera, a strona dostaje ogólny komunikat,
    • escape() - htmlspecialchars(..., ENT_QUOTES) pod nazwą, która mówi, co robi.

    Zmienne mają wartości początkowe, żeby szablon działał także po błędzie. Od tego kroku podgląd to wynik prawdziwego uruchomienia.

    motor.php PHP zmienione: 21 linii
    <?php
    function escape(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $trips = [];
    $tripCount = 0;
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=motory;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wycieczek. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
        <header>
            <h1>Motocykle - moja pasja</h1>
        </header>
        <main>
            <h2>Gdzie pojechać?</h2>
            <dl>
            </dl>
        </main>
        <aside id="buy">
            <h2>Co kupić?</h2>
            <ol>
                <li>Honda CBR125R</li>
                <li>Yamaha YBR125</li>
                <li>Honda VFR800i</li>
                <li>Honda CBR1100XX</li>
                <li>BMW R1200GS LC</li>
            </ol>
        </aside>
        <aside id="stats">
            <h2>Statystyki</h2>
            <p>Wpisanych wycieczek: </p>
            <p>Użytkowników forum: 200</p>
            <p>Przesłanych zdjęć: 1300</p>
        </aside>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  25. Krok 25: Skrypt 1: zapytanie 2

    Kryteria: ,

    R.5.3 Skrypt 1 zawiera instrukcje wysyłające do bazy danych zapytanie 2 lub skrypt 2: zapytanie 3

    R.5.8 Zastosowano znaczące nazewnictwo dla wszystkich zmiennych lub funkcji w języku polskim lub angielskim. Nazwy odzwierciedlają cel zastosowania

    ok. 2 min
    Polecenie z arkusza

    Skrypt 1: Wysyła do bazy danych zapytanie 2.

    $pdo->query(...) wysyła zapytanie 2 - te same linie co w kwerendy.txt - a ->fetchAll() odbiera wszystkie 10 wycieczek do tablicy $trips (R.5.3). W zapytaniu nie ma wartości od użytkownika, więc query wystarcza. Nazwy zmiennych po angielsku mówią, co przechowują (R.5.8).

    motor.php PHP zmienione: 8 linii
    <?php
    function escape(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $trips = [];
    $tripCount = 0;
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=motory;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // Skrypt 1: wycieczki ze zdjęciami
        $trips = $pdo->query(
            'SELECT w.nazwa, w.opis, w.poczatek, z.zrodlo
             FROM wycieczki AS w
             JOIN zdjecia AS z ON z.id = w.zdjecia_id'
        )->fetchAll();
    
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wycieczek. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
        <header>
            <h1>Motocykle - moja pasja</h1>
        </header>
        <main>
            <h2>Gdzie pojechać?</h2>
            <dl>
            </dl>
        </main>
        <aside id="buy">
            <h2>Co kupić?</h2>
            <ol>
                <li>Honda CBR125R</li>
                <li>Yamaha YBR125</li>
                <li>Honda VFR800i</li>
                <li>Honda CBR1100XX</li>
                <li>BMW R1200GS LC</li>
            </ol>
        </aside>
        <aside id="stats">
            <h2>Statystyki</h2>
            <p>Wpisanych wycieczek: </p>
            <p>Użytkowników forum: 200</p>
            <p>Przesłanych zdjęć: 1300</p>
        </aside>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>

    Podgląd strony bez zmian - taki jak po kroku 24.

  26. Krok 26: Skrypt 1: termin z nazwą i początkiem trasy

    Kryteria:

    R.5.4 Skrypt 1 wyświetla tekst: „<nazwa>, rozpoczyna się w <poczatek>,”, gdzie wartości w <> są pobrane z bazy danych

    ok. 3 min
    Polecenie z arkusza

    Zwrócone zapytaniem wartości są wyświetlane jako poszczególne terminy i elementy definicji listy według wzoru: Termin listy: „<nazwa>, rozpoczyna się w <poczatek>, ”.

    W <dl> pętla foreach ($trips as $trip): w składni alternatywnej - HTML terminu zostaje zwykłym HTML-em, a wartości wchodzą przez <?= escape(...) ?> (skrót od <?php echo ... ?>). Wzór z arkusza łącznie z przecinkiem i spacją na końcu - za nimi dojdzie odnośnik (R.5.4). Podgląd: dziesięć terminów.

    motor.php PHP zmienione: 3 linie
    <?php
    function escape(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $trips = [];
    $tripCount = 0;
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=motory;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // Skrypt 1: wycieczki ze zdjęciami
        $trips = $pdo->query(
            'SELECT w.nazwa, w.opis, w.poczatek, z.zrodlo
             FROM wycieczki AS w
             JOIN zdjecia AS z ON z.id = w.zdjecia_id'
        )->fetchAll();
    
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wycieczek. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
        <header>
            <h1>Motocykle - moja pasja</h1>
        </header>
        <main>
            <h2>Gdzie pojechać?</h2>
            <dl>
                <?php foreach ($trips as $trip): ?>
                    <dt><?= escape($trip['nazwa']) ?>, rozpoczyna się w <?= escape($trip['poczatek']) ?>, </dt>
                <?php endforeach; ?>
            </dl>
        </main>
        <aside id="buy">
            <h2>Co kupić?</h2>
            <ol>
                <li>Honda CBR125R</li>
                <li>Yamaha YBR125</li>
                <li>Honda VFR800i</li>
                <li>Honda CBR1100XX</li>
                <li>BMW R1200GS LC</li>
            </ol>
        </aside>
        <aside id="stats">
            <h2>Statystyki</h2>
            <p>Wpisanych wycieczek: </p>
            <p>Użytkowników forum: 200</p>
            <p>Przesłanych zdjęć: 1300</p>
        </aside>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  27. Krok 27: Skrypt 1: odnośnik „zobacz zdjęcie” z rozszerzeniem .jpg

    Kryteria: ,

    R.3.2 Zapisano na stronie przynajmniej jeden odnośnik o treści „zobacz zdjęcie", prowadzący do dowolnego zdjęcia droga1.jpg ÷ droga5.jpg

    R.5.5 Skrypt1 wyświetla odnośnik, którego kliknięcie powoduje otwarcie zdjęcia. Nazwa zdjęcia została pobrana z bazy, zostało dopisane rozszerzenie .jpg

    ok. 3 min
    Polecenie z arkusza

    Następnie odnośnik o treści „zobacz zdjęcie”, prowadzący do zdjęcia, którego nazwa została zwrócona zapytaniem.

    Pole zrodlo ma samą nazwę (droga1), więc rozszerzenie .jpg stoi w HTML-u zaraz za wartością (R.5.5, R.3.2). Nazwa przechodzi przez dwie funkcje, każda od czego innego:

    • rawurlencode() - zapisuje nazwę jako bezpieczny fragment adresu: spacja staje się %20, polska litera - kodem. Dziś nazwy są proste, ale plik „droga górska” bez tego dałby zepsuty odnośnik,
    • escape() - zabezpiecza atrybut href w HTML-u.

    W podglądzie kliknięcia są zablokowane.

    Typowe błędy

    href bez .jpg prowadzi do nieistniejącego pliku droga1 - przeglądarka pokaże błąd 404.

    motor.php PHP zmienione: 3 linie
    <?php
    function escape(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $trips = [];
    $tripCount = 0;
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=motory;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // Skrypt 1: wycieczki ze zdjęciami
        $trips = $pdo->query(
            'SELECT w.nazwa, w.opis, w.poczatek, z.zrodlo
             FROM wycieczki AS w
             JOIN zdjecia AS z ON z.id = w.zdjecia_id'
        )->fetchAll();
    
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wycieczek. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
        <header>
            <h1>Motocykle - moja pasja</h1>
        </header>
        <main>
            <h2>Gdzie pojechać?</h2>
            <dl>
                <?php foreach ($trips as $trip): ?>
                    <dt><?= escape($trip['nazwa']) ?>, rozpoczyna się w <?= escape($trip['poczatek']) ?>,
                        <a href="<?= escape(rawurlencode($trip['zrodlo'])) ?>.jpg">zobacz zdjęcie</a>
                    </dt>
                <?php endforeach; ?>
            </dl>
        </main>
        <aside id="buy">
            <h2>Co kupić?</h2>
            <ol>
                <li>Honda CBR125R</li>
                <li>Yamaha YBR125</li>
                <li>Honda VFR800i</li>
                <li>Honda CBR1100XX</li>
                <li>BMW R1200GS LC</li>
            </ol>
        </aside>
        <aside id="stats">
            <h2>Statystyki</h2>
            <p>Wpisanych wycieczek: </p>
            <p>Użytkowników forum: 200</p>
            <p>Przesłanych zdjęć: 1300</p>
        </aside>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  28. Krok 28: Skrypt 1: opis jako element definicji

    Kryteria: ,

    R.2.6 W bloku lewym umieszczono listę definicji, zastosowano znacznik <dl> oraz zagnieżdżony w nim przynajmniej jeden z <dt>, <dd> (listę wygenerowano skryptem lub w HTML)

    R.5.6 Skrypt 1 wyświetla opis pobrany z bazy

    ok. 1 min
    Polecenie z arkusza

    Element definicji listy: zwrócone zapytaniem pole opis.

    Po terminie <dd> z opisem (R.5.6, R.2.6). Lista ma dziesięć par; opisy nie mieszczą się w 600 px bloku lewego, więc pojawia się pasek przewijania. Najedź kursorem na opis - podświetla się na LightGreen, jak na obrazie 2.

    motor.php PHP zmienione: 1 linia
    <?php
    function escape(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $trips = [];
    $tripCount = 0;
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=motory;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // Skrypt 1: wycieczki ze zdjęciami
        $trips = $pdo->query(
            'SELECT w.nazwa, w.opis, w.poczatek, z.zrodlo
             FROM wycieczki AS w
             JOIN zdjecia AS z ON z.id = w.zdjecia_id'
        )->fetchAll();
    
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wycieczek. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
        <header>
            <h1>Motocykle - moja pasja</h1>
        </header>
        <main>
            <h2>Gdzie pojechać?</h2>
            <dl>
                <?php foreach ($trips as $trip): ?>
                    <dt><?= escape($trip['nazwa']) ?>, rozpoczyna się w <?= escape($trip['poczatek']) ?>,
                        <a href="<?= escape(rawurlencode($trip['zrodlo'])) ?>.jpg">zobacz zdjęcie</a>
                    </dt>
                    <dd><?= escape($trip['opis']) ?></dd>
                <?php endforeach; ?>
            </dl>
        </main>
        <aside id="buy">
            <h2>Co kupić?</h2>
            <ol>
                <li>Honda CBR125R</li>
                <li>Yamaha YBR125</li>
                <li>Honda VFR800i</li>
                <li>Honda CBR1100XX</li>
                <li>BMW R1200GS LC</li>
            </ol>
        </aside>
        <aside id="stats">
            <h2>Statystyki</h2>
            <p>Wpisanych wycieczek: </p>
            <p>Użytkowników forum: 200</p>
            <p>Przesłanych zdjęć: 1300</p>
        </aside>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  29. Krok 29: Skrypt 2: liczba wycieczek w paragrafie

    Kryteria:

    R.5.7 Skrypt 2 wyświetla napis o treści "Wpisanych wycieczek:" wartość pobraną zapytaniem

    ok. 2 min
    Polecenie z arkusza

    Skrypt 2: Wysyła do bazy danych zapytanie 3. Wartość zwrócona zapytaniem jest wyświetlana w paragrafie po słowach: „Wpisanych wycieczek: ”.

    COUNT(*) zwraca jeden wiersz z jedną kolumną, więc zamiast fetchAll jest fetchColumn() - od razu sama wartość. (int) zamienia ją z napisu na liczbę: liczba nie wymaga escape(), bo nie zawiera znaków HTML. W akapicie <?= $tripCount ?> wypisuje 10 w tej samej linii co tekst (R.5.7).

    motor.php PHP zmienione: 4 linie
    <?php
    function escape(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $trips = [];
    $tripCount = 0;
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=motory;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // Skrypt 1: wycieczki ze zdjęciami
        $trips = $pdo->query(
            'SELECT w.nazwa, w.opis, w.poczatek, z.zrodlo
             FROM wycieczki AS w
             JOIN zdjecia AS z ON z.id = w.zdjecia_id'
        )->fetchAll();
    
        // Skrypt 2: liczba wycieczek
        $tripCount = (int) $pdo->query('SELECT COUNT(*) FROM wycieczki')->fetchColumn();
    
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wycieczek. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
        <header>
            <h1>Motocykle - moja pasja</h1>
        </header>
        <main>
            <h2>Gdzie pojechać?</h2>
            <dl>
                <?php foreach ($trips as $trip): ?>
                    <dt><?= escape($trip['nazwa']) ?>, rozpoczyna się w <?= escape($trip['poczatek']) ?>,
                        <a href="<?= escape(rawurlencode($trip['zrodlo'])) ?>.jpg">zobacz zdjęcie</a>
                    </dt>
                    <dd><?= escape($trip['opis']) ?></dd>
                <?php endforeach; ?>
            </dl>
        </main>
        <aside id="buy">
            <h2>Co kupić?</h2>
            <ol>
                <li>Honda CBR125R</li>
                <li>Yamaha YBR125</li>
                <li>Honda VFR800i</li>
                <li>Honda CBR1100XX</li>
                <li>BMW R1200GS LC</li>
            </ol>
        </aside>
        <aside id="stats">
            <h2>Statystyki</h2>
            <p>Wpisanych wycieczek: <?= $tripCount ?></p>
            <p>Użytkowników forum: 200</p>
            <p>Przesłanych zdjęć: 1300</p>
        </aside>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  30. Krok 30: Komunikat, gdy baza nie odpowiada

    ok. 2 min
    Polecenie z arkusza

    Na końcu jest zamykane połączenie z serwerem.

    Nad listą akapit z $dbError - tylko gdy catch coś do niego wpisał. Sprawdzone uruchomieniem z błędną nazwą bazy: strona się wyświetla, w bloku lewym stoi „Nie udało się pobrać wycieczek…”, licznik pokazuje 0, a w treści nie ma nazwy bazy ani kodu błędu SQL. W normalnym działaniu połączenie jest zamykane ($pdo = null) zaraz po obu skryptach, przed pierwszą linią HTML.

    motor.php PHP zmienione: 3 linie
    <?php
    function escape(?string $text): string
    {
        return htmlspecialchars($text ?? '', ENT_QUOTES);
    }
    
    $trips = [];
    $tripCount = 0;
    $dbError = '';
    
    try {
        $pdo = new PDO('mysql:host=localhost;dbname=motory;charset=utf8mb4', 'root', '', [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    
        // Skrypt 1: wycieczki ze zdjęciami
        $trips = $pdo->query(
            'SELECT w.nazwa, w.opis, w.poczatek, z.zrodlo
             FROM wycieczki AS w
             JOIN zdjecia AS z ON z.id = w.zdjecia_id'
        )->fetchAll();
    
        // Skrypt 2: liczba wycieczek
        $tripCount = (int) $pdo->query('SELECT COUNT(*) FROM wycieczki')->fetchColumn();
    
        $pdo = null;
    } catch (PDOException $exception) {
        error_log($exception->getMessage());
        $dbError = 'Nie udało się pobrać wycieczek. Spróbuj ponownie za chwilę.';
    }
    ?>
    <!DOCTYPE html>
    <html lang="pl">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Motocykle</title>
        <link rel="stylesheet" href="styl.css">
    </head>
    <body>
        <img id="bike" src="motor.png" alt="motocykl">
        <header>
            <h1>Motocykle - moja pasja</h1>
        </header>
        <main>
            <h2>Gdzie pojechać?</h2>
            <?php if ($dbError !== ''): ?>
                <p><?= escape($dbError) ?></p>
            <?php endif; ?>
            <dl>
                <?php foreach ($trips as $trip): ?>
                    <dt><?= escape($trip['nazwa']) ?>, rozpoczyna się w <?= escape($trip['poczatek']) ?>,
                        <a href="<?= escape(rawurlencode($trip['zrodlo'])) ?>.jpg">zobacz zdjęcie</a>
                    </dt>
                    <dd><?= escape($trip['opis']) ?></dd>
                <?php endforeach; ?>
            </dl>
        </main>
        <aside id="buy">
            <h2>Co kupić?</h2>
            <ol>
                <li>Honda CBR125R</li>
                <li>Yamaha YBR125</li>
                <li>Honda VFR800i</li>
                <li>Honda CBR1100XX</li>
                <li>BMW R1200GS LC</li>
            </ol>
        </aside>
        <aside id="stats">
            <h2>Statystyki</h2>
            <p>Wpisanych wycieczek: <?= $tripCount ?></p>
            <p>Użytkowników forum: 200</p>
            <p>Przesłanych zdjęć: 1300</p>
        </aside>
        <footer>
            <p>Stronę wykonał: Bartosz Bryniarski</p>
        </footer>
    </body>
    </html>
    Podgląd strony po tym kroku (motor.php)
  31. Etap 5: Porównanie z wersją podstawową

    Krok 31 · po tym etapie masz 35 z 35 kryteriów (pkt)

    Krok 31: Co jest inaczej i którą wersję wybrać na egzaminie

    ok. 2 min

    Obie wersje dają komplet 35 punktów. Różnice:

    wersja podstawowawersja nowoczesna
    układfloat: left bloków; drugi blok prawy „opada”, bo nie mieści się w liniigrid-template-areas: miejsce każdego bloku zapisane wprost
    szerokości blokówwidth: 75% i width: 15%grid-template-columns: 75% 15%
    kolorywpisane w każdej regulezmienne CSS w :root
    zapytaniaOR, JOIN ... ON zdjecia_id = zdjecia.id, INTIN (...), aliasy w i z, TINYINT UNSIGNED
    bazamysqli_connect, mysqli_fetch_row, $row[3]PDO z wyjątkami, fetchAll, fetchColumn, $trip['zrodlo']
    gdzie PHProzproszone po HTML-u, połączenie zamykane po stopcedane na górze pliku, połączenie zamknięte przed HTML-em
    dane na stroniewprost (w dobrych praktykach htmlspecialchars)escape() przy każdej wartości, rawurlencode() w adresie
    awaria bazybiała strona z błędemkomunikat w bloku lewym, licznik 0

    Na egzaminie wersja podstawowa trzyma się funkcji z tabeli 1 arkusza i jest szybsza do napisania. Nowoczesna oddziela pobieranie danych od wyglądu, a jej układ nie rozsypie się po zmianie marginesu czy szerokości - w wersji z float za szeroki blok prawy spadłby pod lewy. Obraz motocykla w obu wersjach jest pozycjonowany absolutnie, bo tego wymaga klucz.

    Podgląd strony bez zmian - taki jak po kroku 30.

  32. Etap 6: Oddanie pracy

    Krok 32 · po tym etapie masz 35 z 35 kryteriów (pkt)

    Krok 32: Plik przeglądarka.txt i nagranie płyty

    ok. 5 min
    Polecenie z arkusza

    Po zakończeniu pracy utwórz plik tekstowy o nazwie przeglądarka.txt. Zapisz w nim nazwę przeglądarki internetowej, w której weryfikowana była poprawność działania witryny. Umieść go w folderze z numerem zdającego. Nagraj płytę z rezultatami pracy. W folderze z numerem zdającego, powinny znajdować się pliki: droga1.jpg, droga2.jpg, droga3.jpg, droga4.jpg, droga5.jpg, import.png, kw1.jpg, kw2.jpg, kw3.jpg, kw4.jpg, kwerendy.txt, motor.php, motor.png, przeglądarka.txt, styl.css, ewentualnie inne przygotowane pliki.

    W przeglądarka.txt nazwa przeglądarki, w której strona była sprawdzana przez http://localhost/.... Sprawdź listę plików z arkusza (w tym przeskalowany droga1.jpg), nagraj płytę, sprawdź jej odczyt i opisz ją numerem zdającego.

    przeglądarka.txt Tekst nowy plik
    Google Chrome

    Podgląd strony bez zmian - taki jak po kroku 30.

Podgląd

To jedno z możliwych rozwiązań. Egzaminator ocenia spełnienie kryteriów z klucza odpowiedzi, a nie zgodność z tym kodem - inna, poprawna droga też daje punkty.

Autor rozwiązania: . Licencja CC BY-NC 4.0 - kopiowanie i przeróbki do celów niekomercyjnych, z podaniem autora i źródła (link do tej strony) (zasady wykorzystania). Treść arkusza i klucza oceniania pochodzi z CKE.