Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Nawigacja z komponentem Link i useRouter

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.

Link – najważniejszy komponent transportowy

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ń.

Podstawowe użycie komponentu Link

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.

Kluczowe przewagi komponentu Link nad zwykłym tagiem <a>

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:

  1. Prefetching (wstępne ładowanie) - automatycznie ładuje docelową stronę w tle, gdy link jest widoczny w oknie przeglądarki, co drastycznie przyspiesza nawigację.
  2. Client-side navigation - przechodzi między stronami bez pełnego przeładowania strony, aktualizując tylko to, co konieczne.
  3. Automatyczna optymalizacja - komponenty
    Link
    są automatycznie optymalizowane dla aplikacji App Router, uwzględniając funkcje takie jak prefetching dynamicznych tras.
  4. Zarządzanie fokusem - po nawigacji, focus jest odpowiednio przenoszony, co poprawia dostępność aplikacji.

Zaawansowane opcje Link

Komponent

Link
oferuje kilka zaawansowanych opcji, które zwiększają jego funkcjonalność:

1. Dynamiczne trasy

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}

2. Przekazywanie parametrów zapytania

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}

3. Zastępowanie domyślnego zachowania

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}

4. Obsługa przewijania i fokusa

1<Link 
2  href="/destinations" 
3  scroll={false} // Wyłącza automatyczne przewijanie do góry strony
4>
5  Destynacje (bez przewijania)
6</Link>

5. Prefetch

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
.

Zastosowanie tagów <a> z komponentem Link

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>

useRouter – nawigacja programistyczna

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ę.

Podstawowe użycie useRouter

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';
).

Metody hooka useRouter

Hook

useRouter
udostępnia kilka przydatnych metod:

1. router.push(url)

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}

2. router.replace(url)

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}

3. router.refresh()

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}

4. router.back()

Przechodzi do poprzedniej strony w historii przeglądarki:

1function goBack() {
2  router.back();
3}

5. router.forward()

Przechodzi do następnej strony w historii przeglądarki:

1function goForward() {
2  router.forward();
3}

6. router.prefetch(href)

Wstępnie ładuje podany URL:

1function prefetchImportantPage() {
2  router.prefetch('/important-page');
3}

Praktyczne przykłady użycia useRouter

Przykład 1: Asystent nawigacji

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}

Przykład 2: Wieloetapowy formularz rezerwacji

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}

usePathname i useSearchParams - monitorowanie trasy

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
.

usePathname - śledzenie bieżącej ścieżki

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.

useSearchParams - dostęp do parametrów zapytania

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.

Łączenie useSearchParams z nawigacją

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.

Intercepting Routes - przechwytywanie tras

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).

Podstawowa koncepcja przechwytywania 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:

  1. (.)
    - przechwytuje segmenty na tym samym poziomie
  2. (..)
    - przechwytuje segmenty o jeden poziom wyżej
  3. (..)(..)
    - przechwytuje segmenty dwa poziomy wyżej
  4. (...)
    - przechwytuje segmenty od korzenia aplikacji

Przykład: Galeria zdjęć z modalem

Struktura 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 modal

Implementacja:

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:

  1. Przy nawigacji do
    /destinations/mars/gallery?image=1
    strona jest przechwytywana przez modal
  2. Użytkownik może przeglądać obrazy w modalu bez opuszczania kontekstu strony głównej
  3. Po zamknięciu modalu, użytkownik wraca do poprzedniej strony bez utraty kontekstu

Parallel Routing - równoległe trasy

W 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).

Podstawowa koncepcja równoległych tras

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:

  1. Równoległego ładowania zawartości o różnych priorytetach
  2. Niezależnej nawigacji między wieloma komponentami stron
  3. Tworzenia złożonych układów z kilkoma regionami nawigacyjnymi

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"

Przykład: Dashboard kosmicznej agencji podróży

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.

Podsumowanie

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:

  1. 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.

  2. useRouter - hook umożliwiający programistyczną nawigację, idealne do dynamicznego kierowania użytkowników na podstawie określonych warunków czy zdarzeń.

  3. usePathname i useSearchParams - hooki do monitorowania bieżącej ścieżki i parametrów zapytania, umożliwiające reaktywne dostosowywanie UI.

  4. Intercepting Routes - zaawansowana funkcja pozwalająca na przechwytywanie nawigacji do określonych ścieżek i wyświetlanie alternatywnego UI, często w formie modalu.

  5. 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.

Ir a CodeWorlds