Kurs JavaScript i React · Moduł 7: Stylowanie i animacje
Projekt: Kosmiczny Panel Kontroli Misji
W tej lekcji4
Gratulacje, Nawigatorze! Dotarłeś do końca tego modułu i nadszedł czas, aby połączyć wszystkie poznane techniki stylowania w jeden, kompleksowy projekt. Twoim zadaniem jest stworzenie Kosmicznego Panelu Kontroli Misji - interaktywnego dashboardu, który wykorzysta wiedzę z całego modułu.
Cel projektu
Zbudujesz panel kontrolny statku kosmicznego, który łączy:
- Styled Components z dynamicznym themingiem
- Animacje CSS z
@keyframes - Responsywny layout z CSS Grid i Flexbox
- Theming (tryb jasny/ciemny)
- Interaktywne elementy z efektami hover i transition
Architektura projektu
Projekt składa się z kilku kluczowych sekcji:
1. System motywów (Theming)
Wykorzystujemy ThemeProvider ze styled-components, aby zarządzać kolorami i stylami w całej aplikacji:
1const darkTheme = {
2 bg: '#0a0a1a',
3 surface: '#12122a',
4 accent: '#00ff88',
5 text: '#e0e0e0',
6 danger: '#ff4444',
7 warning: '#ffaa00'
8};
9
10const lightTheme = {
11 bg: '#f0f4f8',
12 surface: '#ffffff',
13 accent: '#0066ff',
14 text: '#1a1a2e',
15 danger: '#cc0000',
16 warning: '#cc8800'
17};2. Animacje CSS
Panel wykorzystuje kilka typów animacji, które poznałeś w module:
1/* Pulsowanie statusu */
2@keyframes pulse {
3 0%, 100% { opacity: 1; }
4 50% { opacity: 0.5; }
5}
6
7/* Wjazd z przesunięciem */
8@keyframes slideIn {
9 from {
10 opacity: 0;
11 transform: translateY(20px);
12 }
13 to {
14 opacity: 1;
15 transform: translateY(0);
16 }
17}
18
19/* Obracanie radaru */
20@keyframes rotate {
21 from { transform: rotate(0deg); }
22 to { transform: rotate(360deg); }
23}3. Styled Components z propsami
Dynamiczne stylowanie komponentów na podstawie przekazanych propsów:
1const StatusBadge = styled.span`
2 padding: 4px 12px;
3 border-radius: 12px;
4 font-size: 12px;
5 font-weight: bold;
6 background: ${props => props.active ? '#00ff8833' : '#ff444433'};
7 color: ${props => props.active ? '#00ff88' : '#ff4444'};
8 animation: pulse 2s infinite;
9`;
10
11const Card = styled.div`
12 background: ${props => props.theme.surface};
13 border: 1px solid ${props => props.theme.accent}33;
14 border-radius: 12px;
15 padding: 20px;
16 transition: transform 0.3s ease, box-shadow 0.3s ease;
17
18 &:hover {
19 transform: translateY(-4px);
20 box-shadow: 0 8px 25px rgba(0, 255, 136, 0.15);
21 }
22`;4. Responsywny layout z CSS Grid
Panel automatycznie dostosowuje się do szerokości ekranu:
1.dashboard-grid {
2 display: grid;
3 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
4 gap: 20px;
5 padding: 20px;
6}
7
8@media (max-width: 768px) {
9 .dashboard-grid {
10 grid-template-columns: 1fr;
11 }
12}5. Kompletna struktura komponentu
Główny komponent App łączy wszystkie elementy:
1function App() {
2 const [isDark, setIsDark] = useState(true);
3
4 return (
5 <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
6 <GlobalStyle />
7 <Header>
8 <Title>Mission Control</Title>
9 <ThemeToggle onClick={() => setIsDark(!isDark)}>
10 {isDark ? 'Jasny' : 'Ciemny'}
11 </ThemeToggle>
12 </Header>
13 <DashboardGrid>
14 <StatusCard systems={systemsData} />
15 <RadarCard />
16 <MissionsCard missions={missionsData} />
17 <CrewCard crew={crewData} />
18 </DashboardGrid>
19 </ThemeProvider>
20 );
21}Kluczowe techniki zastosowane w projekcie
| Technika | Zastosowanie w projekcie |
|---|---|
| Styled Components | Wszystkie komponenty UI |
| ThemeProvider | Przełączanie dark/light mode |
@keyframes | Pulsowanie statusów, obrót radaru, wjazd kart |
| CSS Grid | Layout dashboardu |
| Flexbox | Układ wewnętrzny kart |
transition | Efekty hover na kartach i przyciskach |
| Props dynamiczne | Kolory statusów, warunkowe style |
| Responsywność | auto-fit i minmax w gridzie |
Podsumowanie modułu
W tym module poznałeś pełne spektrum technik stylowania w React:
- Klasyczny CSS i CSS Modules - fundament stylowania z izolacją klas
- Styled Components - CSS-in-JS z dynamicznymi propsami i themingiem
- SCSS - preprocesor z zmiennymi, mixinami i zagnieżdżaniem
- Tailwind CSS - podejście utility-first dla szybkiego prototypowania
- Frameworki UI - gotowe biblioteki komponentów (MUI, Ant Design, Chakra)
- Design Systems - budowanie spójnych systemów projektowych
- React Transition Group - animowanie cyklu życia komponentów
- Animacje CSS -
@keyframes,transitioni zaawansowane efekty
Każda z tych technik ma swoje miejsce w arsenale kosmicznego programisty. Najlepsi inżynierowie potrafią łączyć je w spójny system - dokładnie tak, jak w tym projekcie.
Wypróbuj panel poniżej i eksperymentuj z kodem - zmień kolory motywu, dodaj nowe animacje lub rozbuduj sekcje dashboardu!
Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2import styled, { keyframes, css, ThemeProvider, createGlobalStyle } from 'styled-components';
3
4// ===== THEMES =====
5const darkTheme = {
6 bg: '#0a0a1a',
7 surface: '#12122a',
8 surfaceHover: '#1a1a3a',
9 accent: '#00ff88',
10 accentDim: 'rgba(0,255,136,0.15)',
11 text: '#e0e0e0',
12 textSecondary: '#8888aa',
13 danger: '#ff4444',
14 warning: '#ffaa00',
15 border: 'rgba(0,255,136,0.2)',
16};
17
18const lightTheme = {
19 bg: '#f0f4f8',
20 surface: '#ffffff',
21 surfaceHover: '#e8ecf0',
22 accent: '#0066ff',
23 accentDim: 'rgba(0,102,255,0.1)',
24 text: '#1a1a2e',
25 textSecondary: '#666688',
26 danger: '#cc0000',
27 warning: '#cc8800',
28 border: 'rgba(0,102,255,0.2)',
29};
30
31// ===== GLOBAL STYLES =====
32const GlobalStyle = createGlobalStyle`
33 * { margin: 0; padding: 0; box-sizing: border-box; }
34 body {
35 font-family: 'Segoe UI', Tahoma, sans-serif;
36 background: ${props => props.theme.bg};
37 color: ${props => props.theme.text};
38 transition: background 0.3s ease, color 0.3s ease;
39 }
40`;
41
42// ===== ANIMATIONS =====
43const pulse = keyframes`
44 0%, 100% { opacity: 1; }
45 50% { opacity: 0.5; }
46`;
47
48const slideIn = keyframes`
49 from { opacity: 0; transform: translateY(20px); }
50 to { opacity: 1; transform: translateY(0); }
51`;
52
53const rotate = keyframes`
54 from { transform: rotate(0deg); }
55 to { transform: rotate(360deg); }
56`;
57
58const glow = keyframes`
59 0%, 100% { box-shadow: 0 0 5px ${() => 'rgba(0,255,136,0.3)'}; }
60 50% { box-shadow: 0 0 20px ${() => 'rgba(0,255,136,0.6)'}; }
61`;
62
63// ===== LAYOUT COMPONENTS =====
64const Header = styled.header`
65 display: flex;
66 justify-content: space-between;
67 align-items: center;
68 padding: 16px 24px;
69 background: ${p => p.theme.surface};
70 border-bottom: 1px solid ${p => p.theme.border};
71`;
72
73const Title = styled.h1`
74 font-size: 20px;
75 color: ${p => p.theme.accent};
76 letter-spacing: 2px;
77 text-transform: uppercase;
78`;
79
80const ThemeToggle = styled.button`
81 background: ${p => p.theme.accentDim};
82 border: 1px solid ${p => p.theme.accent};
83 color: ${p => p.theme.accent};
84 padding: 8px 16px;
85 border-radius: 8px;
86 cursor: pointer;
87 font-size: 14px;
88 transition: all 0.3s ease;
89 &:hover {
90 background: ${p => p.theme.accent};
91 color: ${p => p.theme.bg};
92 }
93`;
94
95const DashboardGrid = styled.div`
96 display: grid;
97 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
98 gap: 20px;
99 padding: 24px;
100`;
101
102const Card = styled.div`
103 background: ${p => p.theme.surface};
104 border: 1px solid ${p => p.theme.border};
105 border-radius: 12px;
106 padding: 20px;
107 animation: ${slideIn} 0.5s ease ${p => p.delay || '0s'} both;
108 transition: transform 0.3s ease, box-shadow 0.3s ease;
109 &:hover {
110 transform: translateY(-4px);
111 box-shadow: 0 8px 25px ${p => p.theme.accentDim};
112 }
113`;
114
115const CardTitle = styled.h3`
116 font-size: 14px;
117 text-transform: uppercase;
118 letter-spacing: 1px;
119 color: ${p => p.theme.accent};
120 margin-bottom: 16px;
121 display: flex;
122 align-items: center;
123 gap: 8px;
124`;
125
126// ===== STATUS COMPONENTS =====
127const StatusBadge = styled.span`
128 display: inline-block;
129 padding: 3px 10px;
130 border-radius: 12px;
131 font-size: 11px;
132 font-weight: bold;
133 background: ${p => p.active ? 'rgba(0,255,136,0.15)' : 'rgba(255,68,68,0.15)'};
134 color: ${p => p.active ? '#00ff88' : '#ff4444'};
135 animation: ${pulse} 2s infinite;
136`;
137
138const SystemRow = styled.div`
139 display: flex;
140 justify-content: space-between;
141 align-items: center;
142 padding: 8px 0;
143 border-bottom: 1px solid ${p => p.theme.border};
144 &:last-child { border-bottom: none; }
145`;
146
147// ===== RADAR COMPONENTS =====
148const RadarWrapper = styled.div`
149 width: 120px;
150 height: 120px;
151 border-radius: 50%;
152 border: 2px solid ${p => p.theme.accent};
153 position: relative;
154 margin: 0 auto 16px;
155 overflow: hidden;
156 background: radial-gradient(circle, ${p => p.theme.accentDim} 0%, transparent 70%);
157`;
158
159const RadarSweep = styled.div`
160 position: absolute;
161 top: 0; left: 50%;
162 width: 2px; height: 50%;
163 background: ${p => p.theme.accent};
164 transform-origin: bottom center;
165 animation: ${rotate} 3s linear infinite;
166`;
167
168const RadarDot = styled.div`
169 position: absolute;
170 width: 6px; height: 6px;
171 background: ${p => p.theme.accent};
172 border-radius: 50%;
173 top: ${p => p.y}%;
174 left: ${p => p.x}%;
175 animation: ${pulse} 1.5s infinite;
176`;
177
178// ===== PROGRESS BAR =====
179const ProgressOuter = styled.div`
180 width: 100%;
181 height: 6px;
182 background: ${p => p.theme.border};
183 border-radius: 3px;
184 margin-top: 6px;
185`;
186
187const ProgressInner = styled.div`
188 height: 100%;
189 width: ${p => p.value || 0}%;
190 background: ${p =>
191 p.value > 75 ? '#00ff88' :
192 p.value > 40 ? '#ffaa00' : '#ff4444'};
193 border-radius: 3px;
194 transition: width 0.5s ease;
195`;
196
197// ===== CREW AVATAR =====
198const AvatarCircle = styled.div`
199 width: 36px;
200 height: 36px;
201 border-radius: 50%;
202 background: ${p => p.color || p.theme.accent};
203 display: flex;
204 align-items: center;
205 justify-content: center;
206 font-size: 14px;
207 font-weight: bold;
208 color: #fff;
209`;
210
211const CrewRow = styled.div`
212 display: flex;
213 align-items: center;
214 gap: 12px;
215 padding: 8px 0;
216`;
217
218const CrewInfo = styled.div`
219 flex: 1;
220 & span { display: block; font-size: 12px; color: ${p => p.theme.textSecondary}; }
221`;
222
223// ===== DATA =====
224const systems = [
225 { name: 'Silniki warp', active: true },
226 { name: 'Osłony energetyczne', active: true },
227 { name: 'Systemy nawigacji', active: true },
228 { name: 'Komunikacja dalekiego zasięgu', active: false },
229 { name: 'Podtrzymywanie życia', active: true },
230];
231
232const missions = [
233 { name: 'Eksploracja Nebuli X-7', progress: 87 },
234 { name: 'Transport zaopatrzenia', progress: 45 },
235 { name: 'Mapowanie asteroid', progress: 62 },
236 { name: 'Naprawa stacji relay', progress: 15 },
237];
238
239const crew = [
240 { name: 'Kpt. Nova', role: 'Dowódca', color: '#e74c3c', initial: 'N' },
241 { name: 'Lt. Orion', role: 'Nawigator', color: '#3498db', initial: 'O' },
242 { name: 'Dr. Stella', role: 'Medyk', color: '#9b59b6', initial: 'S' },
243 { name: 'Ing. Vega', role: 'Inżynier', color: '#2ecc71', initial: 'V' },
244];
245
246// ===== APP =====
247function App() {
248 const [isDark, setIsDark] = useState(true);
249 const theme = isDark ? darkTheme : lightTheme;
250
251 return (
252 <ThemeProvider theme={theme}>
253 <GlobalStyle />
254 <Header>
255 <Title>Mission Control</Title>
256 <ThemeToggle onClick={() => setIsDark(!isDark)}>
257 {isDark ? 'Light Mode' : 'Dark Mode'}
258 </ThemeToggle>
259 </Header>
260
261 <DashboardGrid>
262 {/* Systems Status */}
263 <Card delay="0s">
264 <CardTitle>Systemy statku</CardTitle>
265 {systems.map((s, i) => (
266 <SystemRow key={i}>
267 <span>{s.name}</span>
268 <StatusBadge active={s.active}>
269 {s.active ? 'ONLINE' : 'OFFLINE'}
270 </StatusBadge>
271 </SystemRow>
272 ))}
273 </Card>
274
275 {/* Radar */}
276 <Card delay="0.1s">
277 <CardTitle>Radar</CardTitle>
278 <RadarWrapper>
279 <RadarSweep />
280 <RadarDot x={30} y={25} />
281 <RadarDot x={65} y={40} />
282 <RadarDot x={45} y={70} />
283 </RadarWrapper>
284 <p style={{ textAlign: 'center', fontSize: 13, opacity: 0.7 }}>
285 3 obiekty w zasięgu skanera
286 </p>
287 </Card>
288
289 {/* Missions Progress */}
290 <Card delay="0.2s">
291 <CardTitle>Misje</CardTitle>
292 {missions.map((m, i) => (
293 <div key={i} style={{ marginBottom: 12 }}>
294 <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
295 <span>{m.name}</span>
296 <span style={{ fontWeight: 'bold' }}>{m.progress}%</span>
297 </div>
298 <ProgressOuter>
299 <ProgressInner value={m.progress} />
300 </ProgressOuter>
301 </div>
302 ))}
303 </Card>
304
305 {/* Crew */}
306 <Card delay="0.3s">
307 <CardTitle>Załoga</CardTitle>
308 {crew.map((c, i) => (
309 <CrewRow key={i}>
310 <AvatarCircle color={c.color}>{c.initial}</AvatarCircle>
311 <CrewInfo>
312 <strong>{c.name}</strong>
313 <span>{c.role}</span>
314 </CrewInfo>
315 <StatusBadge active={true}>Na służbie</StatusBadge>
316 </CrewRow>
317 ))}
318 </Card>
319 </DashboardGrid>
320 </ThemeProvider>
321 );
322}
323
324export default App;Widzisz błąd w tej lekcji?
Zadania praktyczne w grze
- Edytor kodu
Zbuduj kompleksowy Design System zawierający: styled-components dla component-level styling, CSS-in-JS patterns, theme provider z dark/light modes, responsive design z breakpoints, CSS Grid i Flexbox layouts, animations z @keyframes, typography system, color tokens, spacing system, oraz accessibility features (focus states, ARIA). Stwórz design system godny najbardziej zaawansowanych interfejsów Deep Space Nine!