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.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.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.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:Link components are automatically optimized for the App Router application, taking into account features like prefetching of dynamic routes.The
Link component offers several advanced options that enhance its functionality: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}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}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}1<Link
2 href="/destinations"
3 scroll={false} // Disables automatic scrolling to top of page
4>
5 Destinations (no scrolling)
6</Link>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.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>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.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';).The
useRouter hook provides several useful methods: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}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}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}Navigates to the previous page in browser history:
1function goBack() {
2 router.back();
3}Navigates to the next page in browser history:
1function goForward() {
2 router.forward();
3}Prefetches the given URL:
1function prefetchImportantPage() {
2 router.prefetch('/important-page');
3}1'use client';
2
3import { useState, useEffect } from 'react';
4import { useRouter } from 'next/navigation';
5
6export default function NavigationAssistant() {
7 const router = useRouter();
8 const [destination, setDestination] = useState('');
9 const [suggestions, setSuggestions] = useState([]);
10
11 useEffect(() => {
12 if (destination.length < 2) {
13 setSuggestions([]);
14 return;
15 }
16
17 // 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}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}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.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.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.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.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".
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:
(.) - intercepts segments at the same level(..) - intercepts segments one level up(..)(..) - intercepts segments two levels up(...) - intercepts segments from the application rootDirectory 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 modalImplementation:
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:
/destinations/mars/gallery?image=1, the page is intercepted by the modalIn 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".
Parallel routes in Next.js 15 enable rendering multiple pages or components in the same view, which is particularly useful for:
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" slotImplementation:
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.
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:
Link Component - the fundamental navigation element that enables fast transitions between pages without a full reload, thanks to automatic prefetching and optimizations.
useRouter - a hook enabling programmatic navigation, ideal for dynamically directing users based on specific conditions or events.
usePathname and useSearchParams - hooks for monitoring the current path and query parameters, enabling reactive UI adjustments.
Intercepting Routes - an advanced feature allowing navigation interception to specific paths and displaying alternative UI, often in the form of a modal.
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.