Kurs JavaScript i React · Moduł 14: Zaawansowane stylowanie
Projekt Główny: Kosmiczny Dashboard z Różnymi Podejściami do Stylowania
W tej lekcji5
W prawdziwych projektach rzadko trafisz na kod ostylowany jednym narzędziem. Stary layout napisano w CSS Modules, ktoś dodał styled-components do dynamicznych kart, a nowy zespół szybko składa widoki w Tailwindzie. Umiejętność poruszania się między tymi światami odróżnia juniora od inżyniera, który ogarnia każdy statek floty. Czas na podsumowanie! W tym projekcie zbudujesz Kosmiczny Dashboard Misji, który łączy różne podejścia do stylowania. To jak zbudowanie kompletnego panelu kontroli misji, gdzie każdy sektor używa najlepszego narzędzia do swojego zadania.
Cel projektu
Stwórz dashboard, który prezentuje informacje o misjach kosmicznych. Projekt powinien demonstrować umiejętność stosowania różnych metod stylowania w jednym projekcie. Kluczowe jest świadome dzielenie ról: każde narzędzie robi to, w czym jest najlepsze, i nie wchodzi w drogę pozostałym.
Wymagania
1. Układ strony (CSS Modules)
Główny layout dashboardu - sidebar, nagłówek, obszar główny - powinien być ostylowany za pomocą CSS Modules. Przypomnij sobie, że plik z rozszerzeniem .module.css to zwykły CSS, w którym każda nazwa klasy dostaje przy budowaniu unikalny sufiks, więc .container nie zderzy się z żadną inną klasą w aplikacji. Siatka ma dwie kolumny (sidebar 250px i resztę) oraz dwa wiersze (nagłówek 60px i treść):
1/* Dashboard.module.css */
2.container {
3 display: grid;
4 grid-template-columns: 250px 1fr;
5 grid-template-rows: 60px 1fr;
6 min-height: 100vh;
7 background: #0a0a2e;
8}
9
10.sidebar {
11 grid-row: 1 / -1;
12 background: #0d1137;
13 padding: 20px;
14}
15
16.header {
17 background: #1a1a4e;
18 display: flex;
19 align-items: center;
20 padding: 0 24px;
21}
22
23.main {
24 padding: 24px;
25 overflow-y: auto;
26}Reguła grid-row: 1 / -1 rozciąga sidebar na wszystkie wiersze, od pierwszej do ostatniej linii siatki. W komponencie użyjesz tych klas przez import styles i zapis className={styles.container}, tak jak w lekcji o CSS Modules.
2. Komponenty interaktywne (Styled-components)
Przyciski, badge'e statusów, karty z dynamicznymi stylami - tu styled-components błyszczy, bo styl zależy od propsów. Zwróć uwagę na znak dolara w $status. To tak zwany transient prop: styled-components w wersji 6 nie filtruje już automatycznie nieznanych propsów i przekazałby zwykłe status do elementu span w DOM (w trybie deweloperskim wypisze wtedy ostrzeżenie). Prefiks $ mówi bibliotece, że prop służy tylko do stylowania:
1const StatusBadge = styled.span`
2 padding: 4px 12px;
3 border-radius: 20px;
4 font-size: 12px;
5 font-weight: bold;
6 background: ${({ $status, theme }) => theme.statusColors[$status]};
7 color: ${({ $status }) => $status === 'active' ? '#0a0a2e' : 'white'};
8`;Użyjesz go jako <StatusBadge $status="active">. Kolor tła pochodzi z motywu (theme.statusColors), więc dodanie nowego statusu wymaga tylko nowego klucza w motywie, a nie zmiany komponentu.
3. Karty i listy (Tailwind CSS)
Szybkie komponenty prezentacyjne z klasami utility. Każda klasa Tailwinda robi jedną rzecz: flex włącza flexbox, gap-3 ustawia odstęp, rounded-lg zaokrągla rogi, a hover:bg-slate-700 zmienia tło po najechaniu:
1function CrewMember({ name, role, avatar }) {
2 return (
3 <div className="flex items-center gap-3 p-3 bg-slate-800 rounded-lg
4 hover:bg-slate-700 transition-colors">
5 <img src={avatar} className="w-10 h-10 rounded-full" alt={name} />
6 <div>
7 <p className="text-white font-medium">{name}</p>
8 <p className="text-gray-400 text-sm">{role}</p>
9 </div>
10 </div>
11 );
12}Nie ma tu żadnego pliku CSS ani styled-component, cały wygląd siedzi w className. To świetne do prostych, statycznych kawałków interfejsu, gorzej sprawdza się przy stylach mocno zależnych od stanu.
4. Theming (Styled-components ThemeProvider)
Cały dashboard powinien korzystać ze wspólnego motywu kolorów. Obiekt ma znane Ci już colors i nowe statusColors, z którego korzysta StatusBadge:
1const spaceTheme = {
2 colors: {
3 background: '#0a0a2e',
4 surface: '#1a1a4e',
5 primary: '#7c4dff',
6 text: '#e0e0e0',
7 success: '#00e676',
8 warning: '#ffa726',
9 danger: '#f44336'
10 },
11 statusColors: {
12 active: '#00e676',
13 pending: '#ffa726',
14 completed: '#64b5f6',
15 failed: '#f44336'
16 }
17};Motyw widzą tylko styled-components wewnątrz ThemeProvider. Klasy CSS Modules i Tailwinda go nie znają, dlatego jeśli chcesz mieć jedno źródło kolorów dla wszystkich trzech podejść, rozważ wystawienie tych samych wartości jako CSS Variables.
5. Responsywność
Dashboard powinien być responsywny - na mniejszych ekranach sidebar chowa się, a siatka kart przechodzi w kolumnę. W tym samym pliku CSS Modules dopisujemy media query:
1/* Dashboard.module.css */
2@media (max-width: 768px) {
3 .container {
4 grid-template-columns: 1fr;
5 }
6 .sidebar {
7 display: none;
8 }
9}Poniżej 768px siatka ma jedną kolumnę, a sidebar znika. Pozostałe reguły .container z pierwszego bloku dalej obowiązują, nadpisujemy tylko to, co się zmienia. W prawdziwym projekcie zamiast całkiem chować nawigację, dodaj przycisk, który ją wysuwa.
Struktura komponentów
Tak wygląda drzewo całej aplikacji. Przy każdym komponencie w nawiasie zapisano narzędzie, którym go stylujesz:
1App
2 ThemeProvider (spaceTheme)
3 GlobalStyles
4 DashboardLayout (CSS Modules)
5 Sidebar (CSS Modules)
6 NavItem (CSS Modules)
7 Header (CSS Modules + Styled StatusBadge)
8 MainContent
9 MissionGrid (Tailwind grid)
10 MissionCard (Styled-components, dynamiczne props)
11 StatusBadge (Styled, zmiana koloru wg status)
12 CrewList (Tailwind)
13 CrewMember (Tailwind)ThemeProvider i GlobalStyles stoją na samej górze, żeby motyw dotarł do każdej karty i badge'a, niezależnie od tego, w której sekcji się znajdą.
Wskazówki
- Zacznij od layoutu (CSS Modules) - stwórz grid z sidebar, header i main
- Dodaj komponent ThemeProvider i GlobalStyles
- Zbuduj interaktywne komponenty (karty misji, badge statusów) z styled-components
- Użyj klas Tailwind do szybkich elementów prezentacyjnych (lista załogi, siatka kart)
- Pamiętaj o spójności kolorów - wszystkie kolory powinny pochodzić z motywu
- Dodaj przynajmniej jeden hover/focus efekt i jedną animację
Moja rada: w jednym komponencie trzymaj się jednego narzędzia. Mieszanie className z Tailwinda i styled-components na tym samym elemencie szybko robi się nieczytelne.
Kryteria oceny
- CSS Modules użyte do layoutu (grid, sidebar, header)
- Styled-components użyte do co najmniej 3 komponentów z dynamicznymi propsami
- Tailwind CSS użyte do co najmniej 2 sekcji prezentacyjnych
- ThemeProvider z zdefiniowanym motywem kolorów
- Responsywność - dashboard działa na różnych rozmiarach ekranu
Poniżej znajdziesz przykład gotowego dashboardu, który łączy wszystkie trzy podejścia. Przeanalizuj kod i zainspiruj się do stworzenia własnej wersji! Gotowy projekt wyślesz mentorowi w następnym zadaniu, a w kolejnych lokacjach te same zasady podziału ról przydadzą Ci się przy każdej większej aplikacji.
Pamiętaj: dobry pulpit misji to nie jedno narzędzie do wszystkiego, tylko każdy sektor wyposażony w to, co działa w nim najlepiej.
Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
3import layoutStyles from './Dashboard.module.css';
4
5// === THEME (styled-components) ===
6const theme = {
7 colors: {
8 primary: '#7c4dff',
9 secondary: '#64b5f6',
10 bg: '#0a0a2e',
11 surface: '#1a1a4e',
12 text: '#e0e0e0',
13 muted: '#9e9e9e',
14 border: '#3949ab',
15 success: '#00e676',
16 warning: '#ffa726',
17 danger: '#f44336',
18 },
19};
20
21const GlobalStyles = createGlobalStyle`
22 * { margin: 0; padding: 0; box-sizing: border-box; }
23 body { font-family: 'Segoe UI', sans-serif; }
24`;
25
26// === STYLED-COMPONENTS (dynamiczne komponenty) ===
27const StatusDot = styled.span`
28 display: inline-block;
29 width: 10px; height: 10px;
30 border-radius: 50%;
31 margin-right: 8px;
32 background: ${({ status, theme }) =>
33 status === 'active' ? theme.colors.success :
34 status === 'warning' ? theme.colors.warning :
35 theme.colors.danger};
36`;
37
38const StyledButton = styled.button`
39 padding: 8px 16px;
40 border: none;
41 border-radius: 6px;
42 cursor: pointer;
43 font-weight: bold;
44 font-size: 13px;
45 transition: all 0.2s;
46 background: ${({ variant, theme }) =>
47 variant === 'primary' ? theme.colors.primary :
48 variant === 'danger' ? theme.colors.danger : 'transparent'};
49 color: ${({ variant }) => variant === 'ghost' ? '#64b5f6' : 'white'};
50 border: ${({ variant }) => variant === 'ghost' ? '1px solid #3949ab' : 'none'};
51 &:hover { opacity: 0.85; }
52`;
53
54const ProgressBar = styled.div`
55 height: 6px;
56 border-radius: 3px;
57 background: #0d1137;
58 margin-top: 8px;
59 overflow: hidden;
60 &::after {
61 content: '';
62 display: block;
63 height: 100%;
64 width: ${({ value }) => value}%;
65 background: ${({ value, theme }) =>
66 value > 70 ? theme.colors.success :
67 value > 40 ? theme.colors.warning :
68 theme.colors.danger};
69 transition: width 0.5s ease;
70 }
71`;
72
73// === DANE ===
74const missions = [
75 { id: 1, name: 'Apollo X', target: 'Mars', status: 'active', progress: 72, crew: ['Nova', 'Rex', 'Luna'] },
76 { id: 2, name: 'Nebula Run', target: 'Orion', status: 'warning', progress: 45, crew: ['Astro', 'Vega'] },
77 { id: 3, name: 'Deep Void', target: 'Andromeda', status: 'critical', progress: 18, crew: ['Echo', 'Titan', 'Zara', 'Sol'] },
78];
79
80const navItems = ['Dashboard', 'Missions', 'Crew', 'Systems', 'Reports'];
81
82// === GLOWNY KOMPONENT ===
83export default function App() {
84 const [activeNav, setActiveNav] = useState('Dashboard');
85 const [selectedMission, setSelectedMission] = useState(null);
86
87 return (
88 <ThemeProvider theme={theme}>
89 <GlobalStyles />
90 <div className={layoutStyles.container}>
91 {/* Sidebar (CSS Modules) */}
92 <aside className={layoutStyles.sidebar}>
93 <h2 className={layoutStyles.logo}>Mission Control</h2>
94 <nav>
95 {navItems.map(item => (
96 <div
97 key={item}
98 className={`${layoutStyles.navItem} ${activeNav === item ? layoutStyles.navActive : ''}`}
99 onClick={() => setActiveNav(item)}
100 >
101 {item}
102 </div>
103 ))}
104 </nav>
105 </aside>
106
107 {/* Header (CSS Modules) */}
108 <header className={layoutStyles.header}>
109 <h1 className={layoutStyles.headerTitle}>Kosmiczny Dashboard</h1>
110 <span className={layoutStyles.headerBadge}>3 active missions</span>
111 </header>
112
113 {/* Main content (mix) */}
114 <main className={layoutStyles.main}>
115 <div className={layoutStyles.grid}>
116 {missions.map(mission => (
117 <div
118 key={mission.id}
119 className={layoutStyles.card}
120 onClick={() => setSelectedMission(
121 selectedMission === mission.id ? null : mission.id
122 )}
123 >
124 <div className={layoutStyles.cardHeader}>
125 <h3 className={layoutStyles.cardTitle}>
126 <StatusDot status={mission.status} />
127 {mission.name}
128 </h3>
129 <span className={layoutStyles.cardTarget}>{mission.target}</span>
130 </div>
131 <p className={layoutStyles.cardMuted}>
132 Zaloga: {mission.crew.length} czlonkow
133 </p>
134 <ProgressBar value={mission.progress} />
135 <p className={layoutStyles.progressLabel}>{mission.progress}%</p>
136
137 {selectedMission === mission.id && (
138 <div className={layoutStyles.cardDetails}>
139 <p className={layoutStyles.detailTitle}>Czlonkowie zalogi:</p>
140 {mission.crew.map(c => (
141 <span key={c} className={layoutStyles.crewBadge}>{c}</span>
142 ))}
143 <div className={layoutStyles.buttonRow}>
144 <StyledButton variant="primary">Szczegoly</StyledButton>
145 <StyledButton variant="ghost">Raport</StyledButton>
146 {mission.status === 'critical' && (
147 <StyledButton variant="danger">Abort</StyledButton>
148 )}
149 </div>
150 </div>
151 )}
152 </div>
153 ))}
154 </div>
155 </main>
156 </div>
157 </ThemeProvider>
158 );
159}Widzisz błąd w tej lekcji?