Kurs JavaScript i React · Moduł 3: Stan i zdarzenia

Projekt: Marsjański Dashboard Kolonii

4 min czytania
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:

  1. Panel systemów kolonii - monitory atmosfery, zasilania, wody i temperatury
  2. Centrum sterowania - przyciski do ręcznej regulacji parametrów
  3. Formularz rejestracji astronautów - kontrolowany formularz do dodawania załogi
  4. Dziennik zdarzeń - log, do którego każdy panel dopisuje wpisy przez funkcję addLogEntry z 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

  1. useState - minimum 4 stany (atmosfera, zasilanie, woda, temperatura) + stan załogi + stan formularza
  2. Obsługa zdarzeń - przyciski regulujące parametry z zabezpieczeniami (min/max)
  3. Kontrolowany formularz - dodawanie nowych astronautów z walidacją
  4. Alarmy - sprawdzanie stanu krytycznego w handlerach i wpis do dziennika
  5. Lifting state up - wspólny dziennik zdarzeń dostępny z wielu komponentów
  6. 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?

Przydatne artykuły