W Metropolii Quantum, system transportowy pozwala mieszkańcom przemieszczać się płynnie między różnymi dzielnicami i obiektami. Te zaawansowane pojazdy, nazywane Q-Linkami, potrafią natychmiastowo przenosić pasażerów do właściwie dowolnego miejsca w mieście, bez potrzeby przeładowywania całej infrastruktury transportowej.
W świecie Next.js 15, podobną rolę odgrywają komponenty
Link oraz hook useRouter. Te narzędzia umożliwiają tworzenie płynnej, wydajnej nawigacji między różnymi częściami aplikacji, bez konieczności przeładowywania całej strony.Komponent
Link to podstawowy element nawigacyjny w Next.js 15, działający jak Q-Link w Metropolii Quantum - przenosi użytkownika do określonego punktu docelowego bez zbędnych opóźnień czy zakłóceń.1import Link from 'next/link';
2
3export default function Navigation() {
4 return (
5 <nav className="flex space-x-6 py-4">
6 <Link
7 href="/"
8 className="text-indigo-600 hover:text-indigo-800 transition-colors"
9 >
10 Strona główna
11 </Link>
12 <Link
13 href="/destinations"
14 className="text-indigo-600 hover:text-indigo-800 transition-colors"
15 >
16 Destynacje
17 </Link>
18 <Link
19 href="/flights"
20 className="text-indigo-600 hover:text-indigo-800 transition-colors"
21 >
22 Loty
23 </Link>
24 <Link
25 href="/about"
26 className="text-indigo-600 hover:text-indigo-800 transition-colors"
27 >
28 O nas
29 </Link>
30 </nav>
31 );
32}Ten prosty kod tworzy podstawowy pasek nawigacyjny, który umożliwia użytkownikom poruszanie się między różnymi sekcjami aplikacji. Zauważ, że komponent
Link przyjmuje właściwości takie jak href (cel nawigacji) oraz możemy przekazać mu klasyczne atrybuty HTML, takie jak className do stylowania.Podobnie jak Q-Linki w Metropolii Quantum, które oferują znacznie więcej niż tradycyjne pojazdy, komponent
Link w Next.js 15 przewyższa zwykły tag HTML <a> pod wieloma względami:Link są automatycznie optymalizowane dla aplikacji App Router, uwzględniając funkcje takie jak prefetching dynamicznych tras.Komponent
Link oferuje kilka zaawansowanych opcji, które zwiększają jego funkcjonalność:1// Komponent karty destynacji
2function DestinationCard({ destination }) {
3 return (
4 <Link
5 href={`/destinations/\${destination.id}\`}
6 className="block p-4 border rounded-lg hover:shadow-md transition-shadow"
7 >
8 <h2 className="text-xl font-semibold">{destination.name}</h2>
9 <p className="text-gray-600">{destination.shortDescription}</p>
10 </Link>
11 );
12}1// Komponent filtru wyszukiwania
2function SearchFilter({ currentCategory, currentSort }) {
3 return (
4 <div className="flex space-x-4">
5 <Link
6 href={{
7 pathname: '/search',
8 query: { category: 'planets', sort: currentSort },
9 }}
10 className={`px-3 py-1 rounded \${currentCategory === 'planets' ? 'bg-indigo-600 text-white' : 'bg-gray-200'}\`}
11 >
12 Planety
13 </Link>
14 <Link
15 href={{
16 pathname: '/search',
17 query: { category: 'moons', sort: currentSort },
18 }}
19 className={`px-3 py-1 rounded \${currentCategory === 'moons' ? 'bg-indigo-600 text-white' : 'bg-gray-200'}\`}
20 >
21 Księżyce
22 </Link>
23 <Link
24 href={{
25 pathname: '/search',
26 query: { category: 'stars', sort: currentSort },
27 }}
28 className={`px-3 py-1 rounded \${currentCategory === 'stars' ? 'bg-indigo-600 text-white' : 'bg-gray-200'}\`}
29 >
30 Gwiazdy
31 </Link>
32 </div>
33 );
34}1// Komponent przycisku "Wróć do góry"
2function ScrollToTopButton() {
3 const handleClick = (e) => {
4 e.preventDefault();
5 window.scrollTo({ top: 0, behavior: 'smooth' });
6 };
7
8 return (
9 <Link
10 href="#top"
11 onClick={handleClick}
12 className="fixed bottom-4 right-4 p-2 bg-indigo-600 text-white rounded-full"
13 aria-label="Przewiń do góry"
14 >
15 <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
16 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
17 </svg>
18 </Link>
19 );
20}1<Link
2 href="/destinations"
3 scroll={false} // Wyłącza automatyczne przewijanie do góry strony
4>
5 Destynacje (bez przewijania)
6</Link>1<Link
2 href="/rarely-visited-page"
3 prefetch={false} // Wyłącza automatyczny prefetching
4>
5 Rzadko odwiedzana strona
6</Link>Domyślnie, Link automatycznie ładuje wstępnie (prefetch) strony, które są widoczne w viewporcie. Można to kontrolować za pomocą właściwości
prefetch.W Next.js 15, komponent
Link automatycznie przekazuje odpowiednie atrybuty do tagu <a>, więc nie musisz zagnieżdżać dodatkowego tagu <a> wewnątrz komponentu Link. To usprawnienie pozwala na bardziej zwięzły kod:1// Poprawne użycie w Next.js 15
2<Link
3 href="/destinations"
4 className="text-blue-600 hover:underline"
5 target="_blank"
6 rel="noopener noreferrer"
7>
8 Zobacz destynacje
9</Link>Podczas gdy komponent
Link jest idealny do nawigacji deklaratywnej (widocznej w UI), czasami potrzebujemy nawigować programistycznie - podobnie jak w Metropolii Quantum, gdzie centralna sztuczna inteligencja może kierować Q-Linki do nowych lokalizacji na podstawie różnych warunków czy zdarzeń.W Next.js 15, hook
useRouter z pakietu next/navigation umożliwia właśnie taką dynamiczną nawigację.1'use client';
2
3import { useRouter } from 'next/navigation';
4
5export default function BookingForm() {
6 const router = useRouter();
7
8 const handleSubmit = async (event) => {
9 event.preventDefault();
10
11 // Logika przetwarzania formularza...
12 const bookingSuccess = await submitBooking(formData);
13
14 if (bookingSuccess) {
15 // Po udanym przesłaniu, przekieruj użytkownika
16 router.push('/booking/confirmation');
17 }
18 };
19
20 return (
21 <form onSubmit={handleSubmit} className="space-y-6">
22 {/* Pola formularza... */}
23 <button
24 type="submit"
25 className="w-full py-2 px-4 bg-indigo-600 text-white rounded"
26 >
27 Zarezerwuj podróż
28 </button>
29 </form>
30 );
31}Uwaga: Pamiętaj, że
useRouter to hook Reacta, więc może być używany tylko w komponentach Clienta (oznaczonych jako 'use client';).Hook
useRouter udostępnia kilka przydatnych metod:Nawiguje do podanego URL, dodając nowy wpis do historii przeglądarki:
1const router = useRouter();
2
3function navigateToHome() {
4 router.push('/');
5}
6
7function navigateToDestination(destinationId) {
8 router.push(\`/destinations/\${destinationId}`);
9}
10
11function navigateWithQueryParams() {
12 router.push('/search?category=planets&sort=distance');
13}Podobnie jak
push, ale zastępuje bieżący wpis w historii zamiast dodawać nowy:1function redirectToLogin() {
2 router.replace('/login'); // Użytkownik nie będzie mógł wrócić do poprzedniej strony przyciskiem "Wstecz"
3}Odświeża bieżącą stronę, ponownie pobierając dane, ale zachowując stan DOM:
1function refreshData() {
2 router.refresh(); // Ponownie pobiera dane z serwera bez pełnego przeładowania strony
3}Przechodzi do poprzedniej strony w historii przeglądarki:
1function goBack() {
2 router.back();
3}Przechodzi do następnej strony w historii przeglądarki:
1function goForward() {
2 router.forward();
3}Wstępnie ładuje podany URL:
1function prefetchImportantPage() {
2 router.prefetch('/important-page');
3}1'use client';
2
3import { useState, useEffect } from 'react';
4import { useRouter } from 'next/navigation';
5
6export default function NavigationAssistant() {
7 const router = useRouter();
8 const [destination, setDestination] = useState('');
9 const [suggestions, setSuggestions] = useState([]);
10
11 useEffect(() => {
12 if (destination.length < 2) {
13 setSuggestions([]);
14 return;
15 }
16
17 // Symulacja pobierania sugestii z API
18 const fetchSuggestions = async () => {
19 // W rzeczywistej aplikacji, pobieralibyśmy dane z API
20 const mockSuggestions = [
21 { id: 'mars', name: 'Mars', type: 'planet' },
22 { id: 'mars-colony-alpha', name: 'Kolonia Alpha na Marsie', type: 'colony' },
23 { id: 'martian-canyon', name: 'Wielki Kanion Marsjański', type: 'landmark' }
24 ].filter(item =>
25 item.name.toLowerCase().includes(destination.toLowerCase())
26 );
27
28 setSuggestions(mockSuggestions);
29 };
30
31 fetchSuggestions();
32 }, [destination]);
33
34 const handleSelect = (suggestion) => {
35 // Wybór trasy w zależności od typu sugestii
36 switch(suggestion.type) {
37 case 'planet':
38 router.push(\`/destinations/\${suggestion.id}`);
39 break;
40 case 'colony':
41 router.push(\`/colonies/\${suggestion.id}`);
42 break;
43 case 'landmark':
44 router.push(\`/landmarks/\${suggestion.id}`);
45 break;
46 default:
47 router.push(\`/search?q=\${encodeURIComponent(suggestion.name)}`);
48 }
49 };
50
51 return (
52 <div className="relative w-full max-w-md">
53 <input
54 type="text"
55 value={destination}
56 onChange={(e) => setDestination(e.target.value)}
57 className="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
58 />
59
60 {suggestions.length > 0 && (
61 <div className="absolute z-10 w-full mt-1 bg-white rounded-lg shadow-lg border border-gray-200">
62 <ul>
63 {suggestions.map((suggestion) => (
64 <li
65 key={suggestion.id}
66 className="px-4 py-3 hover:bg-gray-100 cursor-pointer flex items-center"
67 onClick={() => handleSelect(suggestion)}
68 >
69 <div className="w-8 h-8 mr-3 flex items-center justify-center rounded-full bg-indigo-100 text-indigo-800">
70 {suggestion.type === 'planet' ? '🪐' : suggestion.type === 'colony' ? '🏙️' : '🏞️'}
71 </div>
72 <div>
73 <div className="font-medium">{suggestion.name}</div>
74 <div className="text-sm text-gray-500 capitalize">{suggestion.type}</div>
75 </div>
76 </li>
77 ))}
78 </ul>
79 </div>
80 )}
81 </div>
82 );
83}1'use client';
2
3import { useState } from 'react';
4import { useRouter } from 'next/navigation';
5
6export default function MultiStepBookingForm() {
7 const router = useRouter();
8 const [step, setStep] = useState(1);
9 const [formData, setFormData] = useState({
10 destination: '',
11 departureDate: '',
12 returnDate: '',
13 passengers: 1,
14 contactEmail: '',
15 contactPhone: ''
16 });
17
18 const updateFormData = (field, value) => {
19 setFormData(prev => ({ ...prev, [field]: value }));
20 };
21
22 const goToNextStep = () => {
23 setStep(prev => prev + 1);
24 };
25
26 const goToPreviousStep = () => {
27 setStep(prev => prev - 1);
28 };
29
30 const submitForm = async () => {
31 try {
32 // Tutaj normalnie wysłalibyśmy dane do API
33 console.log('Wysyłanie danych:', formData);
34
35 // Symulacja oczekiwania na odpowiedź API
36 await new Promise(resolve => setTimeout(resolve, 1000));
37
38 // Przekierowanie do strony potwierdzenia z ID rezerwacji
39 router.push('/booking/confirmation?id=QV123456789');
40 } catch (error) {
41 console.error('Błąd podczas rezerwacji:', error);
42 // Obsługa błędu
43 }
44 };
45
46 return (
47 <div className="max-w-3xl mx-auto p-6 bg-white rounded-lg shadow-md">
48 <div className="mb-8">
49 <div className="flex items-center">
50 {[1, 2, 3].map((i) => (
51 <div key={i} className="flex items-center">
52 <div className={`w-10 h-10 rounded-full flex items-center justify-center \${
53 i === step ? 'bg-indigo-600 text-white' :
54 i < step ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-700'
55 }\`}>
56 {i < step ? '✓' : i}
57 </div>
58 {i < 3 && (
59 <div className={`h-1 w-16 \${i < step ? 'bg-green-500' : 'bg-gray-200'}\`}></div>
60 )}
61 </div>
62 ))}
63 </div>
64 <div className="flex justify-between mt-2">
65 <span>Destynacja</span>
66 <span>Szczegóły podróży</span>
67 <span>Dane kontaktowe</span>
68 </div>
69 </div>
70
71 {step === 1 && (
72 <div className="space-y-4">
73 <h2 className="text-2xl font-semibold">Wybierz destynację</h2>
74
75 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
76 {['Mars', 'Europa', 'Tytan'].map((dest) => (
77 <div
78 key={dest}
79 onClick={() => updateFormData('destination', dest)}
80 className={`p-4 border rounded-lg cursor-pointer \${
81 formData.destination === dest ? 'border-indigo-600 bg-indigo-50' : 'border-gray-200'
82 }\`}
83 >
84 <h3 className="font-semibold">{dest}</h3>
85 <p className="text-sm text-gray-600">Odkryj cuda {dest}</p>
86 </div>
87 ))}
88 </div>
89
90 <div className="flex justify-end mt-6">
91 <button
92 onClick={goToNextStep}
93 disabled={!formData.destination}
94 className={`px-6 py-2 rounded-lg \${
95 formData.destination ? 'bg-indigo-600 text-white' : 'bg-gray-200 text-gray-500 cursor-not-allowed'
96 }\`}
97 >
98 Dalej
99 </button>
100 </div>
101 </div>
102 )}
103
104 {step === 2 && (
105 <div className="space-y-4">
106 <h2 className="text-2xl font-semibold">Szczegóły podróży</h2>
107
108 <div className="space-y-4">
109 <div>
110 <label className="block mb-1">Data wylotu</label>
111 <input
112 type="date"
113 value={formData.departureDate}
114 onChange={(e) => updateFormData('departureDate', e.target.value)}
115 className="w-full p-2 border border-gray-300 rounded"
116 min={new Date().toISOString().split('T')[0]}
117 />
118 </div>
119
120 <div>
121 <label className="block mb-1">Data powrotu</label>
122 <input
123 type="date"
124 value={formData.returnDate}
125 onChange={(e) => updateFormData('returnDate', e.target.value)}
126 className="w-full p-2 border border-gray-300 rounded"
127 min={formData.departureDate || new Date().toISOString().split('T')[0]}
128 />
129 </div>
130
131 <div>
132 <label className="block mb-1">Liczba pasażerów</label>
133 <input
134 type="number"
135 value={formData.passengers}
136 onChange={(e) => updateFormData('passengers', e.target.value)}
137 className="w-full p-2 border border-gray-300 rounded"
138 min="1"
139 max="10"
140 />
141 </div>
142 </div>
143
144 <div className="flex justify-between mt-6">
145 <button
146 onClick={goToPreviousStep}
147 className="px-6 py-2 border border-gray-300 rounded-lg"
148 >
149 Wstecz
150 </button>
151 <button
152 onClick={goToNextStep}
153 disabled={!formData.departureDate || !formData.returnDate}
154 className={`px-6 py-2 rounded-lg \${
155 formData.departureDate && formData.returnDate ?
156 'bg-indigo-600 text-white' :
157 'bg-gray-200 text-gray-500 cursor-not-allowed'
158 }\`}
159 >
160 Dalej
161 </button>
162 </div>
163 </div>
164 )}
165
166 {step === 3 && (
167 <div className="space-y-4">
168 <h2 className="text-2xl font-semibold">Dane kontaktowe</h2>
169
170 <div className="space-y-4">
171 <div>
172 <label className="block mb-1">Email</label>
173 <input
174 type="email"
175 value={formData.contactEmail}
176 onChange={(e) => updateFormData('contactEmail', e.target.value)}
177 className="w-full p-2 border border-gray-300 rounded"
178 />
179 </div>
180
181 <div>
182 <label className="block mb-1">Telefon</label>
183 <input
184 type="tel"
185 value={formData.contactPhone}
186 onChange={(e) => updateFormData('contactPhone', e.target.value)}
187 className="w-full p-2 border border-gray-300 rounded"
188 />
189 </div>
190 </div>
191
192 <div className="mt-6 p-4 bg-gray-50 rounded-lg">
193 <h3 className="font-semibold mb-2">Podsumowanie</h3>
194 <div className="grid grid-cols-2 gap-2 text-sm">
195 <div>Destynacja:</div>
196 <div className="font-medium">{formData.destination}</div>
197
198 <div>Data wylotu:</div>
199 <div className="font-medium">{formData.departureDate}</div>
200
201 <div>Data powrotu:</div>
202 <div className="font-medium">{formData.returnDate}</div>
203
204 <div>Pasażerowie:</div>
205 <div className="font-medium">{formData.passengers}</div>
206 </div>
207 </div>
208
209 <div className="flex justify-between mt-6">
210 <button
211 onClick={goToPreviousStep}
212 className="px-6 py-2 border border-gray-300 rounded-lg"
213 >
214 Wstecz
215 </button>
216 <button
217 onClick={submitForm}
218 disabled={!formData.contactEmail || !formData.contactPhone}
219 className={`px-6 py-2 rounded-lg \${
220 formData.contactEmail && formData.contactPhone ?
221 'bg-indigo-600 text-white' :
222 'bg-gray-200 text-gray-500 cursor-not-allowed'
223 }\`}
224 >
225 Zarezerwuj podróż
226 </button>
227 </div>
228 </div>
229 )}
230 </div>
231 );
232}W Metropolii Quantum, system monitorowania ruchu nieustannie śledzi pozycje Q-Linków, umożliwiając centralnej AI dostosowywanie parametrów transportowych w czasie rzeczywistym. W Next.js 15, podobną funkcję pełnią hooki
usePathname i useSearchParams.1'use client';
2
3import Link from 'next/link';
4import { usePathname } from 'next/navigation';
5
6export default function MainNavigation() {
7 const pathname = usePathname();
8
9 const navItems = [
10 { label: 'Strona główna', href: '/' },
11 { label: 'Destynacje', href: '/destinations' },
12 { label: 'Loty', href: '/flights' },
13 { label: 'Promocje', href: '/offers' },
14 { label: 'O nas', href: '/about' }
15 ];
16
17 return (
18 <nav className="bg-white shadow">
19 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
20 <div className="flex justify-between h-16">
21 <div className="flex">
22 <div className="flex-shrink-0 flex items-center">
23 <img className="h-8 w-auto" src="/logo.svg" alt="Quantum Voyages" />
24 </div>
25 <div className="hidden sm:ml-6 sm:flex sm:space-x-8">
26 {navItems.map((item) => {
27 const isActive = pathname === item.href ||
28 (item.href !== '/' && pathname.startsWith(item.href));
29
30 return (
31 <Link
32 key={item.href}
33 href={item.href}
34 className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium \${
35 isActive
36 ? 'border-indigo-500 text-gray-900'
37 : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'
38 }\`}
39 >
40 {item.label}
41 </Link>
42 );
43 })}
44 </div>
45 </div>
46 <div className="hidden sm:ml-6 sm:flex sm:items-center">
47 <Link
48 href="/dashboard"
49 className="px-4 py-2 text-sm rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
50 >
51 Moje konto
52 </Link>
53 </div>
54 </div>
55 </div>
56 </nav>
57 );
58}W tym przykładzie,
usePathname pozwala nam określić aktualną ścieżkę, co umożliwia podświetlenie aktywnej pozycji w menu nawigacyjnym.1'use client';
2
3import { useSearchParams } from 'next/navigation';
4
5export default function SearchResultsHeader() {
6 const searchParams = useSearchParams();
7
8 const query = searchParams.get('q') || '';
9 const category = searchParams.get('category') || 'all';
10 const sort = searchParams.get('sort') || 'relevance';
11
12 return (
13 <div className="mb-6">
14 <h1 className="text-3xl font-bold mb-2">
15 {query ? `Wyniki wyszukiwania dla "\${query}"` : 'Wszystkie destynacje'}
16 </h1>
17 <div className="flex items-center text-sm text-gray-600">
18 <span>Kategoria: <strong className="capitalize">{category}</strong></span>
19 <span className="mx-2">•</span>
20 <span>Sortowanie: <strong className="capitalize">{
21 sort === 'relevance' ? 'Trafność' :
22 sort === 'price-asc' ? 'Cena: od najniższej' :
23 sort === 'price-desc' ? 'Cena: od najwyższej' :
24 sort === 'rating' ? 'Ocena' : 'Trafność'
25 }</strong></span>
26 </div>
27 </div>
28 );
29}Ten kod demonstruje, jak
useSearchParams umożliwia odczytanie i wykorzystanie parametrów zapytania z URL, które mogą określać różne aspekty wyświetlanej strony.1'use client';
2
3import { useSearchParams, useRouter, usePathname } from 'next/navigation';
4
5export default function SearchFilters() {
6 const router = useRouter();
7 const pathname = usePathname();
8 const searchParams = useSearchParams();
9
10 // Pobranie aktualnych parametrów wyszukiwania
11 const currentCategory = searchParams.get('category') || 'all';
12 const currentSort = searchParams.get('sort') || 'relevance';
13 const currentQuery = searchParams.get('q') || '';
14
15 // Funkcja do aktualizacji parametrów z zachowaniem istniejących
16 const createQueryString = (name, value) => {
17 const params = new URLSearchParams(searchParams);
18 params.set(name, value);
19
20 return params.toString();
21 };
22
23 const handleCategoryChange = (category) => {
24 router.push(\`\${pathname}?\${createQueryString('category', category)}`);
25 };
26
27 const handleSortChange = (sort) => {
28 router.push(\`\${pathname}?\${createQueryString('sort', sort)}`);
29 };
30
31 return (
32 <div className="mb-8 space-y-4">
33 <div>
34 <h2 className="text-lg font-medium mb-2">Kategorie</h2>
35 <div className="flex flex-wrap gap-2">
36 {['all', 'planets', 'moons', 'stations'].map((category) => (
37 <button
38 key={category}
39 onClick={() => handleCategoryChange(category)}
40 className={`px-4 py-2 rounded-full text-sm \${
41 currentCategory === category
42 ? 'bg-indigo-600 text-white'
43 : 'bg-gray-200 text-gray-800 hover:bg-gray-300'
44 }\`}
45 >
46 {category === 'all' ? 'Wszystkie' :
47 category === 'planets' ? 'Planety' :
48 category === 'moons' ? 'Księżyce' : 'Stacje kosmiczne'}
49 </button>
50 ))}
51 </div>
52 </div>
53
54 <div>
55 <h2 className="text-lg font-medium mb-2">Sortowanie</h2>
56 <select
57 value={currentSort}
58 onChange={(e) => handleSortChange(e.target.value)}
59 className="w-full sm:w-auto p-2 border border-gray-300 rounded-md"
60 >
61 <option value="relevance">Trafność</option>
62 <option value="price-asc">Cena: od najniższej</option>
63 <option value="price-desc">Cena: od najwyższej</option>
64 <option value="rating">Ocena</option>
65 <option value="distance">Odległość</option>
66 </select>
67 </div>
68 </div>
69 );
70}Ten przykład demonstruje, jak efektywnie łączyć
useSearchParams z useRouter i usePathname w celu tworzenia interaktywnych filtrów, które aktualizują URL i zawartość strony bez pełnego przeładowania.W Metropolii Quantum, czasami Q-Link może zmienić swoją trasę w locie, aby obsłużyć specjalne zapytanie pasażera nie przerywając głównej podróży. W Next.js 15, koncepcja ta jest realizowana przez "Intercepting Routes" (przechwytywanie tras).
Przechwytywanie tras pozwala na "przechwycenie" normalnej nawigacji do określonej ścieżki i wyświetlenie alternatywnego UI, często nakładki (modal) na bieżącą stronę, zamiast całkowitej zmiany strony.
Implementacja opiera się na specjalnej konwencji nazewnictwa folderów:
(.) - przechwytuje segmenty na tym samym poziomie(..) - przechwytuje segmenty o jeden poziom wyżej(..)(..) - przechwytuje segmenty dwa poziomy wyżej(...) - przechwytuje segmenty od korzenia aplikacjiStruktura katalogów:
1app/
2 ├── destinations/
3 │ ├── page.tsx # Strona z listą destynacji /destinations
4 │ └── [planet]/
5 │ ├── page.tsx # Strona konkretnej planety /destinations/mars
6 │ ├── gallery/
7 │ │ └── page.tsx # Pełna galeria /destinations/mars/gallery
8 │ └── @modal/
9 │ └── (..)gallery/
10 │ └── page.tsx # Przechwytuje /destinations/mars/gallery jako modalImplementacja:
1// app/destinations/page.tsx - Strona główna destynacji
2import Link from 'next/link';
3
4export default function DestinationsPage() {
5 const destinations = [
6 { id: 'mars', name: 'Mars', imageUrl: '/images/mars.jpg' },
7 { id: 'europa', name: 'Europa', imageUrl: '/images/europa.jpg' },
8 { id: 'titan', name: 'Tytan', imageUrl: '/images/titan.jpg' }
9 ];
10
11 return (
12 <div className="container mx-auto py-12 px-4">
13 <h1 className="text-4xl font-bold mb-8">Galaktyczne destynacje</h1>
14
15 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
16 {destinations.map(destination => (
17 <Link
18 key={destination.id}
19 href={`/destinations/\${destination.id}\`}
20 className="block group"
21 >
22 <div className="relative h-64 rounded-lg overflow-hidden shadow-lg">
23 <img
24 src={destination.imageUrl}
25 alt={destination.name}
26 className="w-full h-full object-cover transition-transform group-hover:scale-105"
27 />
28 <div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent flex items-end">
29 <h2 className="text-white text-2xl font-bold p-4">
30 {destination.name}
31 </h2>
32 </div>
33 </div>
34 </Link>
35 ))}
36 </div>
37 </div>
38 );
39}1// app/destinations/[planet]/page.tsx - Strona konkretnej planety
2import Link from 'next/link';
3
4export default async function PlanetPage({ params }: { params: Promise<{ planet: string }> }) {
5 const { planet } = await params;
6
7 // Symulowane dane galerii
8 const galleryImages = Array.from({ length: 6 }, (_, i) => ({
9 id: i + 1,
10 src: `/images/\${planet}/\${i + 1}.jpg`,
11 alt: `Zdjęcie \${planet} #\${i + 1}`
12 }));
13
14 return (
15 <div className="container mx-auto py-12 px-4">
16 <h1 className="text-4xl font-bold mb-8 capitalize">
17 {planet}
18 </h1>
19
20 <div className="mb-12">
21 {/* Informacje o planecie... */}
22 </div>
23
24 <section>
25 <div className="flex justify-between items-center mb-6">
26 <h2 className="text-2xl font-bold">Galeria</h2>
27 <Link
28 href={`/destinations/\${planet}/gallery\`}
29 className="text-indigo-600 hover:text-indigo-800"
30 >
31 Zobacz wszystkie
32 </Link>
33 </div>
34
35 <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
36 {galleryImages.slice(0, 3).map(image => (
37 <Link
38 key={image.id}
39 href={`/destinations/\${planet}/gallery?image=\${image.id}\`}
40 className="block aspect-square rounded-lg overflow-hidden"
41 >
42 <img
43 src={image.src}
44 alt={image.alt}
45 className="w-full h-full object-cover hover:scale-105 transition-transform"
46 />
47 </Link>
48 ))}
49 </div>
50 </section>
51 </div>
52 );
53}1// app/destinations/[planet]/gallery/page.tsx - Pełna galeria
2import Link from 'next/link';
3import { notFound } from 'next/navigation';
4
5export default async function GalleryPage({ params, searchParams }) {
6 const { planet } = await params;
7 const imageId = (await searchParams).image;
8
9 // Weryfikacja, czy planeta istnieje
10 const validPlanets = ['mars', 'europa', 'titan'];
11 if (!validPlanets.includes(planet)) {
12 notFound();
13 }
14
15 // Symulowane dane galerii
16 const galleryImages = Array.from({ length: 12 }, (_, i) => ({
17 id: i + 1,
18 src: `/images/\${planet}/\${i + 1}.jpg`,
19 alt: `Zdjęcie \${planet} #\${i + 1}`,
20 title: `\${planet.charAt(0).toUpperCase() + planet.slice(1)} - widok #\${i + 1}`
21 }));
22
23 return (
24 <div className="container mx-auto py-12 px-4">
25 <div className="flex items-center mb-8">
26 <Link
27 href={`/destinations/\${planet}\`}
28 className="text-indigo-600 hover:underline mr-4"
29 >
30 ← Wróć do {planet}
31 </Link>
32 <h1 className="text-3xl font-bold capitalize">
33 Galeria: {planet}
34 </h1>
35 </div>
36
37 <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
38 {galleryImages.map(image => (
39 <Link
40 key={image.id}
41 href={`/destinations/\${planet}/gallery?image=\${image.id}\`}
42 className="block aspect-square rounded-lg overflow-hidden"
43 >
44 <img
45 src={image.src}
46 alt={image.alt}
47 className="w-full h-full object-cover hover:scale-105 transition-transform"
48 />
49 </Link>
50 ))}
51 </div>
52 </div>
53 );
54}1// app/destinations/[planet]/@modal/(..)gallery/page.tsx - Modal galerii
2'use client';
3
4import { useRouter, useSearchParams } from 'next/navigation';
5import { useEffect, useRef, useState } from 'react';
6
7export default function GalleryModal({ params }) {
8 const router = useRouter();
9 const searchParams = useSearchParams();
10 const overlayRef = useRef(null);
11 const { planet } = params;
12 const imageId = searchParams.get('image');
13 const [isClosing, setIsClosing] = useState(false);
14
15 // Jeśli nie ma imageId, przekieruj do galerii
16 useEffect(() => {
17 if (!imageId) {
18 router.replace(`/destinations/\${planet}/gallery`);
19 }
20 }, [imageId, planet, router]);
21
22 // Obsługa klawisza ESC
23 useEffect(() => {
24 const handleKeyDown = (e) => {
25 if (e.key === 'Escape') {
26 handleClose();
27 }
28 };
29
30 window.addEventListener('keydown', handleKeyDown);
31 return () => window.removeEventListener('keydown', handleKeyDown);
32 }, []);
33
34 const handleClose = () => {
35 setIsClosing(true);
36 setTimeout(() => {
37 router.back();
38 }, 300);
39 };
40
41 const handleOverlayClick = (e) => {
42 if (e.target === overlayRef.current) {
43 handleClose();
44 }
45 };
46
47 if (!imageId) return null;
48
49 // Pobierz dane obrazu
50 const imageIndex = parseInt(imageId) - 1;
51 const galleryImages = Array.from({ length: 12 }, (_, i) => ({
52 id: i + 1,
53 src: `/images/\${planet}/\${i + 1}.jpg`,
54 alt: `Zdjęcie \${planet} #\${i + 1}`,
55 title: `\${planet.charAt(0).toUpperCase() + planet.slice(1)} - widok #\${i + 1}`,
56 description: `Szczegółowy opis obrazu #\${i + 1} z planety \${planet}.`
57 }));
58
59 const image = galleryImages[imageIndex] || galleryImages[0];
60 const prevImageId = image.id > 1 ? image.id - 1 : null;
61 const nextImageId = image.id < galleryImages.length ? image.id + 1 : null;
62
63 return (
64 <div
65 ref={overlayRef}
66 onClick={handleOverlayClick}
67 className={`fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4 transition-opacity duration-300 \${
68 isClosing ? 'opacity-0' : 'opacity-100'
69 }\`}
70 >
71 <div
72 className={`relative bg-white rounded-lg max-w-6xl w-full max-h-[90vh] overflow-hidden shadow-xl transition-transform duration-300 \${
73 isClosing ? 'scale-95' : 'scale-100'
74 }\`}
75 onClick={(e) => e.stopPropagation()}
76 >
77 <div className="flex h-full">
78 <div className="w-2/3 bg-black flex items-center justify-center">
79 <img
80 src={image.src}
81 alt={image.alt}
82 className="max-h-[85vh] max-w-full object-contain"
83 />
84 </div>
85 <div className="w-1/3 p-6 flex flex-col h-[85vh]">
86 <button
87 onClick={handleClose}
88 className="absolute top-4 right-4 w-8 h-8 rounded-full flex items-center justify-center bg-gray-200 hover:bg-gray-300"
89 aria-label="Zamknij"
90 >
91 ×
92 </button>
93
94 <h2 className="text-2xl font-bold mb-2">{image.title}</h2>
95 <p className="text-gray-600 mb-6">{image.description}</p>
96
97 <div className="mt-auto flex justify-between">
98 {prevImageId ? (
99 <button
100 onClick={() => router.replace(`/destinations/\${planet}/gallery?image=\${prevImageId}`)}
101 className="px-4 py-2 rounded border border-gray-300 hover:bg-gray-100"
102 >
103 ← Poprzednie
104 </button>
105 ) : <div />}
106
107 {nextImageId && (
108 <button
109 onClick={() => router.replace(`/destinations/\${planet}/gallery?image=\${nextImageId}`)}
110 className="px-4 py-2 rounded border border-gray-300 hover:bg-gray-100"
111 >
112 Następne →
113 </button>
114 )}
115 </div>
116 </div>
117 </div>
118 </div>
119 </div>
120 );
121}W tym zaawansowanym przykładzie:
/destinations/mars/gallery?image=1 strona jest przechwytywana przez modalW Metropolii Quantum, zaawansowane Q-Linki potrafią podzielić się na dwie ścieżki, aby jednocześnie obsługiwać różne punkty docelowe, a następnie połączyć się ponownie, optymalizując miejski przepływ transportowy. W Next.js 15, odpowiednikiem tej technologii jest "Parallel Routing" (równoległe trasy).
Równoległe trasy w Next.js 15 umożliwiają renderowanie wielu stron lub komponentów w tym samym widoku, co jest szczególnie przydatne do:
Implementacja wykorzystuje foldery ze znakiem
@ na początku:1app/
2 ├── dashboard/
3 │ ├── layout.tsx # Układ strony
4 │ ├── page.tsx # Główna zawartość
5 │ ├── @stats/ # Slot "stats"
6 │ │ └── page.tsx # Domyślna strona w slocie "stats"
7 │ └── @activity/ # Slot "activity"
8 │ └── page.tsx # Domyślna strona w slocie "activity"Implementacja:
1// app/dashboard/layout.tsx - Układ z równoległymi trasami
2export default function DashboardLayout({
3 children, // Główna zawartość
4 stats, // Slot @stats
5 activity // Slot @activity
6}) {
7 return (
8 <div className="container mx-auto px-4 py-8">
9 <h1 className="text-3xl font-bold mb-8">Panel kontrolny Quantum Voyages</h1>
10
11 <div className="grid grid-cols-12 gap-8">
12 {/* Główna zawartość */}
13 <div className="col-span-12 lg:col-span-6">
14 {children}
15 </div>
16
17 {/* Panel statystyk */}
18 <div className="col-span-12 md:col-span-6 lg:col-span-3">
19 <div className="bg-white shadow rounded-lg p-4 h-full">
20 {stats}
21 </div>
22 </div>
23
24 {/* Panel aktywności */}
25 <div className="col-span-12 md:col-span-6 lg:col-span-3">
26 <div className="bg-white shadow rounded-lg p-4 h-full">
27 {activity}
28 </div>
29 </div>
30 </div>
31 </div>
32 );
33}1// app/dashboard/page.tsx - Główna zawartość
2export default function DashboardPage() {
3 return (
4 <div className="bg-white shadow rounded-lg p-6">
5 <h2 className="text-2xl font-semibold mb-4">Przegląd rezerwacji</h2>
6
7 <div className="space-y-6">
8 <div className="p-4 border rounded-lg">
9 <h3 className="font-semibold">Nadchodząca podróż</h3>
10 <p className="mt-2">Mars - Kolonia Alpha, 12 czerwca 2150</p>
11 <div className="mt-4 flex justify-between">
12 <span className="text-indigo-600">Status: Potwierdzona</span>
13 <button className="text-sm text-indigo-600 hover:underline">
14 Szczegóły
15 </button>
16 </div>
17 </div>
18
19 <div className="p-4 border rounded-lg">
20 <h3 className="font-semibold">Historia podróży</h3>
21 <ul className="mt-2 space-y-2">
22 <li className="flex justify-between">
23 <span>Europa - Stacja Badawcza</span>
24 <span>24 marca 2150</span>
25 </li>
26 <li className="flex justify-between">
27 <span>Księżyc - Baza Armstrong</span>
28 <span>12 stycznia 2150</span>
29 </li>
30 </ul>
31 </div>
32
33 <div className="p-4 border rounded-lg">
34 <h3 className="font-semibold">Rekomendowane destynacje</h3>
35 <ul className="mt-2 space-y-2">
36 <li>
37 <a href="/destinations/titan" className="text-indigo-600 hover:underline">
38 Tytan - nowo otwarte kolonie
39 </a>
40 </li>
41 <li>
42 <a href="/destinations/venus-orbiter" className="text-indigo-600 hover:underline">
43 Orbiter Wenus - pobyt naukowy
44 </a>
45 </li>
46 </ul>
47 </div>
48 </div>
49 </div>
50 );
51}1// app/dashboard/@stats/page.tsx - Statystyki
2export default function StatsPage() {
3 // W rzeczywistej aplikacji, te dane byłyby pobierane z API
4 const stats = [
5 { label: 'Łączna liczba lotów', value: '7' },
6 { label: 'Odwiedzone planety', value: '3' },
7 { label: 'Punkty lojalnościowe', value: '12,540' },
8 { label: 'Status członka', value: 'Gold' }
9 ];
10
11 return (
12 <div>
13 <h2 className="text-xl font-semibold mb-4">Statystyki podróży</h2>
14
15 <div className="space-y-4">
16 {stats.map((stat, index) => (
17 <div key={index} className="flex justify-between">
18 <span className="text-gray-600">{stat.label}:</span>
19 <span className="font-semibold">{stat.value}</span>
20 </div>
21 ))}
22 </div>
23
24 <div className="mt-6 pt-4 border-t">
25 <div className="flex justify-between items-center">
26 <span className="text-gray-600">Poziom następnego statusu:</span>
27 <span className="text-sm font-medium">Platinum</span>
28 </div>
29 <div className="mt-2 w-full bg-gray-200 rounded-full h-2.5">
30 <div className="bg-indigo-600 h-2.5 rounded-full" style={{ width: '70%' }}></div>
31 </div>
32 <p className="mt-1 text-xs text-right text-gray-500">7,460 punktów do następnego poziomu</p>
33 </div>
34 </div>
35 );
36}1// app/dashboard/@activity/page.tsx - Aktywność
2export default function ActivityPage() {
3 // W rzeczywistej aplikacji, te dane byłyby pobierane z API
4 const activities = [
5 { type: 'reservation', message: 'Zarezerwowałeś podróż na Mars', time: '2 godziny temu' },
6 { type: 'check-in', message: 'Odbyto odprawę online na lot QV-105', time: '3 dni temu' },
7 { type: 'payment', message: 'Dokonano płatności 12,500 CR', time: '1 tydzień temu' },
8 { type: 'review', message: 'Napisałeś recenzję dla Bazy Armstrong', time: '2 tygodnie temu' }
9 ];
10
11 return (
12 <div>
13 <h2 className="text-xl font-semibold mb-4">Ostatnia aktywność</h2>
14
15 <div className="space-y-4">
16 {activities.map((activity, index) => (
17 <div key={index} className="flex items-start">
18 <div className={`mt-1 rounded-full w-2 h-2 mr-2 \${
19 activity.type === 'reservation' ? 'bg-green-500' :
20 activity.type === 'check-in' ? 'bg-blue-500' :
21 activity.type === 'payment' ? 'bg-purple-500' :
22 'bg-yellow-500'
23 }\`}></div>
24 <div>
25 <p className="text-sm">{activity.message}</p>
26 <p className="text-xs text-gray-500">{activity.time}</p>
27 </div>
28 </div>
29 ))}
30 </div>
31
32 <div className="mt-6 flex justify-center">
33 <button className="text-sm text-indigo-600 hover:underline">
34 Zobacz wszystkie aktywności
35 </button>
36 </div>
37 </div>
38 );
39}Ten przykład demonstruje, jak równoległe trasy umożliwiają tworzenie złożonych układów, gdzie różne części strony mogą być niezależnie aktualizowane i ładowane.
Podobnie jak zaawansowany system transportowy w Metropolii Quantum umożliwia mieszkańcom płynne przemieszczanie się pomiędzy różnymi częściami miasta, tak nawigacja w Next.js 15 oferuje deweloperom i użytkownikom niezwykle wydajny i intuicyjny system poruszania się po aplikacji.
Kluczowe elementy nawigacji w Next.js 15:
Komponent Link - podstawowy element nawigacji, który umożliwia szybkie przejścia między stronami bez pełnego przeładowania, dzięki automatycznemu prefetchingowi i optymalizacjom.
useRouter - hook umożliwiający programistyczną nawigację, idealne do dynamicznego kierowania użytkowników na podstawie określonych warunków czy zdarzeń.
usePathname i useSearchParams - hooki do monitorowania bieżącej ścieżki i parametrów zapytania, umożliwiające reaktywne dostosowywanie UI.
Intercepting Routes - zaawansowana funkcja pozwalająca na przechwytywanie nawigacji do określonych ścieżek i wyświetlanie alternatywnego UI, często w formie modalu.
Parallel Routing - umożliwia renderowanie wielu stron lub komponentów w jednym widoku, idealne do złożonych dashboardów i interfejsów z wieloma regionami nawigacyjnymi.
Te narzędzia, wykorzystane razem, umożliwiają tworzenie wydajnych, intuicyjnych i zaawansowanych systemów nawigacji, które znacząco poprawiają doświadczenie użytkownika i wydajność aplikacji.
W następnym rozdziale, odkryjemy techniki grupowania i organizacji tras, które pomogą utrzymać porządek i strukturę w bardziej złożonych aplikacjach, podobnie jak planowanie stref w Metropolii Quantum pomaga utrzymać miasto zorganizowane i efektywne.