Zadania webowe z egzaminu, krok po kroku
Na egzaminie INF.04 część arkuszy ma w części II aplikację internetową w React. Ten kurs bierze takie zadania po kolei i rozkłada każde na małe kroki. Każdy krok ma jedną nową rzecz z Reacta i ćwiczenie, które od razu działa w przeglądarce. Na końcu piszesz całe zadanie od pustego pliku, jak na egzaminie.
Po czterech zadaniach z prawdziwych arkuszy jest dziesięć zadań ćwiczeniowych w tym samym stylu.
Zadania w kursie
| Arkusz | Zadanie | Czego uczy |
|---|
Zadania ćwiczeniowe
Dziesięć dodatkowych zadań napisanych tak jak część II arkusza INF.04. Nie pochodzą z prawdziwych sesji, ale mają tę samą skalę: jeden komponent, formularz albo lista z Bootstrapem, stan i trochę logiki. Każde jest rozłożone na kroki, a na końcu piszesz je od pustego pliku. Rób je po zadaniach z arkuszy, w dowolnej kolejności.
| Zadanie | Temat | Czego uczy |
|---|
Jak korzystać z kursu
- Zacznij od lekcji „Projekt w Vite”. Jest wspólna dla wszystkich zadań: tworzy projekt i podłącza Bootstrapa.
- Potem wybierz zadanie. Jego lekcje idą w kolejności, w jakiej budujesz aplikację na egzaminie.
- W ćwiczeniu piszesz plik
App.jsx. Aplikacja uruchamia się obok po każdej zmianie, a pod nią widać konsolę przeglądarki. Bootstrap jest już podłączony. - „Sprawdź” (albo Ctrl+Enter) uruchamia testy. Przy nieudanym teście jest napisane, co poszło nie tak.
- Lekcja „Zrzuty i oddanie” mówi, co zapisać na płycie. Też jest wspólna.
- Postęp i twój kod zapisują się w tej przeglądarce.
if w JavaScripcie, oraz znasz znaczniki HTML: h2, ol, form, label, input. Reacta uczy od zera, przy pierwszym zadaniu.Zapisy na kursy
Arkusz INF.04-02-22.06 (czerwiec 2022), część II. Aplikacja ma wyświetlić listę kursów i formularz zapisu, a wynik wypisać w konsoli przeglądarki. To dobre pierwsze zadanie: używa wszystkich podstaw Reacta, a logiki jest mało.
Tak ma wyglądać wynik
To gotowa aplikacja. Wpisz imię i numer kursu, kliknij przycisk i zobacz, co pojawia się w konsoli pod spodem. Spróbuj też numeru 5 albo 0.
Co mówi arkusz
- Aplikacja front-end w React (albo Angularze; ten kurs używa Reacta) z jednym komponentem. Formularz ma style Bootstrapa.
- Dane komponentu to tablica trzech nazw kursów: „Programowanie w C#”, „Angular dla początkujących”, „Kurs Django”. Tablica może się kiedyś zmienić, a aplikacja ma wtedy działać bez poprawek.
- Na stronie jest nagłówek
h2„Liczba kursów: 3”, lista numerowana wszystkich kursów i formularz: pole „Imię i nazwisko:”, pole liczbowe „Numer kursu:” i przycisk „Zapisz do kursu”. Na początku pola są puste. - Zatwierdzenie formularza wypisuje w konsoli przeglądarki imię i nazwisko, a potem nazwę kursu o podanym numerze albo komunikat „Nieprawidłowy numer kursu”.
- Na koniec trzeba oddać spakowany projekt, zmienione pliki, zrzuty ekranu i krótką dokumentację (lekcja „Zrzuty i oddanie”).
Plan
Kolejne lekcje budują tę aplikację w takiej kolejności, w jakiej robisz to na egzaminie: komponent, nagłówek z liczbą kursów, lista, formularz, stan pól, zatwierdzenie, numer kursu. Ostatnia lekcja to całe zadanie od pustego pliku.
Projekt w Vite i folder egzaminu
Na egzaminie zaczynasz od pustego pulpitu. Najpierw zakładasz foldery z arkusza, potem projekt Reacta. Projekt tworzy narzędzie Vite: jednym poleceniem robi szkielet aplikacji z serwerem podglądu.
Foldery
Arkusz każe utworzyć folder nazwany twoim numerem PESEL, a w nim trzy podfoldery. Aplikację webową robisz w podfolderze web.
12345678901/ ← twój PESEL ├── konsola/ ← część I (aplikacja konsolowa) ├── web/ ← ten kurs └── testy/ ← zrzuty ekranu i dokument „egzamin”
Nowy projekt
Otwórz terminal (w Visual Studio Code: Terminal → New Terminal) w folderze web i wpisz kolejno:
- Utwórz projekt z szablonu React (JavaScript). Nazwa jest dowolna, w przykładach to
kursy:> npm create vite@latest kursy -- --template reactJeśli Vite zapyta o cokolwiek, zatwierdź Enterem. Nowsze wersje pytają też, czy od razu zainstalować i uruchomić projekt. Możesz się zgodzić, wtedy kroki 3 i 5 dzieją się same. - Wejdź do projektu:
> cd kursy - Zainstaluj biblioteki projektu (React, Vite):
> npm install - Doinstaluj Bootstrapa:
> npm install bootstrap - Uruchom serwer podglądu i otwórz w przeglądarce adres, który wypisze (zwykle
http://localhost:5173):> npm run devSerwer działa, dopóki nie zamkniesz terminala albo nie naciśniesz Ctrl+C. Po każdym zapisaniu pliku strona odświeża się sama.
npm create i npm install nie zadziałają. Na egzaminie środowisko jest przygotowane wcześniej, czasem z gotowym szablonem projektu. Przeczytaj, co jest na pulpicie, i zapytaj komisję, zanim zaczniesz.Które pliki zmieniasz
kursy/ ├── node_modules/ ← biblioteki, nie ruszasz ├── public/ ├── src/ │ ├── App.jsx ← cały komponent zadania │ ├── App.css ← niepotrzebny │ ├── index.css ← niepotrzebny │ └── main.jsx ← tu podłączasz Bootstrapa ├── index.html └── package.json
W src/main.jsx podmień import ./index.css na arkusz Bootstrapa. Szablon Vite ma własne style, które psułyby wygląd formularza, więc usuń ich importy (w App.jsx także import './App.css').
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'bootstrap/dist/css/bootstrap.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
Ten import jest w tabeli pomocy na ostatniej stronie arkusza, więc nie musisz go pamiętać. Całą resztę piszesz w App.jsx. Od następnej lekcji ćwiczenia w kursie to właśnie ten plik.
Konsola przeglądarki
Wynik aplikacji z zadania widać tylko w konsoli. W Chrome i Edge otwiera ją F12 (albo Ctrl+Shift+J), zakładka Console. Trzymaj ją otwartą przez cały czas pracy: tam pojawiają się też błędy Reacta.
StrictMode celowo uruchamia część kodu podwójnie. Na kliknięcie przycisku to nie wpływa: console.log w obsłudze zatwierdzenia formularza wypisze się raz.Komponent i JSX
Komponent w React to zwykła funkcja JavaScriptu, której nazwa zaczyna się wielką literą. Zwraca to, co ma się pojawić na stronie. Zapis podobny do HTML wewnątrz funkcji to JSX: Vite zamienia go na wywołania Reacta, zanim kod trafi do przeglądarki.
function App() {
return (
<div className="container">
<h2>Liczba kursów:</h2>
</div>
);
}
export default App;
Cztery zasady JSX, które przydadzą się w zadaniu
| Zasada | Źle | Dobrze |
|---|---|---|
Komponent zwraca jeden element nadrzędny. Kilka elementów obok siebie owiń w div. | return (<h2>…</h2> <ol>…</ol>) | return (<div><h2>…</h2><ol>…</ol></div>) |
Klasy CSS podajesz w className, bo class to słowo zastrzeżone JavaScriptu. | class="container" | className="container" |
Atrybut for etykiety to w JSX htmlFor. | <label for="imie"> | <label htmlFor="imie"> |
| Znacznik bez pary zamykasz ukośnikiem. | <input type="text"> | <input type="text" /> |
Ostatnia linia pliku, export default App;, udostępnia komponent plikowi main.jsx, który wstawia go do strony. Bez niej strona będzie pusta.
Tablica kursów i nagłówek z liczbą
Dane komponentu to tablica nazw kursów. Arkusz pozwala trzymać ją wprost w komponencie, więc wystarczy stała na początku funkcji:
function App() {
const kursy = ["Programowanie w C#", "Angular dla początkujących", "Kurs Django"];
return (
<div className="container">
<h2>Liczba kursów: {kursy.length}</h2>
</div>
);
}
Nawiasy klamrowe w JSX
W JSX wszystko między { a } jest zwykłym wyrażeniem JavaScriptu. React oblicza je i wstawia wynik w to miejsce strony. {kursy.length} to długość tablicy, czyli 3.
<h2>Liczba kursów: 3</h2>
<h2>Liczba kursów: {kursy.length}</h2>Na ekranie oba zapisy wyglądają tak samo. Arkusz mówi jednak, że tablica może się zmienić, a aplikacja ma się wtedy zachować inaczej. Egzaminator sprawdza więc, czy liczba wynika z tablicy. Testy w tym kursie robią to samo: dopisują do tablicy czwarty kurs i patrzą, czy nagłówek pokaże 4.
Liczba kursów: {kursy.length} daje „Liczba kursów: 3”. Bez spacji po dwukropku wyjdzie „Liczba kursów:3”.Lista numerowana z tablicy
Lista ma pokazywać wszystkie kursy, ile by ich nie było. Nie przepisujesz więc trzech li, tylko każesz Reactowi zrobić po jednym li dla każdego elementu tablicy. Służy do tego metoda map.
<ol>
{kursy.map((kurs) => (
<li key={kurs}>{kurs}</li>
))}
</ol>
mapwywołuje funkcję strzałkową dla każdego elementu tablicy i zbiera wyniki w nową tablicę. Tu wynikiem jest tablica elementówli, a React wstawia je wszystkie dool.olto lista numerowana. Numery 1, 2, 3 dopisuje przeglądarka, więc nie wpisujesz ich sam.uldałaby kropki.keyto atrybut, po którym React rozpoznaje elementy listy. Każdy element musi mieć inny klucz. Nazwy kursów się nie powtarzają, więc nadają się na klucz.
key to nie błąd, tylko ostrzeżenie. Lista się wyświetli, ale w konsoli pojawi się Each child in a list should have a unique "key" prop. Na egzaminie lepiej, żeby konsola na zrzucie była czysta.Drugi parametr funkcji w map to numer elementu (od zera). Możesz go użyć jako klucza, gdy wartości mogą się powtarzać:
{kursy.map((kurs, indeks) => <li key={indeks}>{kurs}</li>)}
Formularz z klasami Bootstrapa
Arkusz podaje wzór formularza z dokumentacji Bootstrapa. Każde pole leży we własnym bloku form-group: etykieta, a pod nią pole z klasą form-control. Przycisk dostaje klasy btn btn-primary, czyli niebieski przycisk.
<form>
<div className="form-group">
<label htmlFor="imieNazwisko">Imię i nazwisko:</label>
<input type="text" className="form-control" id="imieNazwisko" />
</div>
<div className="form-group">
<label htmlFor="numerKursu">Numer kursu:</label>
<input type="number" className="form-control" id="numerKursu" />
</div>
<button type="submit" className="btn btn-primary">Zapisz do kursu</button>
</form>
| Element | Po co |
|---|---|
htmlFor i id | Wiążą etykietę z polem: kliknięcie etykiety ustawia kursor w polu. Wartości muszą być identyczne. |
type="number" | Pole liczbowe ze strzałkami, jak każe arkusz. Wpisanego tekstu nie przyjmie. |
form-control | Pole na całą szerokość, z ramką i zaokrągleniem. |
form-group | Blok z odstępem pod spodem w Bootstrapie 4 (z niego pochodzi wzór w arkuszu). |
btn btn-primary | Kształt przycisku i kolor główny. Sama btn-primary nie wystarczy. |
type="submit" | Kliknięcie zatwierdza formularz. To przyda się przy zatwierdzaniu formularza. |
Znaczące identyfikatory
Arkusz wymaga znaczących nazw identyfikatorów, a klucz oceniania sprawdza to osobno. Nie przepisuj exampleInputEmail1 z pomocy i nie używaj nazw typu pole1, input2. Nazwa ma mówić, co jest w polu: imieNazwisko, numerKursu.
npm install bootstrap instaluje wersję 5, a wzór w arkuszu pochodzi z wersji 4. W wersji 5 klasa form-group nie ma już stylów, więc pola stoją ciasno pod sobą. Zostaw form-group, bo jest we wzorze, i dopisz mb-3 (margines pod blokiem): className="form-group mb-3".Stan pól: useState
Po kliknięciu przycisku program musi znać to, co wpisano w pola. W React nie czytasz pól przez document.getElementById. Wartość pola trzymasz w stanie komponentu, a pole tylko ją pokazuje i zmienia.
import { useState } from 'react';
function App() {
const [imieNazwisko, setImieNazwisko] = useState("");
// ...
<input
type="text"
className="form-control"
id="imieNazwisko"
value={imieNazwisko}
onChange={(e) => setImieNazwisko(e.target.value)}
/>
}
useState("")tworzy stan z wartością początkową: pustym napisem. Dzięki temu pole na starcie jest puste, jak każe arkusz.- Zwraca parę: bieżącą wartość (
imieNazwisko) i funkcję, która ją zmienia (setImieNazwisko). Nazwy wybierasz sam. Zwyczaj tonazwaisetNazwa. value={imieNazwisko}: pole pokazuje to, co jest w stanie.onChange: przy każdym wpisanym znaku zapisujesz nową treść pola (e.target.value) do stanu. React rysuje komponent jeszcze raz i pole pokazuje nową wartość.useStatetrzeba zaimportować zreactw pierwszej linii pliku.
value, ale nie ma onChange. React pokazuje wtedy w kółko wartość ze stanu, a ten się nie zmienia. W konsoli pojawi się ostrzeżenie o polu bez obsługi zmian.Pole liczbowe też daje napis
Nawet z type="number" wartość e.target.value jest napisem, np. "2". Trzymaj ją w stanie jako napis (start: "", żeby pole było puste) i zamień na liczbę dopiero przy zapisie. Tym zajmie się lekcja „Numer kursu i nazwa”.
Zatwierdzenie formularza i konsola
Arkusz mówi o zdarzeniu zatwierdzenia formularza. W React to atrybut onSubmit na znaczniku form. Przycisk z type="submit" zatwierdza formularz po kliknięciu. To samo dzieje się po naciśnięciu Enter w polu.
function zapiszDoKursu(event) {
event.preventDefault();
console.log(imieNazwisko);
}
return (
<form onSubmit={zapiszDoKursu}>
{/* pola */}
<button type="submit" className="btn btn-primary">Zapisz do kursu</button>
</form>
);
event.preventDefault()jest konieczne. Zwykły formularz po zatwierdzeniu wysyła dane i przeładowuje stronę. Przeładowanie czyści konsolę i stan, więc wynik zniknąłby, zanim go zobaczysz.- Funkcję podajesz bez nawiasów:
onSubmit={zapiszDoKursu}. Z nawiasami (zapiszDoKursu()) wywołałaby się od razu przy rysowaniu strony. - Funkcja leży wewnątrz komponentu, więc widzi stan:
imieNazwisko,numerKursui tablicękursy. console.logwypisuje w konsoli przeglądarki. Każde wywołanie to osobna linia.
onClick na przycisku? Zadziała podobnie, ale arkusz każe użyć zdarzenia zatwierdzenia formularza, a klucz oceniania wymienia onSubmit. Zostań przy onSubmit na form.Od numeru kursu do nazwy
Ostatni element logiki: po zatwierdzeniu wypisz nazwę kursu o numerze z drugiego pola. Gdy takiego kursu nie ma, wypisz „Nieprawidłowy numer kursu”. To trzy małe kroki.
- Napis na liczbę. Stan pola to napis, więc zamień go funkcją
Number:Number("2")daje 2, aNumber("")daje 0. - Sprawdzenie zakresu. Poprawne numery to od 1 do liczby kursów. Górną granicę bierzesz z
kursy.length, a nie wpisujesz 3, bo tablica może się zmienić. - Numer na indeks. Użytkownik liczy od 1, a tablica od 0. Kurs nr 1 to
kursy[0], więc sięgasz pokursy[numer - 1].
function zapiszDoKursu(event) {
event.preventDefault();
console.log(imieNazwisko);
const numer = Number(numerKursu);
if (numer >= 1 && numer <= kursy.length) {
console.log(kursy[numer - 1]);
} else {
console.log("Nieprawidłowy numer kursu");
}
}
numerKursu = Number(numerKursu). numerKursu to stała z const [numerKursu, setNumerKursu] = useState(""), więc nie da się do niej nic przypisać: Vite pokaże błąd „numerKursu is read-only”, a przeglądarka „Assignment to constant variable”. Stanu w Reakcie nie zmienia się zresztą przypisaniem, tylko funkcją setNumerKursu. Tu stanu nie trzeba zmieniać. Liczba jest potrzebna tylko w tej funkcji, więc zapisz ją w nowej stałej o innej nazwie: const numer = Number(numerKursu);. Nie pomijaj słowa const, bo samo numer = Number(numerKursu) kończy się błędem „numer is not defined”.| Wpisany numer | Number(…) | Wynik w konsoli |
|---|---|---|
| 1 | 1 | Programowanie w C# |
| 3 | 3 | Kurs Django |
| 4 | 4 | Nieprawidłowy numer kursu (za duży) |
| 0 albo -2 | 0, -2 | Nieprawidłowy numer kursu (za mały) |
| (puste pole) | 0 | Nieprawidłowy numer kursu |
| 1.5 | 1.5 | bez dodatkowej kontroli kursy[0.5], czyli undefined |
Ostatni wiersz to przypadek brzegowy: pole liczbowe przyjmie ułamek. Jeśli chcesz go wykluczyć, dopisz warunek Number.isInteger(numer). Testy w kursie sprawdzają też ten przypadek.
if (Number.isInteger(numer) && numer >= 1 && numer <= kursy.length) {
if (kursy[numer - 1]) bez zamiany na liczbę i bez zakresu. Dla pustego pola "" - 1 to -1, a kursy[-1] to undefined, więc przypadkiem działa. Egzaminator chce jednak widzieć jawne sprawdzenie numeru. Pisz warunek wprost.Całe zadanie od pustego pliku
Teraz wszystko naraz, jak na egzaminie. Plik ma tylko szkielet komponentu. Napisz aplikację bez zaglądania do poprzednich lekcji, a potem kliknij „Sprawdź”. Testy są takie, jak sprawdzałby egzaminator: wygląd, konsola dla dobrych i złych numerów, tablica zmieniona na cztery kursy, nazwy identyfikatorów.
onSubmit, warunek z numerem. Po każdym kroku zapisz plik i spójrz do przeglądarki. Zrzuty ekranu rób na końcu, kiedy wszystko działa.Lista kontrolna przed „Sprawdź”
import { useState } from 'react';na górze pliku,export default App;na dole.- Tablica
kursyz trzema nazwami w kolejności z arkusza. h2z{kursy.length},olzkursy.mapikey.- Dwa bloki
form-group: etykieta zhtmlFor, pole zid,className="form-control",valueionChange. - Przycisk
type="submit"zbtn btn-primary. onSubmitna formularzu,preventDefault, dwaconsole.log.
Formularz filmu
Arkusz INF.04-03-23.06 (czerwiec 2023), część II. Krótkie zadanie: formularz z jednym polem tekstowym i listą rozwijaną, a po kliknięciu jedna linia w konsoli przeglądarki. Nowe względem zapisów na kursy są lista select i własny plik CSS.
Tak ma wyglądać wynik
Wpisz tytuł, wybierz rodzaj i kliknij „Dodaj”. W konsoli pojawi się numer rodzaju, nie jego nazwa.
Co mówi arkusz
- Jeden komponent z formularzem: pole tekstowe z etykietą „Tytuł filmu”, lista rozwijana z etykietą „Rodzaj” i przycisk „Dodaj”.
- Lista ma pięć opcji: pustą, a potem Komedia, Obyczajowy, Sensacyjny i Horror o wartościach od 1 do 4.
- Na początku oba pola są puste.
- „Dodaj” wypisuje w konsoli dane z formularza. W wersji React jest to jedna linia w postaci
tytul: …; rodzaj: …. - Strona ma styl
body { padding: 20px; }, formularz klasy Bootstrapa, pola znaczące identyfikatory. - Plików z archiwum w tym zadaniu nie ma.
Pole tekstowe i lista rozwijana
Lista rozwijana to znacznik select, a jej pozycje to znaczniki option. Każda opcja ma dwie rzeczy: wartość w atrybucie value, którą dostaje program, i napis między znacznikami, który widzi użytkownik.
<div className="form-group">
<label htmlFor="rodzajFilmu">Rodzaj</label>
<select className="form-control" id="rodzajFilmu">
<option value=""></option>
<option value="1">Komedia</option>
<option value="2">Obyczajowy</option>
<option value="3">Sensacyjny</option>
<option value="4">Horror</option>
</select>
</div>
- Pierwsza opcja jest pusta: ma
value=""i nie ma napisu. Arkusz wymaga jej wprost, bo na początku lista ma być pusta. - Etykietę z listą łączy para
htmlForiid, tak samo jak przy polu tekstowym. - Pomoc w arkuszu (Bootstrap 4) mówi, że
form-controldostają pola, listy i obszary tekstu. W Bootstrapie 5 lista ma własną klasęform-select, która dorysowuje strzałkę. Obie są dobre. Testy przyjmują każdą z nich.
selected do opcji. W HTML wybraną opcję oznacza się atrybutem selected, ale w React wybór ustawia value na całym select. Przyjdzie na to pora w następnej lekcji.Stan pola i listy
Lista rozwijana działa w React tak samo jak pole tekstowe: jej wartość trzymasz w stanie, a select dostaje value i onChange. Wybrana opcja to ta, której value równa się wartości ze stanu.
const [tytul, setTytul] = useState("");
const [rodzaj, setRodzaj] = useState("");
<select className="form-control" id="rodzajFilmu"
value={rodzaj} onChange={(event) => setRodzaj(event.target.value)}>
<option value=""></option>
{/* ... */}
</select>
- Stan zaczyna od
"". To wartość pustej opcji, więc na starcie lista jest pusta, jak każe arkusz. - Po wybraniu „Sensacyjny”
event.target.valueto napis"3". Ta liczba trafi potem do konsoli. - Bez
onChangelista zvaluestoi w miejscu: po wybraniu opcji React wraca do wartości ze stanu, a w konsoli pojawia się ostrzeżenie.
Jedna linia w konsoli
Arkusz pokazuje wynik dla Reacta w postaci tytul: Miś; rodzaj: 1. To jedna linia, więc wystarczy jedno wywołanie console.log z napisem sklejonym operatorem +.
function dodajFilm(event) {
event.preventDefault();
console.log("tytul: " + tytul + "; rodzaj: " + rodzaj);
}
<form onSubmit={dodajFilm}>
Ten sam napis można złożyć szablonem z odwrotnymi apostrofami. Wynik jest identyczny:
console.log(`tytul: ${tytul}; rodzaj: ${rodzaj}`);
- Do konsoli trafia wartość opcji (
1), a nie jej napis (Komedia). Tak jest na obrazie w arkuszu. - Pilnuj spacji i średnika: po dwukropku spacja, po średniku spacja.
console.log(tytul, rodzaj)wypisze dane, ale nie w wymaganej postaci.
Styl strony w App.css
Arkusz podaje regułę body { padding: 20px; }. Bootstrap jej nie zastąpi, więc zapisujesz ją we własnym pliku CSS. W projekcie Vite obok App.jsx leży App.css. Komponent musi go zaimportować, inaczej Vite go nie wczyta.
import { useState } from 'react';
import './App.css';body {
padding: 20px;
}- Szablon Vite ma w
App.csswłasne reguły. Na egzaminie usuń je i zostaw tylko swoją. - Plik Bootstrapa importujesz w
main.jsx, aApp.csswApp.jsx. Twój plik wczytuje się później, więc jego reguły wygrywają z Bootstrapem. - Styl wpisany w atrybucie
stylezadziałałby tak samo, ale arkusz mówi o stylu strony, a klucz sprawdza regułę w pliku CSS.
Od tej lekcji ćwiczenia mają dwa pola: App.jsx i pod nim App.css.
Całe zadanie od pustego pliku
Cały formularz od szkieletu. To zadanie na 15–20 minut egzaminu. Zrzuty ekranu: stan początkowy, wypełniony formularz i konsola po kliknięciu „Dodaj”.
Lista kontrolna przed „Sprawdź”
- Dwa stany:
tytulirodzaj, oba"". - Dwa bloki
form-group: etykieta zhtmlFor, pole i lista zid,className="form-control",value,onChange. - Pięć opcji, pierwsza pusta, wartości 1–4.
- Przycisk
type="submit"zbtn btn-primary, napis „Dodaj”. onSubmit,preventDefault, jedna liniatytul: …; rodzaj: ….import './App.css';i regułabodywApp.css.
Galeria zdjęć
Arkusz INF.04-01-25.01 (styczeń 2025), część II. Galeria dwunastu zdjęć w trzech kategoriach. Przełączniki chowają i pokazują kategorie, a przycisk przy zdjęciu zwiększa licznik pobrań. Wynik widać na stronie, nie w konsoli.
Tak ma wyglądać wynik
Wyłącz „Zwierzęta” i zobacz, co zostaje. Kliknij kilka razy „Pobierz” przy jednym zdjęciu.
Co mówi arkusz
- Na pulpicie jest archiwum
pliki3.zipz dwunastoma zdjęciamiobraz1.jpg–obraz12.jpgi plikiemdane.txt. Hasło do archiwum jest w treści arkusza. - Zdjęcia kopiujesz do folderu
assetsprojektu (w Vite:public/assets), a listę obiektów zdane.txtdo tablicy w kodzie. - Obiekt zdjęcia ma pola
id,alt,filename,category(1 kwiaty, 2 zwierzęta, 3 samochody) idownloads. - Nagłówek
h1„Kategorie zdjęć”, trzy przełączniki Kwiaty, Zwierzęta, Samochody, na początku włączone. - Bloki zdjęć obok siebie: zdjęcie,
h4z liczbą pobrań, przycisk „Pobierz”. Widać tylko kategorie z włączonym przełącznikiem. - „Pobierz” zwiększa o 1 liczbę pobrań zapisaną w tablicy obiektów.
- Zdjęcia mają margines 5 pikseli i zaokrąglone rogi. Kod ma pętlę i warunki, i działa także dla innej liczby zdjęć.
public/assets. Pliki z folderu public serwer podaje bez zmian, więc adres assets/obraz1.jpg zadziała wprost w atrybucie src. Gdy wkleisz je do src/assets, każde zdjęcie trzeba by importować osobno.Tablica zdjęć i pętla
Plik dane.txt zawiera gotowe obiekty JavaScriptu oddzielone przecinkami. Kopiujesz je w całości i wklejasz między nawiasy kwadratowe tablicy. Tablica trafia do stanu, bo w lekcji o licznikach będzie się zmieniać.
{id: 0, alt: "Mak", filename: "obraz1.jpg", category:1, downloads: 35},
{id: 1, alt:"Bukiet", filename: "obraz2.jpg", category: 1, downloads: 43},
{id: 2, alt:"Dalmatyńczyk", filename: "obraz3.jpg", category:2, downloads: 2},
...
{id: 11, alt:"Garbus", filename: "obraz12.jpg", category:3, downloads: 321}
Cały plik dane.txt do skopiowania
{id: 0, alt: "Mak", filename: "obraz1.jpg", category:1, downloads: 35},
{id: 1, alt:"Bukiet", filename: "obraz2.jpg", category: 1, downloads: 43},
{id: 2, alt:"Dalmatyńczyk", filename: "obraz3.jpg", category:2, downloads: 2},
{id: 3, alt:"Świnka morska", filename: "obraz4.jpg", category:2, downloads: 53},
{id: 4, alt:"Rotwailer", filename: "obraz5.jpg", category:2, downloads: 43},
{id: 5, alt:"Audi", filename: "obraz6.jpg", category:3, downloads: 11},
{id: 6, alt:"kotki", filename: "obraz7.jpg", category:2, downloads: 22},
{id: 7, alt:"Róża", filename: "obraz8.jpg", category:1, downloads: 33},
{id: 8, alt:"Świnka morska", filename: "obraz9.jpg", category:2, downloads: 123},
{id: 9, alt:"Foksterier", filename: "obraz10.jpg", category:2, downloads: 22},
{id: 10, alt:"Szczeniak", filename: "obraz11.jpg", category:2, downloads: 12},
{id: 11, alt:"Garbus", filename: "obraz12.jpg", category:3, downloads: 321}const [zdjecia, setZdjecia] = useState([
{id: 0, alt: "Mak", filename: "obraz1.jpg", category:1, downloads: 35},
// ... pozostałe obiekty z pliku
]);
{zdjecia.map((zdjecie) => (
<img key={zdjecie.id} src={"assets/" + zdjecie.filename} alt={zdjecie.alt} />
))}
srcskładasz z folderu i polafilename. W klamrach, bo to wyrażenie, a nie stały napis.- Kluczem jest
id, bo jest unikalne. Polealtnie nadaje się na klucz: „Świnka morska” występuje dwa razy. - W JSX znacznik
imgzamykasz ukośnikiem:<img … />. - Arkusz wymaga, żeby aplikacja działała dla innej liczby zdjęć. Pętla po tablicy spełnia to sama. Testy usuwają z tablicy ostatni obiekt i liczą obrazy.
App.css ma regułę zmniejszającą je do 200 pikseli szerokości, żeby mieściły się w podglądzie.Blok zdjęcia
Arkusz chce przy każdym zdjęciu nagłówek czwartego stopnia z liczbą pobrań i przycisk „Pobierz”. Trzy elementy obok siebie w map trzeba owinąć w jeden blok, bo funkcja w map zwraca jeden element. Klucz przechodzi wtedy na blok.
{zdjecia.map((zdjecie) => (
<div className="blok" key={zdjecie.id}>
<img src={"assets/" + zdjecie.filename} alt={zdjecie.alt} />
<h4>Pobrań: {zdjecie.downloads}</h4>
<button type="button" className="btn btn-success">Pobierz</button>
</div>
))}
- Na obrazie z arkusza nagłówek ma postać „Pobrań: 35”. Testy sprawdzają dokładnie ten napis.
- Przycisk jest zielony:
btn btn-success, jak w przykładzie z tabeli pomocy. type="button": przycisk nie leży w formularzu, ale jawny typ nie zaszkodzi i niczego nie wysyła.- Klasa
blokprzyda się w lekcji o wyglądzie, żeby postawić bloki obok siebie.
Trzy przełączniki
Przełącznik Bootstrapa to zwykłe pole wyboru (type="checkbox") z innym wyglądem. Wzór jest w tabeli pomocy arkusza. Klasa form-check-inline stawia przełączniki w jednym rzędzie.
const [kwiaty, setKwiaty] = useState(true);
<div className="form-check form-switch form-check-inline">
<input className="form-check-input" type="checkbox" id="przelacznikKwiaty"
checked={kwiaty} onChange={(event) => setKwiaty(event.target.checked)} />
<label className="form-check-label" htmlFor="przelacznikKwiaty">Kwiaty</label>
</div>
| Pole tekstowe | Pole wyboru |
|---|---|
value={tekst} | checked={kwiaty} |
event.target.value (napis) | event.target.checked (true albo false) |
stan od "" | stan od true: arkusz każe włączyć wszystkie |
Trzy przełączniki to trzy osobne stany: kwiaty, zwierzeta, samochody. Identyfikatory nadaj własne. Przykład z pomocy (ctrId) nie jest znaczącą nazwą.
Które zdjęcia widać
Zdjęcie widać, gdy włączony jest przełącznik jego kategorii. Ten warunek najłatwiej zapisać w osobnej funkcji, która dla zdjęcia zwraca true albo false:
function czyWidoczne(zdjecie) {
if (zdjecie.category === 1) return kwiaty;
if (zdjecie.category === 2) return zwierzeta;
return samochody;
}
{zdjecia.filter(czyWidoczne).map((zdjecie) => (
// blok zdjęcia
))}
filtertworzy nową tablicę z tymi elementami, dla których funkcja zwróciłatrue. Pętlamaprysuje już tylko je.- Funkcja leży w komponencie, więc widzi stany przełączników.
- Tablica w stanie się nie zmienia: ukryte zdjęcia wciąż w niej są i wrócą po włączeniu przełącznika.
To samo bez filter: warunek w środku pętli. Gdy zwraca null, React nic nie rysuje.
{zdjecia.map((zdjecie) => czyWidoczne(zdjecie) ? (
<div className="blok" key={zdjecie.id}>...</div>
) : null)}
Licznik pobrań w tablicy
Arkusz mówi, że liczba pobrań ma być zapisana w tablicy obiektów. W React stanu nie zmienia się w miejscu (zdjecie.downloads++ nie odświeży strony). Tworzysz nową tablicę, w której jeden obiekt jest podmieniony na kopię z większą liczbą, i zapisujesz ją przez setZdjecia.
function pobierz(id) {
setZdjecia(zdjecia.map((zdjecie) =>
zdjecie.id === id ? { ...zdjecie, downloads: zdjecie.downloads + 1 } : zdjecie
));
}
<button type="button" className="btn btn-success"
onClick={() => pobierz(zdjecie.id)}>Pobierz</button>
{ ...zdjecie, downloads: … }to kopia obiektu z jednym polem zmienionym. Trzy kropki kopiują wszystkie pola.- Pozostałe zdjęcia przechodzą bez zmian (
: zdjecie). onClick={() => pobierz(zdjecie.id)}: strzałka sprawia, że funkcja wykona się dopiero po kliknięciu. ZapisonClick={pobierz(zdjecie.id)}wywołałby ją przy rysowaniu i zapętlił aplikację.- Liczba siedzi w tablicy, więc nie znika, gdy kategoria zostanie ukryta i pokazana ponownie. Testy to sprawdzają.
Wygląd zdjęć w App.css
Arkusz podaje styl zdjęć: marginesy zewnętrzne 5 pikseli i zaokrąglone rogi. Bloki mają stać jeden obok drugiego. Blok div domyślnie zajmuje całą szerokość, więc dostaje display: inline-block.
img {
width: 200px;
margin: 5px;
border-radius: 10px;
}
.blok {
display: inline-block;
}
- Szerokość 200 pikseli nie jest w arkuszu. Zdjęcia mają 400 pikseli szerokości, a na obrazie z arkusza są mniejsze, więc rozmiar dobierasz sam.
- Zaokrąglenie da też klasa Bootstrapa
roundedna obrazie. Margines 5 pikseli trzeba jednak zapisać w CSS, bo klasy Bootstrapa mają inne wielkości (m-1to 4 piksele). - Pamiętaj o
import './App.css';w komponencie.
Całe zadanie od pustego pliku
Cała galeria. Tablica jest już wklejona, bo kopiowanie z pliku to nie jest nauka. Reszta od zera. Arkusz chce zrzutów: stan początkowy, różne ustawienia przełączników i licznik po kilku kliknięciach.
Lista kontrolna przed „Sprawdź”
- Stan: tablica zdjęć i trzy przełączniki od
true. h1„Kategorie zdjęć” i trzy blokiform-check form-switch form-check-inline.- Funkcja
czyWidoczne, potemfilterimapz blokiem: obraz,h4„Pobrań: …”, przyciskbtn btn-success. - Funkcja
pobierz(id)zsetZdjeciaionClick={() => pobierz(zdjecie.id)}. App.css: margines 5 pikseli,border-radius,.blok { display: inline-block; }.
Menedżer haseł
Arkusz INF.04-01-26.06 (czerwiec 2026), część II. Uproszczony menedżer haseł: formularz z adresem strony, loginem i hasłem, a pod nim podsumowanie zapisanych danych. Wynik widać na stronie. Nowe jest pole wyboru, które wyłącza pole hasła, i podsumowanie, które zmienia się dopiero po kliknięciu.
Tak ma wyglądać wynik
Wpisz adres i login, kliknij „Zapisz”. Potem odznacz „Wygeneruj hasło”, wpisz hasło i zapisz jeszcze raz.
Co mówi arkusz
- Na pulpicie jest archiwum
Web1.7zz plikiemszablon(formularz Bootstrapa w HTML) i plikiemznaki.txtdo części konsolowej. Hasło do archiwum jest w treści arkusza. Arkusz nazywa szablonszablon.html, a w archiwum jestszablon.txt. - Jeden komponent z dwoma blokami: formularz i podsumowanie.
- Nagłówek
h1„Menedżer haseł - wykonał” i po nim twój numer zdającego. - Pola z etykietami „Adres WWW:”, „Login:”, „Hasło:”. Pierwsze dwa mają podpowiedzi „Adres strony WWW” i „Twój login”. Pole hasła ukrywa znaki.
- Pole wyboru jest na początku zaznaczone i wtedy pole hasła jest nieaktywne. Po odznaczeniu staje się aktywne.
- Przycisk „Zapisz” jest niebieski.
- Trzy akapity z marginesem wewnętrznym 0,5 rem, pierwszy niebieski i wiodący. Wypełniają się po zapisaniu: „Adres: …”, „Login: …” oraz „Hasło automatycznie generowane” albo „Hasło: …”.
<form>
<div class="form-group m-4">
<label for="exampleInputEmail1">Email address</label>
<input type="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
</div>
<div class="form-group m-4">
<label for="exampleInputPassword1">Password</label>
<input type="password" class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>
<button type="submit" class="btn ... m-2">Submit</button>
</form>
Szablon z archiwum na JSX
Arkusz daje gotowy formularz w HTML i każe go przerobić na składnię Reacta. Wklejony bez zmian się nie skompiluje. Zmiany są dwojakiego rodzaju: składnia JSX i treść zadania.
| W szablonie | W JSX | Dlaczego |
|---|---|---|
class="form-control" | className="form-control" | składnia JSX |
for="exampleInputEmail1" | htmlFor="adresWww" | składnia JSX i znacząca nazwa |
<input …> | <input … /> | JSX wymaga zamknięcia znacznika |
Email address | Adres WWW: | napis z arkusza |
type="email" | type="text" | adres strony to nie adres e-mail |
placeholder="Enter email" | placeholder="Adres strony WWW" | podpowiedź z arkusza |
class="btn ... m-2" | className="btn btn-primary m-2" | trzy kropki to miejsce na kolor przycisku |
Szablon ma dwa pola, a zadanie trzy: blok z adresem skopiuj, żeby powstał blok loginu. Pole hasła ma już typ password, który ukrywa wpisywane znaki. Tego arkusz wymaga wprost.
Pole wyboru wyłącza hasło
Pole wyboru i pole hasła łączy jedna wartość logiczna w stanie. Ta sama zmienna ustawia zaznaczenie pierwszego i atrybut disabled drugiego. Kiedy zmienisz stan, oba pola zmieniają się razem.
const [generujHaslo, setGenerujHaslo] = useState(true);
<input type="checkbox" id="generujHaslo"
checked={generujHaslo} onChange={(event) => setGenerujHaslo(event.target.checked)} />
<label htmlFor="generujHaslo">Wygeneruj hasło</label>
<input type="password" className="form-control" id="haslo" disabled={generujHaslo} />
useState(true): arkusz każe, żeby pole wyboru było na początku zaznaczone.disabled={generujHaslo}: gdy stan totrue, pole hasła jest nieaktywne, w przeciwnym razie aktywne. Bez warunku i bezif.- Pole wyboru może też dostać klasy Bootstrapa (
form-check-inputiform-check-labelna etykiecie). Arkusz tego nie wymaga.
Stan pól formularza
Podsumowanie pokaże adres, login i hasło, więc program musi je znać. Każde pole dostaje swój stan, value i onChange, tak samo jak w poprzednich zadaniach.
const [adres, setAdres] = useState("");
const [login, setLogin] = useState("");
const [haslo, setHaslo] = useState("");
<input type="text" className="form-control" id="adresWww" placeholder="Adres strony WWW"
value={adres} onChange={(event) => setAdres(event.target.value)} />
Pole hasła ma teraz i disabled, i value. Kolejność atrybutów nie ma znaczenia.
Podsumowanie po zapisaniu
Akapity mają się zmienić dopiero po kliknięciu „Zapisz”. Gdyby wypisywały {adres} wprost, zmieniałyby się przy każdym wpisanym znaku. Dlatego zapisane dane mają osobny stan, a funkcja zatwierdzająca przepisuje do niego wartości z pól.
const [zapisanyAdres, setZapisanyAdres] = useState("");
const [zapisanyLogin, setZapisanyLogin] = useState("");
const [opisHasla, setOpisHasla] = useState("");
function zapiszDane(event) {
event.preventDefault();
setZapisanyAdres(adres);
setZapisanyLogin(login);
setOpisHasla(generujHaslo ? "Hasło automatycznie generowane" : "Hasło: " + haslo);
}
<p className="p-2 lead text-primary">Adres: {zapisanyAdres}</p>
<p className="p-2">Login: {zapisanyLogin}</p>
<p className="p-2">{opisHasla}</p>
warunek ? a : bto krótkiif, który zwraca wartość:a, gdy warunek jest prawdziwy, ibw przeciwnym razie.- Klasy akapitów są w tabeli pomocy arkusza: margines wewnętrzny 0,5 rem to
p-2, niebieski tekst totext-primary, tekst wiodący tolead. - Pole wyboru i pole hasła działają jak wcześniej. Formularz dostaje
onSubmit={zapiszDane}.
Całe zadanie od pustego pliku
Cały menedżer haseł od szkieletu. Szablon z archiwum jest w lekcji o zadaniu. Zrzuty ekranu według arkusza: stan początkowy, zapis z zaznaczonym i z odznaczonym polem wyboru.
Lista kontrolna przed „Sprawdź”
h1z numerem zdającego.- Siedem stanów: trzy pola, pole wyboru (
true) i trzy zapisane wartości. - Pola z
htmlFor/id,form-control, podpowiedziami,type="password"idisabled={generujHaslo}. - Przycisk
btn btn-primary,onSubmitzpreventDefault. - Trzy akapity
p-2, pierwszy teżlead text-primary, trzeci z operatorem?:.
Kalkulator BMI
Zadanie ćwiczeniowe w stylu części II arkusza INF.04. Formularz z dwoma polami liczbowymi liczy wskaźnik masy ciała. Tym razem wynik nie trafia do konsoli, tylko na stronę, i pojawia się dopiero po kliknięciu. Nowe są obliczenia na liczbach z pól i pokazywanie fragmentu strony pod warunkiem.
Tak ma wyglądać wynik
Wpisz 70 i 175, kliknij „Oblicz”. Potem zostaw jedno pole puste albo wpisz 0.
Treść zadania
- Aplikacja w React z jednym komponentem. Całość w
divz klasącontainer, na górze nagłówekh2„Kalkulator BMI”. - Formularz z klasami Bootstrapa: pole liczbowe z etykietą „Waga (kg):”, pole liczbowe z etykietą „Wzrost (cm):” i niebieski przycisk „Oblicz”. Na początku pola są puste. Identyfikatory pól mają być znaczące.
- Zatwierdzenie formularza nie przeładowuje strony. BMI to waga w kilogramach podzielona przez kwadrat wzrostu w metrach.
- Pod formularzem pojawia się akapit „Twoje BMI: 22.86” (zawsze dwa miejsca po przecinku) i akapit z kategorią: „Kategoria: niedowaga” poniżej 18.5, „waga prawidłowa” poniżej 25, „nadwaga” poniżej 30, „otyłość” od 30.
- Gdy któreś pole jest puste, równe zero albo ujemne, zamiast wyniku jest komunikat „Podaj poprawne dane” w bloku z klasami
alert alert-danger. - Przed pierwszym kliknięciem pod formularzem nie ma nic.
Plan
Formularz, stan pól z obliczeniem, kategoria, obsługa błędnych danych, a na końcu całość od pustego pliku.
Formularz z polami liczbowymi
Pole liczbowe to input z type="number". Przeglądarka dorysowuje strzałki i nie pozwala wpisać liter. Reszta wygląda jak w zapisach na kursy: etykieta z htmlFor, pole z tym samym id, oba w bloku form-group.
<div className="form-group mb-3"> <label htmlFor="waga">Waga (kg):</label> <input type="number" className="form-control" id="waga" /> </div>
- Napis etykiety przepisz dokładnie: „Waga (kg):”, z nawiasem i dwukropkiem.
- Przycisk ma
type="submit", żeby zatwierdzał formularz. Kolor niebieski to klasabtn-primary. - Pole liczbowe przyjmuje domyślnie tylko liczby całkowite. Ułamek, np. 72.5, przeglądarka uzna za błąd i nie zatwierdzi formularza. Gdy arkusz chce ułamków, dopisz
step="any".
Stan pól i wynik na stronie
Pola dostają stan jak zawsze. Nowy jest trzeci stan: wynik. Zaczyna od null, czyli „jeszcze nic nie policzono”. Funkcja zatwierdzająca liczy BMI i zapisuje je w stanie, a React rysuje stronę od nowa z wynikiem.
const [waga, setWaga] = useState("");
const [wzrost, setWzrost] = useState("");
const [bmi, setBmi] = useState(null);
function oblicz(event) {
event.preventDefault();
const kg = Number(waga);
const metry = Number(wzrost) / 100;
setBmi(kg / (metry * metry));
}
{bmi !== null && (
<p>Twoje BMI: {bmi.toFixed(2)}</p>
)}
- Pole zawsze daje napis, także
type="number".Number(waga)zamienia go na liczbę. Przy dzieleniu JavaScript zrobiłby to sam, ale przy dodawaniu skleiłby napisy, więc lepiej mieć nawyk. toFixed(2)zaokrągla do dwóch miejsc i zwraca napis: 22.857 daje „22.86”, a 19.5 daje „19.50”.warunek && elementrysuje element tylko przy prawdziwym warunku. Gdybmitonull, React nie rysuje nic.- Wynik liczysz w funkcji po kliknięciu, a nie w
return. Inaczej zmieniałby się przy każdym wpisanym znaku.
Kategoria wyniku
Kategorię wybiera łańcuch warunków. Każdy if kończy funkcję przez return, więc kolejny sprawdza się tylko wtedy, gdy poprzedni nie pasował. Dzięki temu nie trzeba pisać obu granic przedziału.
function kategoria(wartosc) {
if (wartosc < 18.5) return "niedowaga";
if (wartosc < 25) return "waga prawidłowa";
if (wartosc < 30) return "nadwaga";
return "otyłość";
}
<p>Kategoria: {kategoria(bmi)}</p>
- Kolejność ma znaczenie: od najmniejszej granicy w górę. Gdyby pierwszy był warunek
< 30, niedowaga też dostałaby „nadwaga”. - Granice: 18.5 to już waga prawidłowa, a 25 to już nadwaga. Znak
<(a nie<=) daje dokładnie to. - Funkcję wywołujesz w JSX z nawiasami, bo potrzebny jest jej wynik.
Błędne dane
Dla pustego pola wynik byłby Infinity albo NaN. Arkusz w takich zadaniach wymaga komunikatu. Potrzebny jest stan logiczny blad, ustawiany w funkcji zatwierdzającej.
const [blad, setBlad] = useState(false);
function oblicz(event) {
event.preventDefault();
const kg = Number(waga);
const metry = Number(wzrost) / 100;
if (kg <= 0 || metry <= 0) {
setBlad(true);
setBmi(null);
return;
}
setBlad(false);
setBmi(kg / (metry * metry));
}
{blad && <div className="alert alert-danger">Podaj poprawne dane</div>}
Number("")daje 0, więc warunek<= 0łapie naraz puste pole, zero i liczby ujemne.- Przy błędzie trzeba też schować stary wynik:
setBmi(null). Bez tego pod komunikatem zostałoby poprzednie BMI. returnkończy funkcję, więc obliczenie poniżej się nie wykona.alert alert-dangerto czerwony blok Bootstrapa. Zielony toalert-success, żółtyalert-warning.
Całe zadanie od pustego pliku
Cały kalkulator od szkieletu, w tempie egzaminu: około 20 minut. Zrzuty ekranu: pusty formularz, wynik dla poprawnych danych i komunikat dla błędnych.
Lista kontrolna przed „Sprawdź”
import { useState } from 'react';i cztery stany:waga,wzrost(""),bmi(null),blad(false).h2„Kalkulator BMI”, dwa blokiform-groupz polamitype="number",valueionChange.- Przycisk
type="submit",btn btn-primary, napis „Oblicz”. onSubmitna formularzu,preventDefault,Number(), wzrost podzielony przez 100.- Warunek
<= 0:setBlad(true),setBmi(null),return. toFixed(2), funkcjakategoriaz czterema napisami, akapity podbmi !== null &&.
Wpis z polubieniami
Zadanie ćwiczeniowe w stylu części II arkusza INF.04. Nie ma tu formularza: jest karta z wpisem, licznik polubień i lista komentarzy, którą da się schować. Wszystko dzieje się po kliknięciach w przyciski, a wynik widać na stronie.
Tak ma wyglądać wynik
Kliknij kilka razy „Lubię to”, potem „Nie lubię” więcej razy, niż jest polubień. Dobij do 10 i zobacz kolor. Rozwiń komentarze.
Treść zadania
- Aplikacja w React z jednym komponentem i stylami Bootstrapa. Wpis to karta: blok
card, w nimcard-body. - W karcie: nagłówek
h3„Wycieczka w Tatry” (klasacard-title), akapit „Zdjęcia z sobotniego wyjścia na Kasprowy Wierch.” (klasacard-text) i akapit z licznikiem „Polubienia: 0”. - Trzy przyciski: zielony „Lubię to” zwiększa licznik o 1, czerwony „Nie lubię” zmniejsza o 1, ale licznik nigdy nie jest ujemny, „Resetuj” (
btn-outline-secondary) ustawia 0. - Akapit z licznikiem jest szary (
text-secondary), a od 10 polubień zielony (text-success). - Komentarze są w tablicy w kodzie. Przycisk
btn btn-link„Pokaż komentarze (3)” pokazuje je jako listęlist-groupi zmienia napis na „Ukryj komentarze”. Na początku lista jest schowana. Liczba w nawiasie ma wynikać z tablicy.
Plan
Karta, licznik, dolna granica i reset, kolor, komentarze, a na końcu całość od pustego pliku.
Karta Bootstrapa
Karta to ramka z zaokrąglonymi rogami na jedną treść: wpis, produkt, profil. Bootstrap składa ją z bloków o określonych klasach. Zewnętrzny card rysuje ramkę, wewnętrzny card-body daje odstępy, a card-title i card-text ustawiają tytuł i tekst.
<div className="card mt-3">
<div className="card-body">
<h3 className="card-title">Wycieczka w Tatry</h3>
<p className="card-text">Zdjęcia z sobotniego wyjścia na Kasprowy Wierch.</p>
<p>Polubienia: 0</p>
<button type="button" className="btn btn-success me-2">Lubię to</button>
</div>
</div>
- Przyciski poza formularzem mają
type="button". Bez tego przycisk w formularzu by go zatwierdzał. Tu formularza nie ma, ale arkusze często o to pytają. me-2to margines z prawej (margin end), żeby przyciski się nie stykały.mt-3to margines u góry.- Kolory przycisków:
btn-successzielony,btn-dangerczerwony,btn-outline-secondaryszara ramka bez wypełnienia.
Licznik polubień
Licznik to stan liczbowy. Kliknięcie przycisku woła funkcję z onClick, ta ustawia nową wartość, a React rysuje akapit od nowa.
const [polubienia, setPolubienia] = useState(0);
<p>Polubienia: {polubienia}</p>
<button type="button" className="btn btn-success me-2"
onClick={() => setPolubienia(polubienia + 1)}>Lubię to</button>
- W
onClickjest strzałka. ZapisonClick={setPolubienia(polubienia + 1)}wywołałby funkcję już przy rysowaniu i zapętlił aplikację. - Stan zaczyna od liczby
0. Gdyby zaczynał od napisu"0", plus skleiłby napisy i po kliknięciu byłoby „01”. - Przy formularzach zmiana szła przez
onChangepola. Tu nie ma pola: przycisk sam mówi, jak zmienić stan.
Nie mniej niż zero
Gdy zmiana stanu ma warunek, wygodniej napisać osobną funkcję niż wciskać if do atrybutu. Funkcja podpięta bez nawiasów wykona się dopiero po kliknięciu.
function nieLubie() {
if (polubienia > 0) {
setPolubienia(polubienia - 1);
}
}
<button type="button" className="btn btn-danger me-2" onClick={nieLubie}>Nie lubię</button>
<button type="button" className="btn btn-outline-secondary"
onClick={() => setPolubienia(0)}>Resetuj</button>
onClick={nieLubie}podaje funkcję,onClick={nieLubie()}wywołałby ją od razu. Strzałka jest potrzebna dopiero wtedy, gdy trzeba przekazać argument.- Zamiast
ifmożna też napisaćsetPolubienia(Math.max(0, polubienia - 1)). Wynik ten sam.
Kolor licznika
Wartość atrybutu też może być wyrażeniem w klamrach. Operator ?: wybiera jeden z dwóch napisów: przed dwukropkiem ten dla prawdziwego warunku, po nim ten dla fałszywego.
<p className={polubienia >= 10 ? "text-success" : "text-secondary"}>
Polubienia: {polubienia}
</p>
- Klasy kolorów tekstu w Bootstrapie:
text-successzielony,text-dangerczerwony,text-secondaryszary,text-primaryniebieski. - Gdy potrzebna jest stała klasa i jedna zmienna, sklej je:
className={"fw-bold " + (polubienia >= 10 ? "text-success" : "text-secondary")}. Nawias jest konieczny. - „Od 10” to
>= 10. Przy> 10kolor zmieniłby się dopiero przy 11.
Komentarze: pokaż i ukryj
Pokazywanie i chowanie to stan logiczny. Kliknięcie odwraca go wykrzyknikiem, a lista rysuje się pod warunkiem &&. Ten sam stan wybiera napis przycisku.
const [pokazKomentarze, setPokazKomentarze] = useState(false);
<button type="button" className="btn btn-link"
onClick={() => setPokazKomentarze(!pokazKomentarze)}>
{pokazKomentarze ? "Ukryj komentarze" : "Pokaż komentarze (" + komentarze.length + ")"}
</button>
{pokazKomentarze && (
<ul className="list-group">
{komentarze.map((komentarz) => (
<li key={komentarz} className="list-group-item">{komentarz}</li>
))}
</ul>
)}
list-groupilist-group-itemto lista Bootstrapa w ramce, bez kropek.- Tablica komentarzy się nie zmienia, więc nie musi być w stanie. Zwykła stała w komponencie wystarczy.
- Komentarze się nie powtarzają, więc mogą być kluczem. Test dopisze czwarty komentarz i sprawdzi „(4)”.
Całe zadanie od pustego pliku
Cały wpis od szkieletu. Tablica komentarzy jest już w kodzie startowym, jak dane z archiwum. Zrzuty ekranu: stan początkowy, licznik powyżej 10 i rozwinięte komentarze.
Lista kontrolna przed „Sprawdź”
- Dwa stany:
polubienia(0) ipokazKomentarze(false). - Karta:
card→card-body→h3.card-title,p.card-text, akapit z licznikiem. - Trzy przyciski
type="button"z klasamibtn-success,btn-danger,btn-outline-secondary. - Funkcja
nieLubiez warunkiem> 0, reset przezsetPolubienia(0). classNameakapitu z?:i granicą>= 10.- Przycisk
btn btn-link, napis zkomentarze.length, listalist-groupzmapikeypod&&.
Lista zakupów
Zadanie ćwiczeniowe w stylu części II arkusza INF.04. Pierwsza aplikacja, w której tablica naprawdę się zmienia: użytkownik dopisuje do niej produkty i je usuwa. Nowe są dodawanie i usuwanie elementów tablicy w stanie oraz proste sprawdzanie danych przed zapisem.
Tak ma wyglądać wynik
Dodaj „Jajka”. Spróbuj dodać „Mleko” jeszcze raz albo samą spację. Usuń „Chleb”.
Treść zadania
- Aplikacja w React z jednym komponentem i stylami Bootstrapa. Nagłówek
h1„Lista zakupów”. - Formularz: pole tekstowe z etykietą „Produkt:” i niebieski przycisk „Dodaj”.
- Pod formularzem lista produktów w stylu
list-group. Na początku są na niej „Chleb” i „Mleko”. Przy każdym produkcie mały czerwony przycisk „Usuń”. Pod listą akapit „Liczba produktów: 2”. - „Dodaj” dopisuje produkt na koniec listy i czyści pole. Spacje z początku i końca są obcinane, a pusty produkt się nie dodaje. Strona się nie przeładowuje.
- Produktu, który już jest na liście, nie da się dodać drugi raz. Zamiast tego pojawia się komunikat „Ten produkt już jest na liście” w bloku
alert alert-warning. Po udanym dodaniu komunikat znika. - „Usuń” usuwa produkt z listy, a liczba produktów się zmienia.
Plan
Lista z tablicy, dodawanie, sprawdzanie danych, usuwanie, a na końcu całość od pustego pliku.
Lista z tablicy w stanie
W zapisach na kursy tablica była zwykłą stałą, bo się nie zmieniała. Tu użytkownik będzie dopisywał produkty, więc tablica od początku siedzi w stanie. Wyświetla się ją tak samo: map i key.
const [produkty, setProdukty] = useState(["Chleb", "Mleko"]);
<ul className="list-group mt-3">
{produkty.map((nazwa) => (
<li key={nazwa} className="list-group-item">{nazwa}</li>
))}
</ul>
<p className="mt-3">Liczba produktów: {produkty.length}</p>
useStatemoże trzymać dowolną wartość: napis, liczbę, wartość logiczną, tablicę, obiekt.- Nazwa produktu jest kluczem. Działa, bo w następnych lekcjach powtórki będą zablokowane.
- Liczba z
produkty.lengthzmieni się sama po dodaniu i usunięciu.
Dodawanie do tablicy
React odświeża stronę, gdy dostanie przez set… nową wartość. Dla tablicy znaczy to: nie zmieniaj starej, zbuduj nową. Operator ... rozkłada starą tablicę na elementy, a za nimi dopisujesz nowy.
const [produkt, setProdukt] = useState("");
function dodaj(event) {
event.preventDefault();
setProdukty([...produkty, produkt]);
setProdukt("");
}
[...produkty, produkt]dla["Chleb", "Mleko"]i „Jajka” daje["Chleb", "Mleko", "Jajka"].produkty.push(produkt)dopisuje do starej tablicy. React widzi tę samą tablicę i nie rysuje strony od nowa.- Pole jest powiązane ze stanem przez
value, więcsetProdukt("")czyści je na ekranie.
Bez pustych i bez powtórek
Przed zapisem sprawdzasz dane. Każdy warunek, który ma zatrzymać dodawanie, kończy funkcję przez return. Do zapisu dochodzi tylko poprawny produkt.
const [powtorka, setPowtorka] = useState(false);
function dodaj(event) {
event.preventDefault();
const nazwa = produkt.trim();
if (nazwa === "") {
return;
}
if (produkty.includes(nazwa)) {
setPowtorka(true);
return;
}
setProdukty([...produkty, nazwa]);
setProdukt("");
setPowtorka(false);
}
{powtorka && <div className="alert alert-warning mt-3">Ten produkt już jest na liście</div>}
" Ser ".trim()daje „Ser”. Same spacje dają pusty napis, więc łapie je warunek=== "".includeszwracatrue, gdy tablica ma dokładnie taki element. Wielkość liter ma znaczenie: „mleko” i „Mleko” to dwa różne napisy.- Na tablicę trafia
nazwapotrim, a nie surowyprodukt.
Usuwanie z listy
Usuwanie też buduje nową tablicę. filter przechodzi po wszystkich elementach i zostawia te, dla których warunek jest prawdziwy, czyli wszystkie oprócz usuwanego.
function usun(nazwa) {
setProdukty(produkty.filter((p) => p !== nazwa));
}
<li key={nazwa} className="list-group-item">
{nazwa}
<button type="button" className="btn btn-danger btn-sm float-end"
onClick={() => usun(nazwa)}>Usuń</button>
</li>
- Przycisk musi powiedzieć, który produkt usunąć, więc w
onClickjest strzałka z argumentem. Bez strzałkiusun(nazwa)wykonałoby się przy rysowaniu i wyczyściło listę. btn-smto mniejszy przycisk,float-endprzesuwa go do prawej krawędzi.- Przycisk ma
type="button", bo nie zatwierdza formularza.
Całe zadanie od pustego pliku
Cała lista zakupów od szkieletu, około 20 minut. Zrzuty ekranu: stan początkowy, lista po dodaniu produktu, komunikat o powtórce.
Lista kontrolna przed „Sprawdź”
- Trzy stany:
produkty(["Chleb", "Mleko"]),produkt(""),powtorka(false). h1, formularz zonSubmit, blokform-group, pole zvalueionChange, przycisktype="submit".dodaj:preventDefault,trim,returndla pustego,includesdla powtórki,[...produkty, nazwa], czyszczenie pola.- Alert
alert-warningpodpowtorka &&. ul.list-group,mapzkey, przycisk „Usuń” zfilter, akapit zprodukty.length.
Ankieta o stołówce
Zadanie ćwiczeniowe w stylu części II arkusza INF.04. Formularz ma trzy rodzaje pól, których jeszcze nie było razem: przyciski opcji (wybór jednej odpowiedzi), pole wielowierszowe i pole wyboru ze zgodą. Przycisk wysyłania działa dopiero wtedy, gdy formularz jest gotowy.
Tak ma wyglądać wynik
Zauważ, że „Wyślij” jest szary. Wybierz ocenę, zaznacz zgodę i wyślij. Linia pojawi się w konsoli.
Treść zadania
- Aplikacja w React z jednym komponentem i stylami Bootstrapa. Nagłówek
h2„Ankieta o stołówce”. - Formularz: pole tekstowe „Klasa:”, akapit „Jak oceniasz stołówkę?” i trzy przyciski opcji z etykietami Dobrze, Średnio, Źle. Wartości opcji to
dobrze,srednio,zle. Na początku żadna nie jest wybrana. - Pole wielowierszowe „Uwagi:” na trzy wiersze.
- Pole wyboru „Zgadzam się na wysłanie odpowiedzi”, na początku puste.
- Niebieski przycisk „Wyślij”. Jest nieaktywny, dopóki nie ma zgody albo nie wybrano oceny.
- Wysłanie nie przeładowuje strony. W konsoli pojawia się jedna linia, np.
klasa: 3A; ocena: dobrze; uwagi: Za mało warzyw. Przy pustych uwagach zamiast nich jest słowobrak. Pod formularzem pojawia się zielony komunikat „Dziękujemy za udział w ankiecie!” (alert alert-success).
Plan
Przyciski opcji, ich stan, pole wielowierszowe, zgoda z nieaktywnym przyciskiem, wysłanie, a na końcu całość od pustego pliku.
Przyciski opcji
Przycisk opcji to input z type="radio". Z przycisków o tym samym name da się zaznaczyć tylko jeden. Bootstrap rysuje je tymi samymi klasami co pola wyboru: form-check, form-check-input i form-check-label.
<p>Jak oceniasz stołówkę?</p> <div className="form-check"> <input className="form-check-input" type="radio" name="ocena" id="ocenaDobrze" value="dobrze" /> <label className="form-check-label" htmlFor="ocenaDobrze">Dobrze</label> </div> <div className="form-check"> <input className="form-check-input" type="radio" name="ocena" id="ocenaSrednio" value="srednio" /> <label className="form-check-label" htmlFor="ocenaSrednio">Średnio</label> </div>
namełączy przyciski w grupę. Każdy ma własneid, bo każdy ma własną etykietę.valueto to, co dostanie program. Polskie litery zostaw dla etykiety, a wartość pisz bez nich:srednio,zle.- W galerii zdjęć blok miał jeszcze klasę
form-switch, która zamienia kwadrat w przełącznik. Tu jej nie ma.
Stan wybranej opcji
Trzy przyciski, jedna odpowiedź, więc i jeden stan. Trzyma wartość wybranej opcji. Każdy przycisk sprawdza, czy to on: checked jest prawdziwe tylko dla przycisku o tej wartości.
const [ocena, setOcena] = useState("");
<input className="form-check-input" type="radio" name="ocena" id="ocenaDobrze" value="dobrze"
checked={ocena === "dobrze"} onChange={(event) => setOcena(event.target.value)} />
- Stan
""nie równa się żadnej wartości, więc na starcie nic nie jest zaznaczone. - Wszystkie trzy przyciski mają taki sam
onChange.event.target.valueto wartość klikniętego, więc ten sam kod działa dla każdego. - Przy przyciskach opcji czytasz
value, a niechecked: interesuje cię, który został kliknięty, a nie, że został.
Pole wielowierszowe
Na dłuższy tekst jest znacznik textarea. W HTML jego treść stoi między znacznikami, ale w React działa jak zwykłe pole: wartość podajesz w value, a zmianę łapiesz w onChange.
const [uwagi, setUwagi] = useState("");
<div className="form-group mb-3 mt-3">
<label htmlFor="uwagi">Uwagi:</label>
<textarea className="form-control" id="uwagi" rows="3"
value={uwagi} onChange={(event) => setUwagi(event.target.value)}></textarea>
</div>
rows="3"ustawia wysokość na trzy wiersze. Dłuższy tekst się przewija.- Między
<textarea>a</textarea>w React nic nie wpisujesz. Można też zamknąć go od razu:<textarea … />. - Klasa jest ta sama co przy polu tekstowym:
form-control.
Zgoda i nieaktywny przycisk
W menedżerze haseł disabled wyłączało pole. Przycisk wyłącza się tak samo. Warunek może mieć kilka części połączonych operatorami logicznymi.
const [zgoda, setZgoda] = useState(false);
<div className="form-check mb-3">
<input className="form-check-input" type="checkbox" id="zgoda"
checked={zgoda} onChange={(event) => setZgoda(event.target.checked)} />
<label className="form-check-label" htmlFor="zgoda">Zgadzam się na wysłanie odpowiedzi</label>
</div>
<button type="submit" className="btn btn-primary" disabled={!zgoda || ocena === ""}>Wyślij</button>
!zgodato „nie ma zgody”.||to „albo”: przycisk jest wyłączony, gdy brakuje choć jednej rzeczy.- To samo inaczej:
disabled={!(zgoda && ocena !== "")}, czyli „nieprawda, że jest zgoda i jest ocena”. - Nieaktywnego przycisku nie da się kliknąć, więc formularz nie zostanie wysłany.
Wysłanie ankiety
Wynik to jedna linia w konsoli, jak w formularzu filmu. Nowe jest zastępowanie pustej wartości słowem: operator ?: wybiera, co wstawić do napisu. Po wypisaniu stan wyslano pokazuje podziękowanie.
const [wyslano, setWyslano] = useState(false);
function wyslij(event) {
event.preventDefault();
const tekstUwag = uwagi === "" ? "brak" : uwagi;
console.log("klasa: " + klasa + "; ocena: " + ocena + "; uwagi: " + tekstUwag);
setWyslano(true);
}
{wyslano && <div className="alert alert-success mt-3">Dziękujemy za udział w ankiecie!</div>}
- Do konsoli trafia wartość opcji (
zle), a nie napis z etykiety („Źle”). - Wyrażenie z
?:można wstawić od razu do napisu, ale wtedy potrzebny jest nawias:"; uwagi: " + (uwagi === "" ? "brak" : uwagi). - Pilnuj spacji i średników w linii, test porównuje ją znak po znaku.
Całe zadanie od pustego pliku
Cała ankieta od szkieletu, około 25 minut. Zrzuty ekranu: pusty formularz z nieaktywnym przyciskiem, wypełniony formularz, konsola z linią i podziękowanie.
Lista kontrolna przed „Sprawdź”
- Pięć stanów:
klasa,ocena,uwagi(""),zgoda,wyslano(false). - Pole „Klasa:” w
form-group, akapit z pytaniem, trzy opcje z jednymname,checked={ocena === "…"}. textareazrows="3", pole wyboru zevent.target.checked.disabled={!zgoda || ocena === ""}na przyciskutype="submit".onSubmit,preventDefault, jedna linia z „brak” dla pustych uwag, alert podwyslano &&.
Kantor
Zadanie ćwiczeniowe w stylu części II arkusza INF.04. Przelicznik złotych na obce waluty. Nie ma tu przycisku: wynik zmienia się w trakcie pisania. Kursy są w tablicy obiektów, z której powstają i opcje listy, i tabela.
Tak ma wyglądać wynik
Wpisz kwotę i pozmieniaj walutę. Usuń kwotę.
Treść zadania
- Aplikacja w React z jednym komponentem i stylami Bootstrapa. Nagłówek
h1„Kantor”. - W kodzie jest tablica obiektów z kodem waluty i kursem: EUR 4.3, USD 3.95, GBP 5.05, CHF 4.55. Tablica może się zmienić, a aplikacja ma działać bez poprawek.
- Pole liczbowe „Kwota w PLN:” (na początku puste) i lista rozwijana „Waluta:” z kodami walut z tablicy (na początku EUR).
- Pod polami akapit z klasą
lead: „Otrzymasz: 23.26 EUR” (kwota podzielona przez kurs, dwa miejsca po przecinku). Wynik zmienia się od razu przy wpisywaniu i zmianie waluty. Gdy kwota jest pusta, zerowa albo ujemna, akapit pokazuje „Wpisz kwotę”. - Na dole tabela
table table-stripedz kolumnami Waluta i Kurs, kurs z dwoma miejscami po przecinku. - Aplikacja nie ma przycisku ani formularza do zatwierdzania.
Plan
Tabela kursów, pola z opcjami z tablicy, wynik liczony w trakcie pisania, komunikat dla braku kwoty, a na końcu całość od pustego pliku.
Tabela kursów
Tabela z tablicy obiektów to ta sama pętla co lista, tylko każdy obiekt daje wiersz tr, a jego pola komórki td. Nagłówki są w thead, wiersze z pętli w tbody.
<table className="table table-striped">
<thead>
<tr>
<th>Waluta</th>
<th>Kurs</th>
</tr>
</thead>
<tbody>
{kursy.map((k) => (
<tr key={k.kod}>
<td>{k.kod}</td>
<td>{k.kurs.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
tableto podstawowy wygląd tabeli Bootstrapa,table-stripeddodaje paski co drugi wiersz. Inne:table-bordered(ramki),table-hover(podświetlenie wiersza pod myszą).- Klucz dostaje wiersz
tr, bo to on powstaje w pętli. Kod waluty jest unikalny. - Bez
tbodyReact wypisze w konsoli ostrzeżenie, żetrnie może leżeć wprost wtable. 4.3.toFixed(2)daje „4.30”. Liczba w tablicy zostaje bez zmian, zmienia się tylko to, co widać.
Opcje listy z tablicy
W formularzu filmu opcje były wpisane ręcznie. Gdy dane są w tablicy, opcje robi pętla, więc nowa waluta w tablicy sama pojawi się na liście.
const [kwota, setKwota] = useState("");
const [waluta, setWaluta] = useState("EUR");
<select className="form-select" id="waluta"
value={waluta} onChange={(event) => setWaluta(event.target.value)}>
{kursy.map((k) => (
<option key={k.kod} value={k.kod}>{k.kod}</option>
))}
</select>
- Stan
"EUR"wybiera na starcie opcję o tej wartości. Pustej opcji tu nie ma, bo arkusz chce wybranej waluty od początku. - Wartość opcji to kod waluty, a nie kurs. Kurs znajdziesz po kodzie w następnej lekcji. Dzięki temu w stanie jest coś czytelnego.
- Pól nie ma w znaczniku
form. Bez przycisku nie ma czego zatwierdzać, a blokiform-groupdziałają także poza formularzem.
Wynik w trakcie pisania
Każda zmiana stanu rysuje komponent od nowa: React wykonuje całą funkcję App jeszcze raz. Stała policzona nad return ze stanu jest więc zawsze aktualna i nie potrzebuje własnego stanu ani przycisku.
const wybrana = kursy.find((k) => k.kod === waluta);
const zlote = Number(kwota);
<p className="lead">Otrzymasz: {(zlote / wybrana.kurs).toFixed(2)} {waluta}</p>
findzwraca pierwszy element, dla którego warunek jest prawdziwy. Tu: obiekt waluty z wybranym kodem.- Nawias wokół dzielenia jest potrzebny: bez niego
toFixeddziałałoby na samym kursie. - Spacja między
{…}a{waluta}w jednej linii zostaje na stronie: „23.26 EUR”. leadto klasa Bootstrapa na większy, wyróżniony akapit.
Brak kwoty
Przy pustym polu wynik to „0.00 EUR”, a przy ujemnym bzdura. Akapit wybiera więc jeden z dwóch napisów operatorem ?:. Cały napis z wynikiem składasz plusem.
<p className="lead">
{zlote > 0 ? "Otrzymasz: " + (zlote / wybrana.kurs).toFixed(2) + " " + waluta : "Wpisz kwotę"}
</p>
- Warunek
> 0łapie naraz puste pole (Number("")to 0), zero i liczby ujemne. - Zamiast jednego akapitu z
?:możesz napisać dwa z&&: jeden dlazlote > 0, drugi dlazlote <= 0. Działa tak samo.
Całe zadanie od pustego pliku
Cały kantor od szkieletu, około 20 minut. Tablica kursów jest w kodzie startowym. Zrzuty ekranu: stan początkowy z „Wpisz kwotę”, wynik dla EUR i dla innej waluty.
Lista kontrolna przed „Sprawdź”
- Dwa stany:
kwota("") iwaluta("EUR"). Wynik nie ma własnego stanu. h1„Kantor”, dwa blokiform-group, poletype="number",selectz opcjami zkursy.mapikey.findpo kodzie,Number(kwota), akapitleadz?:itoFixed(2).- Tabela
table table-striped,theadz Waluta i Kurs,tbodyzmap, kurs przeztoFixed(2). - Bez przycisku i bez
onSubmit.
Dziennik ocen
Zadanie ćwiczeniowe w stylu części II arkusza INF.04. Tabela uczniów z ocenami i średnimi, a pod nią formularz, który dopisuje ocenę wybranemu uczniowi. To najwięcej logiki w kursie: liczenie średniej, wyróżnianie wierszy i zmiana tablicy schowanej w obiekcie w stanie.
Tak ma wyglądać wynik
Dodaj Celinie dwie wysokie oceny i zobacz, jak zmienia się średnia i kolor wiersza. Spróbuj oceny 7.
Treść zadania
- Aplikacja w React z jednym komponentem i stylami Bootstrapa. Nagłówek
h1„Dziennik ocen”. - Dane są w kodzie: tablica czterech uczniów, każdy z polami
id,imie,nazwiskoi tablicąoceny. Liczba uczniów może się zmienić. - Tabela z obramowaniem (
table table-bordered) i kolumnami: Lp. (od 1), Imię i nazwisko, Oceny (rozdzielone przecinkiem i spacją), Średnia (dwa miejsca po przecinku). Wiersz ucznia ze średnią co najmniej 4.75 ma zielone tło (table-success). - Pod tabelą akapit „Średnia klasy: 3.83”, liczony ze wszystkich ocen.
- Niżej nagłówek
h3„Dodaj ocenę” i formularz: lista „Uczeń:” z imionami i nazwiskami (na początku pierwszy uczeń), pole liczbowe „Ocena:” i niebieski przycisk „Dodaj ocenę”. - Zatwierdzenie bez przeładowania dopisuje ocenę na koniec ocen wybranego ucznia, a tabela i średnie od razu się zmieniają. Pole oceny jest potem puste.
- Ocena, która nie jest liczbą całkowitą od 1 do 6, nic nie zmienia, tylko pokazuje komunikat „Ocena musi być liczbą od 1 do 6” (
alert alert-danger).
Plan
Tabela, średnia ucznia, wyróżnienie i średnia klasy, formularz, dopisywanie oceny, a na końcu całość od pustego pliku.
Tabela z tablicy obiektów
Każdy uczeń to wiersz, a jego pola to komórki. Dwie rzeczy są tu nowe: numer porządkowy z pętli i tablica ocen zamieniona na jeden napis.
<tbody>
{uczniowie.map((u, i) => (
<tr key={u.id}>
<td>{i + 1}</td>
<td>{u.imie} {u.nazwisko}</td>
<td>{u.oceny.join(", ")}</td>
</tr>
))}
</tbody>
- Funkcja w
mapdostaje drugi argument: numer elementu od zera. Lp. toi + 1. Poleidnie nadaje się na Lp., bo nie musi iść po kolei. join(", ")skleja elementy tablicy w napis z przecinkiem i spacją między nimi. Sama tablica w klamrach dałaby „565”.- Spacja między
{u.imie}a{u.nazwisko}zostaje na stronie. table-borderedrysuje ramki wokół wszystkich komórek.
Średnia ucznia
Średnia to suma podzielona przez liczbę ocen. Sumę tablicy liczy metoda reduce: przechodzi po elementach i za każdym razem dokłada element do wyniku z poprzedniego kroku.
function srednia(oceny) {
return oceny.reduce((suma, x) => suma + x, 0) / oceny.length;
}
<td>{srednia(u.oceny).toFixed(2)}</td>
- Dla [5, 6, 5]: suma 0 + 5 = 5, potem 5 + 6 = 11, potem 11 + 5 = 16. Zero na końcu
reduceto wartość początkowa. - To samo pętlą:
let suma = 0; for (const x of oceny) suma += x;. Na egzaminie oba zapisy są dobre. - Funkcja dostaje tablicę ocen, a nie ucznia. Dzięki temu w następnej lekcji policzy też średnią klasy.
Wyróżnienie i średnia klasy
Klasa wiersza zależy od średniej, więc wybiera ją ?:, jak kolor licznika polubień. Średnia klasy to średnia wszystkich ocen naraz: wystarczy złożyć je w jedną tablicę i podać tej samej funkcji.
const wszystkieOceny = uczniowie.flatMap((u) => u.oceny);
<tr key={u.id} className={srednia(u.oceny) >= 4.75 ? "table-success" : ""}>
<p>Średnia klasy: {srednia(wszystkieOceny).toFixed(2)}</p>
flatMaprobi to comap, a potem spłaszcza wynik: z czterech tablic po trzy oceny powstaje jedna tablica dwunastu ocen.- Pusty napis w
classNameoznacza „bez klasy”. - Inne tła wierszy:
table-danger(czerwone),table-warning(żółte),table-info(niebieskie).
Formularz dodawania oceny
Lista uczniów powstaje z tej samej tablicy co tabela. Wartością opcji jest id, bo po nim w następnej lekcji znajdziesz ucznia w tablicy.
const [uczen, setUczen] = useState("1");
const [ocena, setOcena] = useState("");
<select className="form-select" id="uczen"
value={uczen} onChange={(event) => setUczen(event.target.value)}>
{uczniowie.map((u) => (
<option key={u.id} value={u.id}>{u.imie} {u.nazwisko}</option>
))}
</select>
- Stan zaczyna od
"1", napisu, bo lista zawsze daje napisy. Wybrany jest wtedy uczeń oid1. - Atrybuty
min="1"imax="6"kazałyby przeglądarce zablokować złą ocenę, zanim zobaczy ją kod. Arkusz chce komunikatu z aplikacji, więc ich nie dodawaj.
Dopisanie oceny w tablicy
Ocena trafia do tablicy, która siedzi w obiekcie, który siedzi w tablicy w stanie. Zasada jest ta sama co w galerii: niczego nie zmieniasz w miejscu. map buduje nową tablicę uczniów, w niej jeden nowy obiekt ucznia, a w nim nowa tablica ocen.
function dodajOcene(event) {
event.preventDefault();
const liczba = Number(ocena);
if (!Number.isInteger(liczba) || liczba < 1 || liczba > 6) {
setBlad(true);
return;
}
setUczniowie(uczniowie.map((u) =>
u.id === Number(uczen) ? { ...u, oceny: [...u.oceny, liczba] } : u
));
setOcena("");
setBlad(false);
}
{ ...u, oceny: … }to kopia ucznia z nowym polemoceny. Pozostałe pola przechodzą bez zmian.[...u.oceny, liczba]to nowa tablica ocen z jedną więcej na końcu.u.idto liczba, auczennapis z listy.===nie uzna 2 i „2” za równe, stądNumber(uczen).- Puste pole daje 0, ułamek nie przejdzie
Number.isInteger, a 7 nie przejdzie> 6.
Całe zadanie od pustego pliku
Cały dziennik od szkieletu, około 30 minut. Tablica uczniów jest w kodzie startowym. Zrzuty ekranu: stan początkowy, tabela po dodaniu oceny, komunikat o złej ocenie.
Lista kontrolna przed „Sprawdź”
- Stany:
uczniowie(gotowy),uczen("1"),ocena(""),blad(false). - Funkcja
sredniazreduce, użyta w kolumnie, w klasie wiersza i w średniej klasy (flatMap). - Tabela
table table-bordered,theadz czterema nagłówkami,map((u, i) => …),i + 1,join(", "),toFixed(2). h3, lista zvalue={u.id}, poletype="number"bezmin/max, przycisk „Dodaj ocenę”.dodajOcene:preventDefault,Number.isIntegeri zakres,mapz kopią ucznia, czyszczenie pola, alert podblad &&.
Filmoteka
Zadanie ćwiczeniowe w stylu części II arkusza INF.04. Lista filmów z wyszukiwarką, wyborem gatunku i sortowaniem. Jak w kantorze, nie ma przycisku: lista zmienia się w trakcie pisania. W galerii zdjęć filtrem były przełączniki, tu są tekst i lista rozwijana naraz.
Tak ma wyglądać wynik
Wpisz „mi”, potem wybierz gatunek. Wpisz coś, czego nie ma. Zaznacz „Od najnowszych”.
Treść zadania
- Aplikacja w React z jednym komponentem i stylami Bootstrapa. Nagłówek
h1„Filmoteka”. - Dane są w kodzie: tablica ośmiu filmów z polami
id,tytul,gatunek,rok. Liczba filmów może się zmienić. - Pole tekstowe „Szukaj:” z podpowiedzią „Wpisz fragment tytułu”, lista „Gatunek:” z opcjami Wszystkie, Komedia, Dramat, Animacja i pole wyboru „Od najnowszych”.
- Akapit „Znaleziono: 8” z liczbą widocznych filmów, a pod nim bloki filmów z ramką i zaokrąglonymi rogami: tytuł w
h5, pod nim „komedia, 1981”. - Widać filmy, których tytuł zawiera wpisany tekst (wielkość liter i spacje na brzegach nie mają znaczenia) i które mają wybrany gatunek. „Wszystkie” nie ogranicza gatunku.
- Gdy nic nie pasuje, pod licznikiem jest szary akapit „Brak filmów spełniających kryteria”.
- „Od najnowszych” układa widoczne filmy od najnowszego. Bez zaznaczenia kolejność jest taka jak w tablicy.
Plan
Bloki filmów, szukanie, gatunek, komunikat o braku wyników, sortowanie, a na końcu całość od pustego pliku.
Bloki filmów
Każdy film to blok z ramką. Klasy Bootstrapa robią wygląd bez własnego CSS: border rysuje ramkę, rounded zaokrągla rogi, p-2 daje odstęp w środku, mb-2 odstęp pod blokiem.
<p>Znaleziono: {filmy.length}</p>
{filmy.map((f) => (
<div key={f.id} className="border rounded p-2 mb-2">
<h5>{f.tytul}</h5>
<p className="mb-0">{f.gatunek}, {f.rok}</p>
</div>
))}
mb-0zabiera akapitowi domyślny odstęp od dołu, żeby blok nie był za wysoki.- Tablica filmów się nie zmienia, więc jest zwykłą stałą. Zmieniać się będzie to, co widać, a to policzysz z niej przy rysowaniu.
Szukanie po tytule
To, co widać, liczysz nad return, jak wynik w kantorze. filter zostawia filmy, których tytuł zawiera tekst z pola. Pętla w JSX idzie już po wyniku filtrowania, a nie po całej tablicy.
const [szukaj, setSzukaj] = useState("");
const widoczne = filmy.filter((f) =>
f.tytul.toLowerCase().includes(szukaj.trim().toLowerCase())
);
<p>Znaleziono: {widoczne.length}</p>
{widoczne.map((f) => ( … ))}
includesrozróżnia wielkość liter: „Miś” nie zawiera „mi”. Dlatego obie strony zamieniasz na małe litery.- Pusty napis zawiera się w każdym tytule, więc przy pustym polu widać wszystko. Nie trzeba osobnego warunku.
- „ś” to inna litera niż „s”: szukanie „s” nie znajdzie „Miś”.
Gatunek z listy
Drugi filtr to drugi warunek w tym samym filter. Film przechodzi, gdy spełnia oba. Opcja „Wszystkie” ma pustą wartość, a pusta wartość ma przepuszczać każdy gatunek.
const [gatunek, setGatunek] = useState("");
const widoczne = filmy.filter((f) =>
f.tytul.toLowerCase().includes(szukaj.trim().toLowerCase()) &&
(gatunek === "" || f.gatunek === gatunek)
);
<select className="form-select" id="gatunek" value={gatunek} onChange={…}>
<option value="">Wszystkie</option>
<option value="komedia">Komedia</option>
…
</select>
- Nawias wokół
gatunek === "" || …jest konieczny.&&wiąże mocniej niż||, więc bez nawiasu „Wszystkie” przepuściłoby każdy film bez względu na szukanie. - Wartość opcji to
komediaz małej litery, bo tak jest w danych. Napis może być z wielkiej.
Brak wyników
Pusta lista wygląda jak błąd aplikacji. Arkusze zwykle chcą wtedy komunikatu. Warunek sprawdza długość tablicy z wynikiem.
{widoczne.length === 0 && (
<p className="text-muted">Brak filmów spełniających kryteria</p>
)}
- Pułapka:
{widoczne.length && …}przy zerze wypisze na stronie cyfrę 0. React nie rysujefalse, ale liczbę 0 rysuje. Dlatego pełne porównanie=== 0. text-mutedto szary, przygaszony tekst.
Sortowanie od najnowszych
sort z funkcją porównującą układa tablicę. Funkcja dostaje dwa elementy i zwraca liczbę: ujemną, gdy pierwszy ma być wcześniej, dodatnią, gdy później. b.rok - a.rok daje kolejność malejącą.
const [najnowsze, setNajnowsze] = useState(false);
let widoczne = filmy.filter((f) => …);
if (najnowsze) {
widoczne = [...widoczne].sort((a, b) => b.rok - a.rok);
}
- W przeciwieństwie do
mapifilter,sortzmienia tablicę, na której działa. Gdyby to była tablicafilmy, po odznaczeniu nie byłoby już do czego wrócić. Kopia[...widoczne]to wyklucza. letzamiastconst, bo zmienna dostaje drugą wartość. To samo jednym wyrażeniem:const lista = najnowsze ? [...widoczne].sort(…) : widoczne;.a.rok - b.rokdałoby kolejność rosnącą, od najstarszych.
Całe zadanie od pustego pliku
Cała filmoteka od szkieletu, około 25 minut. Tablica filmów jest w kodzie startowym. Zrzuty ekranu: wszystkie filmy, wynik szukania z gatunkiem, komunikat o braku wyników, lista od najnowszych.
Lista kontrolna przed „Sprawdź”
- Trzy stany:
szukaj,gatunek(""),najnowsze(false). filterz dwoma warunkami:toLowerCase,trim,includesi(gatunek === "" || …).- Sortowanie kopii:
[...widoczne].sort((a, b) => b.rok - a.rok). - Trzy kontrolki z etykietami i klasami:
form-controlzplaceholder,form-selectz czterema opcjami,form-check. - „Znaleziono: …” z
length, komunikat pod=== 0 &&, blokiborder rounded p-2 mb-2zkey.
Formularz kontaktowy
Zadanie ćwiczeniowe w stylu części II arkusza INF.04. Formularz z trzema polami, który przed wysłaniem sprawdza dane i pokazuje wszystkie błędy naraz. Nowe są: tablica błędów w stanie, lista w alercie i czyszczenie formularza po udanym wysłaniu.
Tak ma wyglądać wynik
Kliknij „Wyślij” bez wpisywania czegokolwiek. Potem popraw pola po kolei i wyślij jeszcze raz.
Treść zadania
- Aplikacja w React z jednym komponentem i stylami Bootstrapa. Nagłówek
h2„Kontakt”. - Formularz: pole tekstowe „Imię:”, pole adresu e-mail „E-mail:”, pole wielowierszowe „Wiadomość:” na cztery wiersze i niebieski przycisk „Wyślij”. Przeglądarka nie sprawdza pól sama: robi to aplikacja.
- Pod polem wiadomości mały napis „Znaków: 0”, zmieniający się w trakcie pisania.
- Wysłanie nie przeładowuje strony. Aplikacja sprawdza: imię nie jest puste ani złożone ze spacji („Podaj imię”), adres zawiera znak @ („Adres e-mail musi zawierać znak @”), wiadomość bez spacji na brzegach ma co najmniej 10 znaków („Wiadomość musi mieć co najmniej 10 znaków”).
- Wszystkie znalezione błędy są w czerwonym bloku
alert alert-dangerjako lista, w kolejności pól. - Bez błędów pojawia się zielony blok
alert alert-success: „Dziękujemy, Anna! Odpowiemy na adres anna@poczta.pl.”, a pola są czyszczone. Przy błędach pola zostają, a podziękowanie znika.
Plan
Formularz, stan pól z licznikiem znaków, lista błędów, podziękowanie z czyszczeniem pól, a na końcu całość od pustego pliku.
Formularz z wiadomością
Pole type="email" na telefonie pokazuje klawiaturę z małpą, a przeglądarka sama sprawdza, czy adres wygląda poprawnie. Tu sprawdzać ma aplikacja, więc formularz dostaje noValidate: przeglądarka nie zablokuje wysłania swoim dymkiem.
<form noValidate>
<div className="form-group mb-3">
<label htmlFor="email">E-mail:</label>
<input type="email" className="form-control" id="email" />
</div>
<div className="form-group mb-3">
<label htmlFor="wiadomosc">Wiadomość:</label>
<textarea className="form-control" id="wiadomosc" rows="4"></textarea>
</div>
…
</form>
- W JSX
noValidatepisze się z wielkim V, jakhtmlForiclassName. Sama nazwa, bez wartości, znaczytrue. - Bez
noValidateadres „ania.pl” zatrzymałaby przeglądarka i nigdy nie zobaczyłbyś swojego komunikatu.
Stan pól i licznik znaków
Trzy pola, trzy stany. Licznik znaków to tylko wyrażenie w JSX: długość napisu ze stanu. Każdy wpisany znak zmienia stan, więc licznik zmienia się razem z nim.
const [wiadomosc, setWiadomosc] = useState("");
<textarea className="form-control" id="wiadomosc" rows="4"
value={wiadomosc} onChange={(event) => setWiadomosc(event.target.value)}></textarea>
<small className="form-text">Znaków: {wiadomosc.length}</small>
form-textto klasa Bootstrapa na drobny napis pomocniczy pod polem.lengthliczy wszystkie znaki, także spacje: „Dzień dobry” ma 11.
Lista błędów
Zamiast jednego stanu logicznego na każdy błąd wygodniej trzymać w stanie tablicę komunikatów. Funkcja buduje ją od zera przy każdym wysłaniu. Pusta tablica znaczy „wszystko w porządku”.
const [bledy, setBledy] = useState([]);
function wyslij(event) {
event.preventDefault();
const nowe = [];
if (imie.trim() === "") {
nowe.push("Podaj imię");
}
if (!email.includes("@")) {
nowe.push("Adres e-mail musi zawierać znak @");
}
if (wiadomosc.trim().length < 10) {
nowe.push("Wiadomość musi mieć co najmniej 10 znaków");
}
setBledy(nowe);
}
{bledy.length > 0 && (
<div className="alert alert-danger mt-3">
<ul className="mb-0">
{bledy.map((blad) => <li key={blad}>{blad}</li>)}
</ul>
</div>
)}
pushna tablicynowejest w porządku: to zwykła zmienna w funkcji, nie stan. Do stanu trafia dopiero gotowa tablica przezsetBledy.- Warunki są osobnymi
if, bezelse, żeby złapać wszystkie błędy naraz. - Komunikaty są różne, więc mogą być kluczem.
Podziękowanie i czyszczenie pól
Po udanym wysłaniu formularz wraca do stanu początkowego, a nad nim zostaje podziękowanie z danymi, które przed chwilą były w polach. Napis trzeba więc złożyć przed wyczyszczeniem.
const [podziekowanie, setPodziekowanie] = useState("");
setBledy(nowe);
if (nowe.length > 0) {
setPodziekowanie("");
return;
}
setPodziekowanie("Dziękujemy, " + imie.trim() + "! Odpowiemy na adres " + email + ".");
setImie("");
setEmail("");
setWiadomosc("");
{podziekowanie !== "" && <div className="alert alert-success mt-3">{podziekowanie}</div>}
setImie("")nie zmienia od razu zmiennejimiew tej funkcji: nowa wartość będzie dopiero przy następnym rysowaniu. Mimo to składaj napis wcześniej, tak czyta się kod łatwiej.- Przy błędach pola zostają, żeby użytkownik poprawił tylko to, co złe.
Całe zadanie od pustego pliku
Cały formularz od szkieletu, około 25 minut. Zrzuty ekranu: wszystkie trzy błędy, dwa błędy po częściowym poprawieniu, podziękowanie.
Lista kontrolna przed „Sprawdź”
- Pięć stanów:
imie,email,wiadomosc,podziekowanie("") ibledy([]). <form noValidate onSubmit={wyslij}>, polatext,email,textarea rows="4",small.form-textzlength.wyslij:preventDefault, lokalna tablica, trzy osobneif,setBledy, przy błędachreturn.- Bez błędów: podziękowanie z
imie.trim()i adresem, potem trzy razy czyszczenie. alert-dangerzulimappodbledy.length > 0,alert-successpodpodziekowanie !== "".
Sklep z koszykiem
Zadanie ćwiczeniowe w stylu części II arkusza INF.04. Karty produktów i koszyk, który liczy ilości, wartości, dostawę i kwotę do zapłaty. W stanie jest tylko koszyk. Wszystkie kwoty wynikają z niego przy każdym rysowaniu.
Tak ma wyglądać wynik
Dodaj kilka razy ten sam owoc, potem inny. Dobij do 30 zł i zobacz dostawę. Usuń wszystko.
Treść zadania
- Aplikacja w React z jednym komponentem i stylami Bootstrapa. Nagłówek
h1„Sklep z owocami”. - Dane są w kodzie: tablica produktów z polami
id,nazwa,cena. Liczba produktów może się zmienić. - Produkty są kartami Bootstrapa w siatce: cztery w rzędzie na szerokim ekranie. Karta ma nazwę w
h4, akapit „Cena: 3.49 zł” i niebieski przycisk „Do koszyka”. - Pod kartami nagłówek
h2„Koszyk”. Pusty koszyk to akapit „Koszyk jest pusty”. Pełny to tabela z kolumnami Produkt, Ilość, Wartość i przyciskiem „Usuń” w każdym wierszu. - „Do koszyka” dodaje produkt z ilością 1, a przy produkcie, który już jest w koszyku, zwiększa ilość. „Usuń” usuwa wszystkie sztuki produktu.
- Pod tabelą akapit „Dostawa: 9.99 zł”, a od 30 zł wartości produktów „Dostawa: gratis”, i pogrubiony akapit „Do zapłaty: … zł” (produkty plus dostawa).
- Wszystkie kwoty mają dwa miejsca po przecinku.
Plan
Karty produktów, dodawanie do koszyka, wartość i suma, usuwanie z pustym koszykiem, dostawa, a na końcu całość od pustego pliku.
Karty produktów
Siatka Bootstrapa dzieli szerokość na 12 części. Kolumny leżą w bloku row. Klasa col-md-3 daje kolumnie 3 części, czyli cztery kolumny w rzędzie, od średniej szerokości ekranu. Na wąskim ekranie kolumny stają jedna pod drugą.
<div className="row">
{produkty.map((p) => (
<div key={p.id} className="col-md-3">
<div className="card mb-3">
<div className="card-body">
<h4 className="card-title">{p.nazwa}</h4>
<p className="card-text">Cena: {p.cena.toFixed(2)} zł</p>
<button type="button" className="btn btn-primary">Do koszyka</button>
</div>
</div>
</div>
))}
</div>
- Klucz dostaje zewnętrzny element z pętli, czyli kolumna.
- Cena 6.5 w danych jest na stronie jako „6.50”:
toFixed(2). - Podgląd w ćwiczeniu jest wąski, więc karty mogą tam stać jedna pod drugą. W pełnym oknie przeglądarki staną obok siebie.
Dodawanie do koszyka
Koszyk trzyma tylko id produktu i ilość. Nazwę i cenę bierzesz z tablicy produktów. Dzięki temu cena jest w jednym miejscu. Dodawanie ma dwa przypadki: produktu nie ma w koszyku albo już jest.
const [koszyk, setKoszyk] = useState([]);
function dodaj(id) {
if (koszyk.some((p) => p.id === id)) {
setKoszyk(koszyk.map((p) => (p.id === id ? { ...p, ilosc: p.ilosc + 1 } : p)));
} else {
setKoszyk([...koszyk, { id: id, ilosc: 1 }]);
}
}
function produkt(id) {
return produkty.find((p) => p.id === id);
}
<td>{produkt(p.id).nazwa}</td>
<td>{p.ilosc}</td>
somezwracatrue, gdy choć jeden element spełnia warunek.- Zwiększanie ilości to ten sam wzór co licznik pobrań w galerii, a dopisywanie to wzór z listy zakupów.
- Przycisk w karcie przekazuje id:
onClick={() => dodaj(p.id)}.
Wartość i suma
Wartość pozycji to cena razy ilość. Suma koszyka to reduce po pozycjach, jak średnia w dzienniku ocen. Obie liczby wynikają z koszyka, więc nie mają własnego stanu.
const suma = koszyk.reduce((razem, p) => razem + produkt(p.id).cena * p.ilosc, 0);
<td>{(produkt(p.id).cena * p.ilosc).toFixed(2)} zł</td>
<p className="fw-bold">Do zapłaty: {suma.toFixed(2)} zł</p>
- Mnożenie w nawiasie, bo
toFixedma działać na wyniku, a nie na ilości. - Ceny z groszami w JavaScripcie dają czasem wyniki w rodzaju 12.969999999.
toFixed(2)zaokrągla to przy wyświetlaniu. fw-boldto pogrubienie tekstu.
Usuwanie i pusty koszyk
Pusta tabela z samym nagłówkiem wygląda źle. Operator ?: wybiera w JSX jeden z dwóch całych fragmentów strony: akapit albo blok z tabelą.
function usun(id) {
setKoszyk(koszyk.filter((p) => p.id !== id));
}
{koszyk.length === 0 ? (
<p>Koszyk jest pusty</p>
) : (
<div>
<table className="table">…</table>
<p className="fw-bold">Do zapłaty: {suma.toFixed(2)} zł</p>
</div>
)}
- Każda gałąź
?:zwraca jeden element. Tabelę i akapit z sumą trzeba więc owinąć wdiv. - Nawiasy wokół gałęzi pozwalają rozpisać JSX na kilka linii.
btn-outline-dangerto czerwona ramka bez wypełnienia, mniej krzykliwa niż pełny czerwony przycisk.
Dostawa gratis od 30 zł
Koszt dostawy wynika z sumy, więc to kolejna stała nad return. Liczbę zero łatwiej potem dodać do sumy niż napis „gratis”, dlatego napis wybierasz dopiero przy wyświetlaniu.
const dostawa = suma >= 30 ? 0 : 9.99;
<p>Dostawa: {dostawa === 0 ? "gratis" : dostawa.toFixed(2) + " zł"}</p>
<p className="fw-bold">Do zapłaty: {(suma + dostawa).toFixed(2)} zł</p>
- „Od 30 zł” to
>= 30. Liczy się wartość produktów, bez dostawy. - Stała
dostawamusi stać podsuma, bo z niej korzysta.
Całe zadanie od pustego pliku
Cały sklep od szkieletu, około 30 minut. Tablica produktów jest w kodzie startowym. Zrzuty ekranu: pusty koszyk, koszyk z płatną dostawą, koszyk z darmową dostawą.
Lista kontrolna przed „Sprawdź”
- Jeden stan:
koszyk([]) z obiektami{ id, ilosc }. - Funkcje
dodaj(some,mapalbo[...]),usun(filter),produkt(find). - Stałe
suma(reduce) idostawa(?:) nadreturn. row→col-md-3zkey→card mb-3→card-bodyzh4.card-title,p.card-text, przyciskiem.h2„Koszyk”,?:z akapitem albo tabelą, przycisk „Usuń” w wierszu, „Dostawa: …” ifw-bold„Do zapłaty: …”, wszędzietoFixed(2).
Zamówienie pizzy
Zadanie ćwiczeniowe w stylu części II arkusza INF.04, najdłuższe w kursie. Składa w całość prawie wszystko z poprzednich zadań: listę z tablicy, cenę liczoną w trakcie wyboru, warunek na liczbę, nieaktywny przycisk i podsumowanie zapisane po kliknięciu. Nowe są pola wyboru z pętli, których stan to jedna tablica.
Tak ma wyglądać wynik
Wybierz rozmiar i kilka dodatków, zmień liczbę sztuk. Wpisz 6 sztuk. Kliknij „Zamów”.
Treść zadania
- Aplikacja w React z jednym komponentem i stylami Bootstrapa. Nagłówek
h1„Pizzeria”. - Dane są w kodzie: tablica rozmiarów (
id,nazwa,cena) i tablica dodatków (to samo). Obie mogą się zmienić. - Lista rozwijana „Rozmiar:” z opcjami „mała (25 zł)”, „średnia (32 zł)”, „duża (39 zł)”, na początku mała.
- Akapit „Dodatki:” i pod nim pola wyboru z etykietami w rodzaju „Podwójny ser (+4 zł)”, na początku puste.
- Pole liczbowe „Liczba sztuk:”, na początku 1.
- Akapit z klasą
lead: „Cena: 25.00 zł”, czyli cena rozmiaru plus cena dodatków, razy liczba sztuk, liczona w trakcie wyboru. Gdy liczba sztuk nie jest całkowita od 1 do 5, zamiast ceny jest żółty komunikat „Liczba sztuk musi być od 1 do 5”, a przycisk jest nieaktywny. - Zielony przycisk „Zamów”. Zamówienie nie przeładowuje strony i pokazuje pod formularzem kartę: „Zamówienie: duża pizza, dodatki: Podwójny ser, Salami, sztuk: 2, razem: 96.00 zł”. Dodatki są w kolejności z tablicy, a bez dodatków jest „brak”. Karta zmienia się dopiero przy kolejnym zamówieniu.
Plan
Rozmiar z ceną, dodatki jako tablica wybranych, cena z dodatkami, liczba sztuk, podsumowanie, a na końcu całość od pustego pliku.
Rozmiar z tablicy
Opcje listy i cena biorą się z jednej tablicy, jak w kantorze. W stanie jest id rozmiaru, a resztę danych znajduje find.
const [rozmiar, setRozmiar] = useState("mala");
const wybranyRozmiar = rozmiary.find((r) => r.id === rozmiar);
const cena = wybranyRozmiar.cena;
<select className="form-select" id="rozmiar"
value={rozmiar} onChange={(event) => setRozmiar(event.target.value)}>
{rozmiary.map((r) => (
<option key={r.id} value={r.id}>{r.nazwa} ({r.cena} zł)</option>
))}
</select>
<p className="lead mt-3">Cena: {cena.toFixed(2)} zł</p>
- Napis opcji składa się z kilku kawałków:
{r.nazwa} ({r.cena} zł)daje „mała (25 zł)”. Nawiasy i „zł” to zwykły tekst. - Stała
cenaw następnych lekcjach urośnie o dodatki i liczbę sztuk.
Dodatki: tablica wybranych
W galerii były trzy przełączniki i trzy stany. Tu dodatków może być dowolnie wiele, więc stan to jedna tablica z id zaznaczonych. Pole sprawdza, czy jego id jest w tablicy, a kliknięcie je dopisuje albo usuwa.
const [wybrane, setWybrane] = useState([]);
function przelacz(id) {
if (wybrane.includes(id)) {
setWybrane(wybrane.filter((x) => x !== id));
} else {
setWybrane([...wybrane, id]);
}
}
{dodatki.map((d) => (
<div key={d.id} className="form-check">
<input className="form-check-input" type="checkbox" id={"dodatek-" + d.id}
checked={wybrane.includes(d.id)} onChange={() => przelacz(d.id)} />
<label className="form-check-label" htmlFor={"dodatek-" + d.id}>{d.nazwa} (+{d.cena} zł)</label>
</div>
))}
includeszwracatruealbofalse, czyli dokładnie to, czego chcechecked.onChangenie czyta tuevent.target.checked:przelaczsama wie, czy dodać, czy usunąć.- Identyfikatory z pętli składasz z napisu i
id, żeby każde pole miało inny, a etykieta trafiała do swojego.
Cena z dodatkami
Tablica wybrane ma same id. Ceny są w tablicy dodatków, więc najpierw wybierasz z niej obiekty zaznaczonych, a potem sumujesz ich ceny.
const wybraneDodatki = dodatki.filter((d) => wybrane.includes(d.id)); const cenaDodatkow = wybraneDodatki.reduce((suma, d) => suma + d.cena, 0); const cena = wybranyRozmiar.cena + cenaDodatkow;
filteridzie po tablicy dodatków, więcwybraneDodatkimają kolejność z tablicy, a nie z klikania. Przyda się to w podsumowaniu.- Bez dodatków
reducezwraca wartość początkową, 0. Dlatego zero na końcu jest potrzebne.
Liczba sztuk
Poprawność liczby sztuk to wartość logiczna liczona przy rysowaniu. Korzystają z niej dwa miejsca: akapit z ceną i przycisk.
const [sztuki, setSztuki] = useState("1");
const liczba = Number(sztuki);
const poprawne = Number.isInteger(liczba) && liczba >= 1 && liczba <= 5;
const cena = (wybranyRozmiar.cena + cenaDodatkow) * liczba;
{poprawne ? (
<p className="lead">Cena: {cena.toFixed(2)} zł</p>
) : (
<div className="alert alert-warning">Liczba sztuk musi być od 1 do 5</div>
)}
<button type="submit" className="btn btn-success" disabled={!poprawne}>Zamów</button>
- Stan zaczyna od napisu
"1", bo pole zawsze daje napis. Liczbę robi z niegoNumber. - Nawias wokół sumy w cenie jest konieczny. Bez niego razy liczbę sztuk szłyby tylko dodatki.
- Pusty napis daje 0, więc łapie go warunek
>= 1.
Podsumowanie zamówienia
Podsumowanie ma pokazywać to, co zamówiono, a nie to, co jest teraz w polach. Dlatego ma osobny stan, ustawiany tylko w funkcji zatwierdzającej, jak podsumowanie w menedżerze haseł.
const [zamowienie, setZamowienie] = useState("");
function zamow(event) {
event.preventDefault();
const nazwy = wybraneDodatki.map((d) => d.nazwa);
const tekstDodatkow = nazwy.length > 0 ? nazwy.join(", ") : "brak";
setZamowienie("Zamówienie: " + wybranyRozmiar.nazwa + " pizza, dodatki: " + tekstDodatkow +
", sztuk: " + liczba + ", razem: " + cena.toFixed(2) + " zł");
}
{zamowienie !== "" && (
<div className="card mt-3">
<div className="card-body">{zamowienie}</div>
</div>
)}
- Funkcja korzysta ze stałych policzonych nad
return:wybraneDodatki,liczba,cena. Nie trzeba liczyć ich drugi raz. mapz obiektów robi tablicę samych nazw, ajoinskleja ją przecinkami.
Całe zadanie od pustego pliku
Całe zamówienie od szkieletu, około 30 minut. Tablice są w kodzie startowym. Zrzuty ekranu: stan początkowy, wybrane dodatki z ceną, komunikat o złej liczbie sztuk, karta z zamówieniem.
Lista kontrolna przed „Sprawdź”
- Cztery stany:
rozmiar("mala"),wybrane([]),sztuki("1"),zamowienie(""). - Funkcja
przelaczzincludes,filteri[...]. - Nad
return:findrozmiaru,filterireducedodatków,Number,poprawne,cena. - Lista z
rozmiary.map, pola wyboru zdodatki.mapwform-check, poletype="number". ?:z akapitemleadalboalert-warning, przyciskbtn btn-successzdisabled={!poprawne}.zamow:preventDefault, nazwy przezmapijoinalbo „brak”, karta podzamowienie !== "".
Zrzuty ekranu, dokumentacja i oddanie
Działająca aplikacja to nie wszystko. Klucz oceniania osobno punktuje zrzuty ekranu i pliki na płycie. Te punkty łatwo zdobyć, a łatwo też stracić przez pośpiech w ostatnich minutach.
Zrzuty ekranu aplikacji webowej
Każdy zrzut obejmuje cały ekran z widocznym paskiem zadań: przeglądarkę z aplikacją i otwartą konsolą oraz edytor z projektem albo terminal z npm run dev. Pliki nazywasz web1, web2… i zapisujesz w folderze testy.
Zrób jeden zrzut na każdą interakcję, o której mówi arkusz: stan początkowy, każdy przycisk, wynik poprawny i błędny. Przykład dla zadania „Zapisy na kursy”:
| Plik | Co ma być widać |
|---|---|
web1 | Stan początkowy: nagłówek, lista, puste pola. |
web2 | Wypełnione pola, np. „Jan Kowalski” i 2, jeszcze przed kliknięciem. |
web3 | Po kliknięciu „Zapisz do kursu” z poprawnym numerem: w konsoli imię i nazwisko oraz nazwa kursu. |
web4 | Po kliknięciu z błędnym numerem, np. 7: w konsoli „Nieprawidłowy numer kursu”. |
Cały ekran zapisuje do pliku Win+PrtSc (trafia do Obrazy\Zrzuty ekranu, potem przenieś go i zmień nazwę). Możesz też nacisnąć PrtSc i wkleić zrzut w Paint, a potem zapisać go jako PNG.
Co zostaje w folderze web
- Zatrzymaj serwer (Ctrl+C w terminalu).
- Spakuj cały folder projektu
kursydoweb.zip: prawy przycisk myszy → Kompresuj do pliku ZIP (w starszym Windows: Wyślij do → Folder skompresowany), potem zmień nazwę archiwum naweb.zip. - Skopiuj do
webpliki, które zmieniałeś:App.jsximain.jsx. - Usuń folder projektu. Arkusz każe zostawić tylko archiwum i zmienione pliki źródłowe.
web/ ├── web.zip ├── App.jsx └── main.jsx
web.zip i zobacz, czy jest w nim src/App.jsx z twoim kodem. Pakowanie z node_modules trwa dłużej, ale arkusz mówi o całym folderze projektu, więc tak jest bezpieczniej.Dokument „egzamin”
W edytorze tekstu z pakietu biurowego (Word albo Writer) utwórz plik egzamin w folderze testy. Jest jeden dla obu aplikacji: konsolowej i webowej. Wystarczą trzy informacje, np.:
| System operacyjny | Windows 11 Pro (wpisz ten ze stanowiska: Ustawienia → System → Informacje) |
|---|---|
| Środowiska programistyczne | Visual Studio Code (oraz środowisko z części konsolowej, np. PyCharm) |
| Języki, frameworki, biblioteki | JavaScript, React, Vite, Bootstrap, Node.js (oraz język części konsolowej) |
Ostatnie minuty
- Folder z PESEL ma trzy podfoldery:
konsola,web,testy. - W
testy: zrzutykonsola1…,web1…i dokumentegzamin. - Nagraj płytę, otwórz ją i sprawdź, czy pliki się czytają. Płytę opisz numerem PESEL.
Quiz
Pytania o błędy, które najczęściej kosztują punkty w tym zadaniu. Po wybraniu odpowiedzi zobaczysz krótkie wyjaśnienie.
Ściąga
Polecenia
> npm create vite@latest kursy -- --template react > cd kursy > npm install > npm install bootstrap > npm run dev
src/main.jsx
import 'bootstrap/dist/css/bootstrap.css'
Najważniejsze zapisy
| Co | Zapis |
|---|---|
| liczba elementów tablicy | {kursy.length} |
| lista z tablicy | {kursy.map((kurs) => <li key={kurs}>{kurs}</li>)} |
| stan pola | const [imie, setImie] = useState(""); |
| pole sterowane stanem | value={imie} onChange={(e) => setImie(e.target.value)} |
| zatwierdzenie | <form onSubmit={zapisz}> i event.preventDefault(); |
| numer na nazwę | kursy[Number(numer) - 1] po sprawdzeniu 1 … kursy.length |
| lista rozwijana | <select value={rodzaj} onChange={(e) => setRodzaj(e.target.value)}> |
| pole wyboru, przełącznik | checked={kwiaty} onChange={(e) => setKwiaty(e.target.checked)} |
| pole wyłączane stanem | disabled={generujHaslo} |
| tylko część tablicy | {zdjecia.filter(czyWidoczne).map(…)} |
| zmiana jednego obiektu w tablicy | setZdjecia(zdjecia.map((z) => z.id === id ? { ...z, downloads: z.downloads + 1 } : z)) |
| kliknięcie z argumentem | onClick={() => pobierz(zdjecie.id)} |
| jeden z dwóch napisów | generujHaslo ? "Hasło automatycznie generowane" : "Hasło: " + haslo |
| własny plik CSS | import './App.css'; w App.jsx |
| zdjęcia w Vite | public/assets/obraz1.jpg → src={"assets/" + plik} |
| klasy Bootstrapa | form-group, form-control, form-check form-switch, btn btn-primary, btn btn-success, p-2, lead, text-primary |
Całe rozwiązania
Wzorcowe rozwiązania pokazują się tu, gdy kurs ma plik z rozwiązaniami.