We use cookies to enhance your experience on the site
CodeWorlds

Navigation with the Link Component and useRouter

In the Quantum Metropolis, the transportation system allows residents to move smoothly between different districts and facilities. These advanced vehicles, called Q-Links, can instantly transport passengers to virtually any location in the city, without the need to reload the entire transport infrastructure.

In the world of Next.js 15, the

Link
component and
useRouter
hook play a similar role. These tools enable creating smooth, efficient navigation between different parts of the application, without the need to reload the entire page.

Link - The Most Important Transport Component

The

Link
component is the fundamental navigation element in Next.js 15, working like a Q-Link in the Quantum Metropolis - it takes the user to a specified destination without unnecessary delays or disruptions.

Basic Usage of the Link Component

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        Home
11      </Link>
12      <Link 
13        href="/destinations" 
14        className="text-indigo-600 hover:text-indigo-800 transition-colors"
15      >
16        Destinations
17      </Link>
18      <Link 
19        href="/flights" 
20        className="text-indigo-600 hover:text-indigo-800 transition-colors"
21      >
22        Flights
23      </Link>
24      <Link 
25        href="/about" 
26        className="text-indigo-600 hover:text-indigo-800 transition-colors"
27      >
28        About Us
29      </Link>
30    </nav>
31  );
32}

This simple code creates a basic navigation bar that allows users to move between different sections of the application. Notice that the

Link
component accepts properties like
href
(navigation target) and we can pass it classic HTML attributes like
className
for styling.

Key Advantages of the Link Component over the Regular <a> Tag

Just like Q-Links in the Quantum Metropolis, which offer much more than traditional vehicles, the

Link
component in Next.js 15 surpasses the regular HTML
<a>
tag in many ways:

  1. Prefetching - automatically loads the target page in the background when the link is visible in the browser viewport, which drastically speeds up navigation.
  2. Client-side navigation - transitions between pages without a full page reload, updating only what is necessary.
  3. Automatic optimization -
    Link
    components are automatically optimized for the App Router application, taking into account features like prefetching of dynamic routes.
  4. Focus management - after navigation, focus is properly transferred, improving the application's accessibility.

Advanced Link Options

The

Link
component offers several advanced options that enhance its functionality:

1. Dynamic Routes

1// Destination card component
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. Passing Query Parameters

1// Search filter component
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        Planets
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        Moons
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        Stars
31      </Link>
32    </div>
33  );
34}

3. Overriding Default Behavior

1// "Scroll to top" button component
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="Scroll to top"
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. Scroll and Focus Handling

1<Link 
2  href="/destinations" 
3  scroll={false} // Disables automatic scrolling to top of page
4>
5  Destinations (no scrolling)
6</Link>

5. Prefetch

1<Link 
2  href="/rarely-visited-page" 
3  prefetch={false} // Disables automatic prefetching
4>
5  Rarely visited page
6</Link>

By default, Link automatically prefetches pages that are visible in the viewport. You can control this with the

prefetch
property.

Using <a> Tags with the Link Component

In Next.js 15, the

Link
component automatically passes appropriate attributes to the
<a>
tag, so you don't need to nest an additional
<a>
tag inside the
Link
component. This improvement allows for more concise code:

1// Correct usage in Next.js 15
2<Link 
3  href="/destinations"
4  className="text-blue-600 hover:underline"
5  target="_blank"
6  rel="noopener noreferrer"
7>
8  View Destinations
9</Link>

useRouter - Programmatic Navigation

While the

Link
component is ideal for declarative navigation (visible in the UI), sometimes we need to navigate programmatically - similarly to the Quantum Metropolis, where the central artificial intelligence can direct Q-Links to new locations based on various conditions or events.

In Next.js 15, the

useRouter
hook from the
next/navigation
package enables exactly this kind of dynamic navigation.

Basic Usage of 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    // Form processing logic...
12    const bookingSuccess = await submitBooking(formData);
13    
14    if (bookingSuccess) {
15      // After successful submission, redirect the user
16      router.push('/booking/confirmation');
17    }
18  };
19  
20  return (
21    <form onSubmit={handleSubmit} className="space-y-6">
22      {/* Form fields... */}
23      <button 
24        type="submit" 
25        className="w-full py-2 px-4 bg-indigo-600 text-white rounded"
26      >
27        Book a Trip
28      </button>
29    </form>
30  );
31}

Note: Remember that

useRouter
is a React hook, so it can only be used in Client components (marked with
'use client';
).

useRouter Hook Methods

The

useRouter
hook provides several useful methods:

1. router.push(url)

Navigates to the given URL, adding a new entry to the browser history:

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)

Similar to

push
, but replaces the current history entry instead of adding a new one:

1function redirectToLogin() {
2  router.replace('/login'); // The user won't be able to go back to the previous page with the "Back" button
3}

3. router.refresh()

Refreshes the current page, re-fetching data while preserving the DOM state:

1function refreshData() {
2  router.refresh(); // Re-fetches data from the server without a full page reload
3}

4. router.back()

Navigates to the previous page in browser history:

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

5. router.forward()

Navigates to the next page in browser history:

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

6. router.prefetch(href)

Prefetches the given URL:

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

Practical useRouter Usage Examples

Example 1: Navigation Assistant

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    // Simulating fetching suggestions from API
18    const fetchSuggestions = async () => {
19      // In a real application, we would fetch data from the API
20      const mockSuggestions = [
21        { id: 'mars', name: 'Mars', type: 'planet' },
22        { id: 'mars-colony-alpha', name: 'Mars Colony Alpha', type: 'colony' },
23        { id: 'martian-canyon', name: 'Great Martian Canyon', 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    // Route selection based on suggestion type
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}

Example 2: Multi-Step Booking Form

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      // Here we would normally send data to the API
33      console.log('Sending data:', formData);
34      
35      // Simulating waiting for API response
36      await new Promise(resolve => setTimeout(resolve, 1000));
37      
38      // Redirect to confirmation page with booking ID
39      router.push('/booking/confirmation?id=QV123456789');
40    } catch (error) {
41      console.error('Booking error:', error);
42      // Error handling
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>Destination</span>
66          <span>Trip Details</span>
67          <span>Contact Information</span>
68        </div>
69      </div>
70      
71      {step === 1 && (
72        <div className="space-y-4">
73          <h2 className="text-2xl font-semibold">Choose a Destination</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">Discover the wonders of {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              Next
99            </button>
100          </div>
101        </div>
102      )}
103      
104      {step === 2 && (
105        <div className="space-y-4">
106          <h2 className="text-2xl font-semibold">Trip Details</h2>
107          
108          <div className="space-y-4">
109            <div>
110              <label className="block mb-1">Departure Date</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">Return Date</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">Number of Passengers</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              Back
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              Next
161            </button>
162          </div>
163        </div>
164      )}
165      
166      {step === 3 && (
167        <div className="space-y-4">
168          <h2 className="text-2xl font-semibold">Contact Information</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">Phone</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">Summary</h3>
194            <div className="grid grid-cols-2 gap-2 text-sm">
195              <div>Destination:</div>
196              <div className="font-medium">{formData.destination}</div>
197              
198              <div>Departure Date:</div>
199              <div className="font-medium">{formData.departureDate}</div>
200              
201              <div>Return Date:</div>
202              <div className="font-medium">{formData.returnDate}</div>
203              
204              <div>Passengers:</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              Back
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              Book a Trip
226            </button>
227          </div>
228        </div>
229      )}
230    </div>
231  );
232}

usePathname and useSearchParams - Route Monitoring

In the Quantum Metropolis, the traffic monitoring system constantly tracks Q-Link positions, enabling the central AI to adjust transport parameters in real time. In Next.js 15, the

usePathname
and
useSearchParams
hooks serve a similar function.

usePathname - Tracking the Current Path

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: 'Home', href: '/' },
11    { label: 'Destinations', href: '/destinations' },
12    { label: 'Flights', href: '/flights' },
13    { label: 'Offers', href: '/offers' },
14    { label: 'About Us', 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              My Account
52            </Link>
53          </div>
54        </div>
55      </div>
56    </nav>
57  );
58}

In this example,

usePathname
allows us to determine the current path, which enables highlighting the active item in the navigation menu.

useSearchParams - Accessing Query Parameters

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 ? `Search results for "\${query}"` : 'All destinations'}
16      </h1>
17      <div className="flex items-center text-sm text-gray-600">
18        <span>Category: <strong className="capitalize">{category}</strong></span>
19        <span className="mx-2"></span>
20        <span>Sorting: <strong className="capitalize">{
21          sort === 'relevance' ? 'Relevance' :
22          sort === 'price-asc' ? 'Price: lowest first' :
23          sort === 'price-desc' ? 'Price: highest first' :
24          sort === 'rating' ? 'Rating' : 'Relevance'
25        }</strong></span>
26      </div>
27    </div>
28  );
29}

This code demonstrates how

useSearchParams
allows reading and utilizing query parameters from the URL, which can determine various aspects of the displayed page.

Combining useSearchParams with Navigation

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  // Get current search parameters
11  const currentCategory = searchParams.get('category') || 'all';
12  const currentSort = searchParams.get('sort') || 'relevance';
13  const currentQuery = searchParams.get('q') || '';
14  
15  // Function to update parameters while preserving existing ones
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">Categories</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' ? 'All' : 
47               category === 'planets' ? 'Planets' :
48               category === 'moons' ? 'Moons' : 'Stacje kosmiczne'}
49            </button>
50          ))}
51        </div>
52      </div>
53      
54      <div>
55        <h2 className="text-lg font-medium mb-2">Sorting</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">Relevance</option>
62          <option value="price-asc">Price: lowest first</option>
63          <option value="price-desc">Price: highest first</option>
64          <option value="rating">Rating</option>
65          <option value="distance">Distance</option>
66        </select>
67      </div>
68    </div>
69  );
70}

This example demonstrates how to effectively combine

useSearchParams
with
useRouter
and
usePathname
to create interactive filters that update the URL and page content without a full reload.

Intercepting Routes

In the Quantum Metropolis, sometimes a Q-Link can change its route mid-flight to handle a passenger's special request without interrupting the main journey. In Next.js 15, this concept is implemented through "Intercepting Routes".

Basic Concept of Route Interception

Route interception allows you to "intercept" normal navigation to a specific path and display an alternative UI, often an overlay (modal) on the current page, instead of a complete page change.

The implementation is based on a special folder naming convention:

  1. (.)
    - intercepts segments at the same level
  2. (..)
    - intercepts segments one level up
  3. (..)(..)
    - intercepts segments two levels up
  4. (...)
    - intercepts segments from the application root

Example: Photo Gallery with Modal

Directory structure:

1app/
2  ├── destinations/
3  │   ├── page.tsx              # Destinations list page /destinations
4  │   └── [planet]/
5  │       ├── page.tsx          # Specific planet page /destinations/mars
6  │       ├── gallery/
7  │       │   └── page.tsx      # Full gallery /destinations/mars/gallery
8  │       └── @modal/
9  │           └── (..)gallery/
10  │               └── page.tsx  # Intercepts /destinations/mars/gallery as modal

Implementation:

1// app/destinations/page.tsx - Destinations home
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: 'Titan', 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">Galactic Destinations</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 - Specific planet page
2import Link from 'next/link';
3
4export default async function PlanetPage({ params }: { params: Promise<{ planet: string }> }) {
5  const { planet } = await params;
6  
7  // Simulated gallery data
8  const galleryImages = Array.from({ length: 6 }, (_, i) => ({
9    id: i + 1,
10    src: `/images/\${planet}/\${i + 1}.jpg`,
11    alt: `Photo of \${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">Gallery</h2>
27          <Link 
28            href={`/destinations/\${planet}/gallery\`}
29            className="text-indigo-600 hover:text-indigo-800"
30          >
31            View all
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 - Full gallery
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  // Verify that the planet exists
10  const validPlanets = ['mars', 'europa', 'titan'];
11  if (!validPlanets.includes(planet)) {
12    notFound();
13  }
14  
15  // Simulated gallery data
16  const galleryImages = Array.from({ length: 12 }, (_, i) => ({
17    id: i + 1,
18    src: `/images/\${planet}/\${i + 1}.jpg`,
19    alt: `Photo of \${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          ← Back to {planet}
31        </Link>
32        <h1 className="text-3xl font-bold capitalize">
33          Gallery: {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 - Gallery modal
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  // If no imageId, redirect to gallery
16  useEffect(() => {
17    if (!imageId) {
18      router.replace(`/destinations/\${planet}/gallery`);
19    }
20  }, [imageId, planet, router]);
21  
22  // Handle ESC key
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  // Get image data
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: `Photo of \${planet} #\${i + 1}`,
55    title: `\${planet.charAt(0).toUpperCase() + planet.slice(1)} - widok #\${i + 1}`,
56    description: `Detailed description of image #\${i + 1} from planet \${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="Close"
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                  ← Previous
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                  Next →
113                </button>
114              )}
115            </div>
116          </div>
117        </div>
118      </div>
119    </div>
120  );
121}

In this advanced example:

  1. When navigating to
    /destinations/mars/gallery?image=1
    , the page is intercepted by the modal
  2. The user can browse images in the modal without leaving the context of the main page
  3. After closing the modal, the user returns to the previous page without losing context

Parallel Routing

In the Quantum Metropolis, advanced Q-Links can split into two paths to simultaneously serve different destinations and then merge back together, optimizing urban transport flow. In Next.js 15, the equivalent of this technology is "Parallel Routing".

Basic Concept of Parallel Routes

Parallel routes in Next.js 15 enable rendering multiple pages or components in the same view, which is particularly useful for:

  1. Loading content in parallel with different priorities
  2. Independent navigation between multiple page components
  3. Creating complex layouts with multiple navigational regions

The implementation uses folders with the

@
prefix:

1app/
2  ├── dashboard/
3  │   ├── layout.tsx            # Page layout
4  │   ├── page.tsx              # Main content
5  │   ├── @stats/               # "stats" Slot
6  │   │   └── page.tsx          # Default page in "stats" slot
7  │   └── @activity/            # "activity" Slot
8  │       └── page.tsx          # Default page in "activity" slot

Example: Space Travel Agency Dashboard

Implementation:

1// app/dashboard/layout.tsx - Layout with parallel routes
2export default function DashboardLayout({
3  children,   // Main content
4  stats,      // @stats Slot
5  activity    // @activity Slot
6}) {
7  return (
8    <div className="container mx-auto px-4 py-8">
9      <h1 className="text-3xl font-bold mb-8">Quantum Voyages Control Panel</h1>
10      
11      <div className="grid grid-cols-12 gap-8">
12        {/* Main content */}
13        <div className="col-span-12 lg:col-span-6">
14          {children}
15        </div>
16        
17        {/* Statistics panel */}
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        {/* Activity panel */}
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 - Main content
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">Booking Overview</h2>
6      
7      <div className="space-y-6">
8        <div className="p-4 border rounded-lg">
9          <h3 className="font-semibold">Upcoming Trip</h3>
10          <p className="mt-2">Mars - Colony Alpha, June 12, 2150</p>
11          <div className="mt-4 flex justify-between">
12            <span className="text-indigo-600">Status: Confirmed</span>
13            <button className="text-sm text-indigo-600 hover:underline">
14              Details
15            </button>
16          </div>
17        </div>
18        
19        <div className="p-4 border rounded-lg">
20          <h3 className="font-semibold">Travel History</h3>
21          <ul className="mt-2 space-y-2">
22            <li className="flex justify-between">
23              <span>Europa - Research Station</span>
24              <span>March 24, 2150</span>
25            </li>
26            <li className="flex justify-between">
27              <span>Moon - Armstrong Base</span>
28              <span>January 12, 2150</span>
29            </li>
30          </ul>
31        </div>
32        
33        <div className="p-4 border rounded-lg">
34          <h3 className="font-semibold">Recommended Destinations</h3>
35          <ul className="mt-2 space-y-2">
36            <li>
37              <a href="/destinations/titan" className="text-indigo-600 hover:underline">
38                Titan - newly opened colonies
39              </a>
40            </li>
41            <li>
42              <a href="/destinations/venus-orbiter" className="text-indigo-600 hover:underline">
43                Venus Orbiter - scientific stay
44              </a>
45            </li>
46          </ul>
47        </div>
48      </div>
49    </div>
50  );
51}
1// app/dashboard/@stats/page.tsx - Statistics
2export default function StatsPage() {
3  // In a real application, this data would be fetched from the API
4  const stats = [
5    { label: 'Total Flights', value: '7' },
6    { label: 'Visited Planets', value: '3' },
7    { label: 'Loyalty Points', value: '12,540' },
8    { label: 'Member Status', value: 'Gold' }
9  ];
10  
11  return (
12    <div>
13      <h2 className="text-xl font-semibold mb-4">Travel Statistics</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">Next status level:</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 points to next level</p>
33      </div>
34    </div>
35  );
36}
1// app/dashboard/@activity/page.tsx - Activity
2export default function ActivityPage() {
3  // In a real application, this data would be fetched from the API
4  const activities = [
5    { type: 'reservation', message: 'You booked a trip to Mars', time: '2 hours ago' },
6    { type: 'check-in', message: 'Online check-in completed for flight QV-105', time: '3 days ago' },
7    { type: 'payment', message: 'Payment of 12,500 CR completed', time: '1 week ago' },
8    { type: 'review', message: 'You wrote a review for Armstrong Base', time: '2 weeks ago' }
9  ];
10  
11  return (
12    <div>
13      <h2 className="text-xl font-semibold mb-4">Recent Activity</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          View all activities
35        </button>
36      </div>
37    </div>
38  );
39}

This example demonstrates how parallel routes enable creating complex layouts where different parts of the page can be independently updated and loaded.

Summary

Just as the advanced transportation system in the Quantum Metropolis enables residents to move smoothly between different parts of the city, navigation in Next.js 15 offers developers and users an extremely efficient and intuitive system for moving through the application.

Key elements of navigation in Next.js 15:

  1. Link Component - the fundamental navigation element that enables fast transitions between pages without a full reload, thanks to automatic prefetching and optimizations.

  2. useRouter - a hook enabling programmatic navigation, ideal for dynamically directing users based on specific conditions or events.

  3. usePathname and useSearchParams - hooks for monitoring the current path and query parameters, enabling reactive UI adjustments.

  4. Intercepting Routes - an advanced feature allowing navigation interception to specific paths and displaying alternative UI, often in the form of a modal.

  5. Parallel Routing - enables rendering multiple pages or components in a single view, ideal for complex dashboards and interfaces with multiple navigational regions.

These tools, used together, enable creating efficient, intuitive, and advanced navigation systems that significantly improve user experience and application performance.

In the next chapter, we will explore route grouping and organization techniques that help maintain order and structure in more complex applications, just as zone planning in the Quantum Metropolis helps keep the city organized and efficient.

Go to CodeWorlds