Czas polaczyc wszystko, czego sie nauczyłeś! W tym projekcie zbudujesz kompletna aplikację "Kosmiczny Katalog Gwiazd" (Star Catalog), która pobiera dane z API, obsługuje stany ładowania i błędów, umożliwia wyszukiwanie, i wspiera paginacje.
Twoją aplikacja powinna zawierac:
fetch() z async/await1// Struktura komponentów:
2// App
3// ├── SearchBar (input wyszukiwania)
4// ├── useFetch (custom hook)
5// ├── LoadingSpinner / ErrorMessage / EmptyState
6// ├── StarList (lista wynikow)
7// │ └── StarCard (pojedynczy wynik)
8// └── Pagination (nawigacja stron)1import React, { useState, useEffect } from 'react';
2
3// Custom Hook - useFetch
4function useFetch(url) {
5 const [data, setData] = useState(null);
6 const [loading, setLoading] = useState(true);
7 const [error, setError] = useState(null);
8
9 useEffect(() => {
10 if (!url) return;
11 const controller = new AbortController();
12
13 async function fetchData() {
14 try {
15 setLoading(true);
16 setError(null);
17 const response = await fetch(url, {
18 signal: controller.signal
19 });
20 if (!response.ok) {
21 throw new Error(\`HTTP \${response.status}\`);
22 }
23 const result = await response.json();
24 setData(result);
25 } catch (err) {
26 if (err.name !== 'AbortError') {
27 setError(err.message);
28 }
29 } finally {
30 setLoading(false);
31 }
32 }
33
34 fetchData();
35 return () => controller.abort();
36 }, [url]);
37
38 const refetch = () => {
39 setData(null);
40 setLoading(true);
41 setError(null);
42 // Trigger re-fetch via state change
43 };
44
45 return { data, loading, error, refetch };
46}
47
48// Komponenty pomocnicze
49function LoadingSpinner() {
50 return (
51 <div style={{ textAlign: 'center', padding: '40px' }}>
52 <div className="spinner"></div>
53 <p>Skanowanie galaktyki...</p>
54 </div>
55 );
56}
57
58function ErrorMessage({ message, onRetry }) {
59 return (
60 <div className="error-panel">
61 <h3>Utracono lacznosc!</h3>
62 <p>{message}</p>
63 <button onClick={onRetry}>Ponow skanowanie</button>
64 </div>
65 );
66}
67
68function EmptyState({ query }) {
69 return (
70 <div className="empty-panel">
71 <p>Brak wynikow {query ? \`dla "\${query}"\` : ''}</p>
72 <p>Sprobuj innego zapytania</p>
73 </div>
74 );
75}
76
77function StarCard({ star }) {
78 return (
79 <div className="star-card">
80 <h3>{star.name}</h3>
81 <p>Wzrost: {star.height} cm</p>
82 <p>Waga: {star.mass} kg</p>
83 <p>Kolor wlosow: {star.hair_color}</p>
84 <p>Rok urodzenia: {star.birth_year}</p>
85 </div>
86 );
87}
88
89function Pagination({ page, totalPages, onPageChange }) {
90 return (
91 <div className="pagination">
92 <button
93 onClick={() => onPageChange(page - 1)}
94 disabled={page === 1}
95 >
96 Poprzednia
97 </button>
98 <span>Strona {page} z {totalPages}</span>
99 <button
100 onClick={() => onPageChange(page + 1)}
101 disabled={page >= totalPages}
102 >
103 Nastepna
104 </button>
105 </div>
106 );
107}
108
109// Glowna aplikacja
110function App() {
111 const [searchQuery, setSearchQuery] = useState('');
112 const [debouncedQuery, setDebouncedQuery] = useState('');
113 const [page, setPage] = useState(1);
114
115 // Debounce wyszukiwania
116 useEffect(() => {
117 const timer = setTimeout(() => {
118 setDebouncedQuery(searchQuery);
119 setPage(1); // Reset strony przy nowym wyszukiwaniu
120 }, 500);
121 return () => clearTimeout(timer);
122 }, [searchQuery]);
123
124 // Buduj URL z parametrami
125 const url = debouncedQuery
126 ? \`https://swapi.dev/api/people/?search=\${debouncedQuery}&page=\${page}\`
127 : \`https://swapi.dev/api/people/?page=\${page}\`;
128
129 const { data, loading, error } = useFetch(url);
130 const totalPages = data ? Math.ceil(data.count / 10) : 1;
131
132 return (
133 <div className="app">
134 <h1>Kosmiczny Katalog Gwiazd</h1>
135
136 <input
137 type="text"
138 value={searchQuery}
139 onChange={e => setSearchQuery(e.target.value)}
140 placeholder="Szukaj w galaktyce..."
141 className="search-input"
142 />
143
144 {loading && <LoadingSpinner />}
145
146 {error && (
147 <ErrorMessage
148 message={error}
149 onRetry={() => setPage(p => p)}
150 />
151 )}
152
153 {!loading && !error && data && (
154 <>
155 {data.results.length === 0 ? (
156 <EmptyState query={debouncedQuery} />
157 ) : (
158 <>
159 <p>Znaleziono: {data.count} wynikow</p>
160 <div className="star-grid">
161 {data.results.map(star => (
162 <StarCard key={star.name} star={star} />
163 ))}
164 </div>
165 <Pagination
166 page={page}
167 totalPages={totalPages}
168 onPageChange={setPage}
169 />
170 </>
171 )}
172 </>
173 )}
174 </div>
175 );
176}
177
178export default App;Ten projekt laczy wszystkie techniki z tego modułu: fetch API, loading/error/empty states, useEffect, custom hook useFetch, debouncing, i paginacje. Sprobuj zbudowac go samodzielnie, a następnie porownaj z powyższym rozwiązaniem!