Kurs JavaScript i React · Moduł 3: Stan i zdarzenia
Projekt: Marsjański Dashboard Kolonii
W tej lekcji5
Gratulacje - dotarłeś do finałowego projektu modułu "Stan i zdarzenia"! W poprzednich lekcjach poznałeś useState, obsługę zdarzeń, przepływ danych, podnoszenie stanu (lifting state up) i kontrolowane formularze. Teraz połączysz całą tę wiedzę w jedną spójną aplikację.
Twoim zadaniem jest zbudowanie Marsjańskiego Dashboardu Kolonii - interaktywnego panelu zarządzania bazą na Marsie. Ra, twój kosmiczny przewodnik, zebrał wymagania od centrum dowodzenia. Misja: napisać aplikację React, która monitoruje i kontroluje kluczowe systemy kolonii.
Co budujesz
Dashboard składa się z czterech połączonych sekcji:
- Panel systemów kolonii - monitory atmosfery, zasilania, wody i temperatury
- Centrum sterowania - przyciski do ręcznej regulacji parametrów
- Formularz rejestracji astronautów - kontrolowany formularz do dodawania załogi
- Dziennik zdarzeń - log, do którego każdy panel dopisuje wpisy przez funkcję
addLogEntryz props
Wszystkie sekcje muszą ze sobą współpracować - zmiany w jednym panelu wpływają na inne.
Wymagania techniczne
1. Wiele stanów (useState)
Każdy system kolonii ma własny stan:
1const [atmosphere, setAtmosphere] = useState(78);
2const [power, setPower] = useState(85);
3const [water, setWater] = useState(62);
4const [temperature, setTemperature] = useState(-10);
5const [crew, setCrew] = useState([
6 { id: 1, name: 'Anna Kowalska', role: 'Komandor' },
7 { id: 2, name: 'Jan Wiśniewski', role: 'Inżynier' }
8]);2. Obsługa zdarzeń
Każdy system ma przyciski do regulacji. Pamiętaj o zakresach bezpiecznych:
1const handleAtmosphereBoost = () => {
2 setAtmosphere(prev => Math.min(100, prev + 5));
3 addLogEntry('Zwiększono poziom O2 o 5%');
4};3. Kontrolowany formularz rejestracji
Formularz do dodawania nowych astronautów do załogi:
1const [newCrew, setNewCrew] = useState({ name: '', role: '' });
2
3const handleCrewChange = (event) => {
4 const { name, value } = event.target;
5 setNewCrew(prev => ({ ...prev, [name]: value }));
6};
7
8const handleCrewSubmit = (event) => {
9 event.preventDefault();
10 if (!newCrew.name.trim()) return;
11 setCrew(prev => [...prev, { id: Date.now(), ...newCrew }]);
12 setNewCrew({ name: '', role: '' });
13};4. Alarmy w obsłudze zdarzeń
Stan krytyczny sprawdzaj w handlerze, który zmienia wartość - alarm pojawi się dokładnie w chwili zmiany:
1const changePower = (delta) => {
2 const next = Math.min(100, Math.max(0, power + delta));
3 setPower(next);
4 if (next < 20) {
5 addLogEntry('ALARM: Krytycznie niski poziom zasilania!');
6 }
7};Zmiany w czasie (np. zużycie wody co 5 sekund) wymagają hooka useEffect ze sprzątaniem - poznasz go w module o efektach i wtedy rozbudujesz ten dashboard.
5. Lifting state up - wspólny dziennik zdarzeń
Stan dziennika i funkcja addLogEntry żyją w komponencie głównym i są przekazywane w dół:
1function MarsColonyDashboard() {
2 const [log, setLog] = useState([]);
3
4 const addLogEntry = (message) => {
5 setLog(prev => [
6 { id: Date.now(), time: new Date().toLocaleTimeString(), message },
7 ...prev
8 ].slice(0, 20));
9 };
10
11 return (
12 <div>
13 <SystemsPanel onLog={addLogEntry} />
14 <CrewPanel onLog={addLogEntry} />
15 <EventLog entries={log} />
16 </div>
17 );
18}Struktura komponentów
Tak może wyglądać drzewo komponentów - stan żyje na górze, a panele dostają go przez props:
1MarsColonyDashboard (stan główny)
2├── SystemsPanel (props: wartości + callbacki)
3│ └── SystemGauge × 4
4├── CrewPanel (props: crew, formularz, callbacki)
5│ ├── CrewList
6│ └── CrewForm
7└── EventLog (props: entries)Dane płyną w dół przez props, zdarzenia w górę przez callbacki - serce jednokierunkowego przepływu danych w React!
Kryteria oceny
- useState - minimum 4 stany (atmosfera, zasilanie, woda, temperatura) + stan załogi + stan formularza
- Obsługa zdarzeń - przyciski regulujące parametry z zabezpieczeniami (min/max)
- Kontrolowany formularz - dodawanie nowych astronautów z walidacją
- Alarmy - sprawdzanie stanu krytycznego w handlerach i wpis do dziennika
- Lifting state up - wspólny dziennik zdarzeń dostępny z wielu komponentów
- Komponentyzacja - minimum 3 osobne komponenty komunikujące się przez props
Szkielet startowy
Na start dostajesz szkielet aplikacji zgodny z powyższą strukturą: MarsColonyDashboard trzyma stany i dziennik, a panele dostają dane i funkcje przez props. Przyciski atmosfery i zasilania już działają i dopisują wpisy do dziennika - uzupełnij miejsca oznaczone TODO: stany wody i temperatury, limity 0-100, alarm niskiego zasilania i formularz rejestracji astronautów.
Kod do tej lekcji: App.jsx
1import { useState } from 'react';
2
3// Szkielet projektu "Marsjański Dashboard Kolonii" (lekcja 3_7).
4// Stany i dziennik żyją w MarsColonyDashboard, a panele dostają dane i funkcje przez props.
5// Uzupełnij miejsca oznaczone TODO.
6
7let nextLogId = 1;
8
9function SystemGauge({ label, value, unit, onAdjust }) {
10 return (
11 <div style={styles.gauge}>
12 <h3 style={styles.gaugeLabel}>{label}</h3>
13 <p style={styles.gaugeValue}>{value}{unit}</p>
14 <button style={styles.button} onClick={() => onAdjust(-5)}>-5</button>
15 <button style={styles.button} onClick={() => onAdjust(5)}>+5</button>
16 </div>
17 );
18}
19
20function SystemsPanel({ atmosphere, power, onAtmosphereChange, onPowerChange }) {
21 return (
22 <section style={styles.panel}>
23 <h2 style={styles.title}>Systemy kolonii</h2>
24 <div style={styles.gauges}>
25 <SystemGauge label="Atmosfera (O2)" value={atmosphere} unit="%" onAdjust={onAtmosphereChange} />
26 <SystemGauge label="Zasilanie" value={power} unit="%" onAdjust={onPowerChange} />
27 {/* TODO: wskaźniki wody (%) i temperatury (°C) */}
28 </div>
29 </section>
30 );
31}
32
33function CrewPanel({ crew }) {
34 // TODO: kontrolowany formularz { name, role } z e.preventDefault() i walidacją.
35 // Nowego astronautę dodaje funkcja z props, bo stan załogi żyje w rodzicu.
36 return (
37 <section style={styles.panel}>
38 <h2 style={styles.title}>Załoga ({crew.length})</h2>
39 <ul style={styles.list}>
40 {crew.map((member) => (
41 <li key={member.id}>{member.name} - {member.role}</li>
42 ))}
43 </ul>
44 </section>
45 );
46}
47
48function EventLog({ entries }) {
49 return (
50 <section style={styles.panel}>
51 <h2 style={styles.title}>Dziennik zdarzeń</h2>
52 {entries.length === 0 ? (
53 <p style={styles.muted}>Brak wpisów - użyj przycisków w panelu systemów.</p>
54 ) : (
55 <ul style={styles.list}>
56 {entries.map((entry) => (
57 <li key={entry.id}>[{entry.time}] {entry.message}</li>
58 ))}
59 </ul>
60 )}
61 </section>
62 );
63}
64
65function MarsColonyDashboard() {
66 const [atmosphere, setAtmosphere] = useState(78);
67 const [power, setPower] = useState(85);
68 // TODO: stany water (62) i temperature (-10)
69 const [crew, setCrew] = useState([
70 { id: 1, name: 'Anna Kowalska', role: 'Komandor' },
71 { id: 2, name: 'Jan Wiśniewski', role: 'Inżynier' },
72 ]);
73 const [log, setLog] = useState([]);
74
75 // Wspólny dziennik: panele dopisują wpisy przez tę funkcję przekazaną w props
76 const addLogEntry = (message) => {
77 const entry = { id: nextLogId++, time: new Date().toLocaleTimeString(), message };
78 setLog((prev) => [entry, ...prev].slice(0, 20));
79 };
80
81 const changeAtmosphere = (delta) => {
82 // TODO: pilnuj zakresu 0-100 (Math.min i Math.max)
83 setAtmosphere(atmosphere + delta);
84 addLogEntry('Atmosfera: ' + (delta > 0 ? '+' : '') + delta + '%');
85 };
86
87 const changePower = (delta) => {
88 // TODO: zakres 0-100 i alarm w dzienniku, gdy zasilanie spadnie poniżej 20%
89 setPower(power + delta);
90 addLogEntry('Zasilanie: ' + (delta > 0 ? '+' : '') + delta + '%');
91 };
92
93 // TODO: funkcja addCrewMember(member), która przez setCrew doda astronautę
94 // i dopisze wpis do dziennika; przekaż ją do CrewPanel
95
96 return (
97 <div style={styles.app}>
98 <h1 style={styles.header}>Marsjański Dashboard Kolonii</h1>
99 <SystemsPanel
100 atmosphere={atmosphere}
101 power={power}
102 onAtmosphereChange={changeAtmosphere}
103 onPowerChange={changePower}
104 />
105 <CrewPanel crew={crew} />
106 <EventLog entries={log} />
107 </div>
108 );
109}
110
111const styles = {
112 app: { fontFamily: 'Arial, sans-serif', background: '#0b1020', color: '#e6ecff', minHeight: '100vh', padding: '20px' },
113 header: { color: '#ff8c42', textAlign: 'center', marginTop: 0 },
114 panel: { background: '#151c33', borderRadius: '12px', padding: '16px', marginBottom: '16px' },
115 title: { marginTop: 0, color: '#7fd1ff' },
116 gauges: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '12px' },
117 gauge: { background: '#1e2747', borderRadius: '10px', padding: '12px', textAlign: 'center' },
118 gaugeLabel: { margin: '0 0 6px', fontSize: '15px' },
119 gaugeValue: { margin: '0 0 10px', fontSize: '28px', fontWeight: 'bold' },
120 button: { margin: '0 4px', padding: '6px 12px', border: 'none', borderRadius: '6px', background: '#3a4f9a', color: 'white', cursor: 'pointer' },
121 list: { margin: 0, paddingLeft: '20px', lineHeight: 1.7 },
122 muted: { color: '#8f9bbf', margin: 0 },
123};
124
125export default function App() {
126 return <MarsColonyDashboard />;
127}Powodzenia, pilocie! Ten projekt to twoja pierwsza prawdziwa misja w świecie React - pokaż, że potrafisz zarządzać kolonią na Marsie. Pamiętaj: każdy panel to osobny moduł statku, ale dane i decyzje płyną przez jednego dowódcę - komponent główny.
Widzisz błąd w tej lekcji?