W Metropolii Quantum, system transportowy umożliwia mieszkańcom dotarcie do każdego zakątka miasta, a nawet do niezbadanych jeszcze terytoriów. Specjalne pojazdy Q-Hoppery potrafią zebrać wiele celów podróży i odwiedzić je wszystkie po kolei, dostosowując się dynamicznie do potrzeb pasażerów. Ta elastyczność jest kluczowa dla funkcjonowania tak zaawansowanej struktury miejskiej.
W świecie Next.js 15, podobną elastyczność zapewniają zaawansowane funkcje routingu, takie jak zagnieżdżone ścieżki i parametry catch-all. Pozwalają one na obsługę złożonych struktur URL i dynamiczne generowanie tras w oparciu o dane.
W Next.js 15, możemy tworzyć zagnieżdżone ścieżki dynamiczne, łącząc segmenty statyczne i dynamiczne na różnych poziomach hierarchii:
1app/
2 ├── destinations/ # /destinations
3 │ ├── page.tsx # Strona z listą destynacji
4 │ └── [destinationId]/ # Dynamiczny segment dla ID destynacji
5 │ ├── page.tsx # /destinations/:destinationId
6 │ └── attractions/ # /destinations/:destinationId/attractions
7 │ ├── page.tsx # Strona z atrakcjami dla destynacji
8 │ └── [attractionId] # Zagnieżdżony segment dynamiczny
9 │ └── page.tsx # /destinations/:destinationId/attractions/:attractionIdW tej strukturze, mamy dwa poziomy dynamicznych segmentów:
[destinationId] i [attractionId]. To pozwala na tworzenie ścieżek takich jak /destinations/mars/attractions/olympus-mons.Oto przykład jak zaimplementować system zagnieżdżonych ścieżek dla naszej aplikacji Quantum Voyages:
1// app/destinations/[destinationId]/page.tsx
2import { getDestinationById } from '@/lib/api';
3import { DestinationDetails } from '@/components/destinations/DestinationDetails';
4import { AttractionsPreview } from '@/components/destinations/AttractionsPreview';
5import Link from 'next/link';
6import { notFound } from 'next/navigation';
7
8// Generowanie statycznych parametrów dla najpopularniejszych destynacji
9export async function generateStaticParams() {
10 // Pobierz najpopularniejsze destynacje dla prerendering
11 const destinations = await getPopularDestinations();
12
13 return destinations.map((destination) => ({
14 destinationId: destination.id,
15 }));
16}
17
18export default async function DestinationPage({ params }: { params: Promise<{ destinationId: string }> }) {
19 const { destinationId } = await params;
20 const destination = await getDestinationById(destinationId);
21
22 // Jeśli destynacja nie istnieje, wyświetl stronę 404
23 if (!destination) {
24 notFound();
25 }
26
27 return (
28 <div className="container mx-auto px-4 py-12">
29 <h1 className="text-4xl font-bold mb-6">{destination.name}</h1>
30
31 <DestinationDetails destination={destination} />
32
33 <section className="mt-12">
34 <div className="flex justify-between items-center mb-6">
35 <h2 className="text-2xl font-semibold">Popularne atrakcje</h2>
36 <Link
37 href={`/destinations/\${destinationId}/attractions\`}
38 className="text-indigo-600 hover:underline"
39 >
40 Zobacz wszystkie atrakcje
41 </Link>
42 </div>
43
44 <AttractionsPreview destinationId={destinationId} />
45 </section>
46 </div>
47 );
48}1// app/destinations/[destinationId]/attractions/page.tsx
2import { getDestinationById, getAttractionsForDestination } from '@/lib/api';
3import Link from 'next/link';
4import Image from 'next/image';
5import { notFound } from 'next/navigation';
6
7export default async function DestinationAttractionsPage({ params }: { params: Promise<{ destinationId: string }> }) {
8 const { destinationId } = await params;
9 const destination = await getDestinationById(destinationId);
10
11 if (!destination) {
12 notFound();
13 }
14
15 const attractions = await getAttractionsForDestination(destinationId);
16
17 return (
18 <div className="container mx-auto px-4 py-12">
19 <div className="flex items-center gap-2 mb-8 text-sm text-gray-500">
20 <Link href="/destinations" className="hover:text-indigo-600">Destynacje</Link>
21 <span>/</span>
22 <Link href={`/destinations/\${destinationId}\`} className="hover:text-indigo-600">
23 {destination.name}
24 </Link>
25 <span>/</span>
26 <span>Atrakcje</span>
27 </div>
28
29 <h1 className="text-3xl font-bold mb-2">Atrakcje na {destination.name}</h1>
30 <p className="text-gray-600 mb-8">{destination.description}</p>
31
32 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
33 {attractions.map((attraction) => (
34 <Link
35 key={attraction.id}
36 href={`/destinations/\${destinationId}/attractions/\${attraction.id}\`}
37 className="block group"
38 >
39 <div className="rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-shadow">
40 <div className="relative h-48">
41 <Image
42 src={attraction.imageUrl}
43 alt={attraction.name}
44 fill
45 className="object-cover transition-transform group-hover:scale-105"
46 />
47 </div>
48 <div className="p-4">
49 <h2 className="text-xl font-semibold mb-2">{attraction.name}</h2>
50 <p className="text-gray-600 line-clamp-2">{attraction.shortDescription}</p>
51 </div>
52 </div>
53 </Link>
54 ))}
55 </div>
56 </div>
57 );
58}1// app/destinations/[destinationId]/attractions/[attractionId]/page.tsx
2import { getDestinationById, getAttractionById } from '@/lib/api';
3import Link from 'next/link';
4import Image from 'next/image';
5import { notFound } from 'next/navigation';
6
7export default async function AttractionPage({
8 params
9}: {
10 params: Promise<{ destinationId: string; attractionId: string }>
11}) {
12 const { destinationId, attractionId } = await params;
13
14 const [destination, attraction] = await Promise.all([
15 getDestinationById(destinationId),
16 getAttractionById(destinationId, attractionId)
17 ]);
18
19 if (!destination || !attraction) {
20 notFound();
21 }
22
23 return (
24 <div className="container mx-auto px-4 py-12">
25 <div className="flex items-center gap-2 mb-8 text-sm text-gray-500">
26 <Link href="/destinations" className="hover:text-indigo-600">Destynacje</Link>
27 <span>/</span>
28 <Link href={`/destinations/\${destinationId}\`} className="hover:text-indigo-600">
29 {destination.name}
30 </Link>
31 <span>/</span>
32 <Link href={`/destinations/\${destinationId}/attractions\`} className="hover:text-indigo-600">
33 Atrakcje
34 </Link>
35 <span>/</span>
36 <span>{attraction.name}</span>
37 </div>
38
39 <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
40 <div className="relative h-[400px] lg:h-[600px] rounded-lg overflow-hidden">
41 <Image
42 src={attraction.imageUrl}
43 alt={attraction.name}
44 fill
45 className="object-cover"
46 priority
47 />
48 </div>
49
50 <div>
51 <h1 className="text-4xl font-bold mb-4">{attraction.name}</h1>
52 <div className="flex items-center mb-6">
53 <span className="px-3 py-1 bg-indigo-100 text-indigo-800 rounded-full text-sm">
54 {attraction.category}
55 </span>
56 <span className="mx-4 text-gray-300">|</span>
57 <div className="flex items-center text-amber-500">
58 {/* Gwiazdki oceny */}
59 {Array.from({ length: 5 }).map((_, i) => (
60 <svg key={i} xmlns="http://www.w3.org/2000/svg" className={`h-5 w-5 \${i < attraction.rating ? 'fill-current' : 'text-gray-300'}\`} viewBox="0 0 20 20" fill="currentColor">
61 <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
62 </svg>
63 ))}
64 <span className="ml-2 text-gray-700">{attraction.rating.toFixed(1)}</span>
65 </div>
66 </div>
67
68 <p className="text-gray-700 mb-6">{attraction.description}</p>
69
70 <div className="space-y-4 mb-8">
71 <div className="flex">
72 <div className="w-32 font-semibold">Lokalizacja:</div>
73 <div>{attraction.location}</div>
74 </div>
75 <div className="flex">
76 <div className="w-32 font-semibold">Koszt zwiedzania:</div>
77 <div>{attraction.price} kredytów</div>
78 </div>
79 <div className="flex">
80 <div className="w-32 font-semibold">Czas zwiedzania:</div>
81 <div>{attraction.duration}</div>
82 </div>
83 </div>
84
85 <button className="w-full py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
86 Zarezerwuj wizytę
87 </button>
88 </div>
89 </div>
90 </div>
91 );
92}W Metropolii Quantum, technologia Q-Route umożliwia podróżowanie do dowolnej liczby miejsc na podstawie jednego kodu trasy. Podobnie, w Next.js 15, parametry catch-all umożliwiają obsługę ścieżek z dowolną liczbą segmentów.
Parametry catch-all są oznaczane trzema kropkami przed nazwą parametru w nawiasach kwadratowych:
1[...param]Ta notacja pozwala na dopasowanie dowolnej liczby segmentów URL, które zostaną przekazane jako tablica do komponentu.
1app/
2 ├── shop/ # /shop
3 │ └── [...categories]/ # /shop/category1
4 │ └── page.tsx # /shop/category1/category2/category3/etc.W tym przykładzie, ścieżka
/shop/clothing/men/shirts zostanie dopasowana przez [...categories] i parametr categories będzie zawierał tablicę ['clothing', 'men', 'shirts'].Implementacja systemu kategorii produktów z parametrami catch-all:
1// app/shop/[...categories]/page.tsx
2import { fetchCategorizedProducts } from '@/lib/api/products';
3import { Breadcrumbs } from '@/components/shop/Breadcrumbs';
4import { ProductGrid } from '@/components/shop/ProductGrid';
5import { CategoryFilters } from '@/components/shop/CategoryFilters';
6
7export default async function CategoriesPage({ params }: { params: Promise<{ categories: string[] }> }) {
8 const { categories } = await params;
9
10 // Pobieranie produktów na podstawie wszystkich przekazanych kategorii
11 const { products, availableFilters } = await fetchCategorizedProducts(categories);
12
13 // Konstruowanie tytułu na podstawie ostatniej kategorii
14 const currentCategory = categories[categories.length - 1];
15 const formattedCategoryName = currentCategory
16 .split('-')
17 .map(word => word.charAt(0).toUpperCase() + word.slice(1))
18 .join(' ');
19
20 return (
21 <div className="container mx-auto px-4 py-12">
22 <Breadcrumbs categories={categories} />
23
24 <h1 className="text-3xl font-bold mb-8">{formattedCategoryName}</h1>
25
26 <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
27 <div className="lg:col-span-1">
28 <CategoryFilters
29 categories={categories}
30 availableFilters={availableFilters}
31 />
32 </div>
33
34 <div className="lg:col-span-3">
35 <ProductGrid products={products} />
36 </div>
37 </div>
38 </div>
39 );
40}1// components/shop/Breadcrumbs.tsx
2import Link from 'next/link';
3
4export function Breadcrumbs({ categories }: { categories: string[] }) {
5 return (
6 <div className="flex items-center flex-wrap gap-2 mb-8 text-sm text-gray-500">
7 <Link href="/shop" className="hover:text-indigo-600">
8 Sklep
9 </Link>
10
11 {categories.map((category, index) => {
12 // Budowanie ścieżki URL dla tej kategorii
13 const url = `/shop/\${categories.slice(0, index + 1).join('/')}`;
14 const isLast = index === categories.length - 1;
15
16 const formattedName = category
17 .split('-')
18 .map(word => word.charAt(0).toUpperCase() + word.slice(1))
19 .join(' ');
20
21 return (
22 <div key={category} className="flex items-center">
23 <span className="mx-2">/</span>
24 {isLast ? (
25 <span>{formattedName}</span>
26 ) : (
27 <Link href={url} className="hover:text-indigo-600">
28 {formattedName}
29 </Link>
30 )}
31 </div>
32 );
33 })}
34 </div>
35 );
36}W Next.js 15 możemy też zdefiniować opcjonalne parametry catch-all, używając podwójnych nawiasów kwadratowych:
1[[...param]]Główna różnica polega na tym, że opcjonalny parametr catch-all dopasuje też ścieżkę bez żadnych segmentów. Na przykład,
app/shop/[[...categories]]/page.tsx dopasuje:/shop (w tym przypadku categories będzie pustą tablicą [])/shop/clothing (categories = ['clothing'])/shop/clothing/men/shirts (categories = ['clothing', 'men', 'shirts'])1// app/shop/[[...categories]]/page.tsx
2import { fetchCategorizedProducts, fetchFeaturedProducts } from '@/lib/api/products';
3import { Breadcrumbs } from '@/components/shop/Breadcrumbs';
4import { ProductGrid } from '@/components/shop/ProductGrid';
5import { CategoryFilters } from '@/components/shop/CategoryFilters';
6import { FeaturedProducts } from '@/components/shop/FeaturedProducts';
7import { CategoryPreview } from '@/components/shop/CategoryPreview';
8
9export default async function ShopPage({ params }: { params: Promise<{ categories?: string[] }> }) {
10 const categories = (await params).categories || [];
11 const isRootShopPage = categories.length === 0;
12
13 if (isRootShopPage) {
14 // Jesteśmy na głównej stronie sklepu
15 const featuredProducts = await fetchFeaturedProducts();
16 const topCategories = await fetchTopCategories();
17
18 return (
19 <div className="container mx-auto px-4 py-12">
20 <h1 className="text-4xl font-bold mb-12">Sklep Quantum Voyages</h1>
21
22 <section className="mb-16">
23 <h2 className="text-2xl font-semibold mb-8">Polecane produkty</h2>
24 <FeaturedProducts products={featuredProducts} />
25 </section>
26
27 <section>
28 <h2 className="text-2xl font-semibold mb-8">Przeglądaj kategorie</h2>
29 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
30 {topCategories.map(category => (
31 <CategoryPreview key={category.id} category={category} />
32 ))}
33 </div>
34 </section>
35 </div>
36 );
37 } else {
38 // Jesteśmy na stronie kategorii
39 const { products, availableFilters } = await fetchCategorizedProducts(categories);
40
41 // Konstruowanie tytułu na podstawie ostatniej kategorii
42 const currentCategory = categories[categories.length - 1];
43 const formattedCategoryName = currentCategory
44 .split('-')
45 .map(word => word.charAt(0).toUpperCase() + word.slice(1))
46 .join(' ');
47
48 return (
49 <div className="container mx-auto px-4 py-12">
50 <Breadcrumbs categories={categories} />
51
52 <h1 className="text-3xl font-bold mb-8">{formattedCategoryName}</h1>
53
54 <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
55 <div className="lg:col-span-1">
56 <CategoryFilters
57 categories={categories}
58 availableFilters={availableFilters}
59 />
60 </div>
61
62 <div className="lg:col-span-3">
63 <ProductGrid products={products} />
64 </div>
65 </div>
66 </div>
67 );
68 }
69}W Next.js 15, możemy uzyskać dostęp do parametrów ścieżki na kilka sposobów:
1// Server Component
2export default async function ProductPage({ params }: { params: Promise<{ productId: string }> }) {
3 // Użycie (await params).productId
4 return <div>Product ID: {(await params).productId}</div>;
5}W komponentach po stronie klienta, możemy używać hooka
useParams z next/navigation:1'use client';
2
3import { useParams } from 'next/navigation';
4
5export default function ClientProductDetails() {
6 const params = useParams();
7 const productId = params.productId;
8
9 return <div>Viewing product: {productId}</div>;
10}Next.js 15 oferuje funkcje do generowania statycznych parametrów i metadanych:
1// Generowanie statycznych parametrów
2export async function generateStaticParams() {
3 const products = await fetchTopProducts();
4
5 return products.map((product) => ({
6 productId: product.id,
7 }));
8}
9
10// Generowanie metadanych
11export async function generateMetadata({ params }: { params: Promise<{ productId: string }> }) {
12 const product = await fetchProductById((await params).productId);
13
14 return {
15 title: product.name,
16 description: product.description,
17 openGraph: {
18 images: [{ url: product.imageUrl }],
19 },
20 };
21}Podczas pracy z parametrami dynamicznymi, ważne jest, aby przeprowadzić ich walidację:
1export default async function ProductPage({ params }: { params: Promise<{ productId: string }> }) {
2 // Sprawdź, czy productId jest poprawnym UUID
3 if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test((await params).productId)) {
4 // Wyrzuć błąd, który zostanie przechwycony przez najbliższy error.js
5 throw new Error('Invalid product ID format');
6 }
7
8 const product = await fetchProductById((await params).productId);
9
10 // Jeśli produkt nie istnieje, wyświetl stronę 404
11 if (!product) {
12 notFound();
13 }
14
15 return <ProductDetails product={product} />;
16}Next.js 15 umożliwia eleganckie obsługiwanie sytuacji, gdy zasób nie zostanie znaleziony lub wystąpi błąd:
1// app/products/[productId]/error.tsx
2'use client';
3
4import { useEffect } from 'react';
5
6export default function ProductError({
7 error,
8 reset,
9}: {
10 error: Error & { digest?: string };
11 reset: () => void;
12}) {
13 useEffect(() => {
14 // Możesz zalogować błąd do zewnętrznego serwisu
15 console.error(error);
16 }, [error]);
17
18 return (
19 <div className="container mx-auto px-4 py-16 text-center">
20 <h2 className="text-2xl font-bold mb-4">Coś poszło nie tak!</h2>
21 <p className="mb-8 text-gray-600">{error.message}</p>
22 <button
23 onClick={reset}
24 className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
25 >
26 Spróbuj ponownie
27 </button>
28 </div>
29 );
30}1// app/products/[productId]/not-found.tsx
2import Link from 'next/link';
3
4export default function ProductNotFound() {
5 return (
6 <div className="container mx-auto px-4 py-16 text-center">
7 <h2 className="text-3xl font-bold mb-4">Produkt nie znaleziony</h2>
8 <p className="mb-8 text-gray-600">
9 Przepraszamy, nie mogliśmy znaleźć szukanego produktu.
10 </p>
11 <Link
12 href="/products"
13 className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
14 >
15 Wróć do wszystkich produktów
16 </Link>
17 </div>
18 );
19}W Metropolii Quantum, interaktywny system dokumentacji pozwala na nawigację przez złożone hierarchie informacji. Zaimplementujmy podobny system w Next.js 15:
1app/
2 ├── docs/
3 │ └── [[...slug]]/ # Opcjonalny catch-all
4 │ └── page.tsx # /docs, /docs/guides, /docs/guides/routing, etc.1// app/docs/[[...slug]]/page.tsx
2import { fetchDocumentBySlug, fetchDocCategories } from '@/lib/api/documentation';
3import { DocSidebar } from '@/components/docs/DocSidebar';
4import { DocContent } from '@/components/docs/DocContent';
5import { DocBreadcrumbs } from '@/components/docs/DocBreadcrumbs';
6import { DocPagination } from '@/components/docs/DocPagination';
7import { notFound } from 'next/navigation';
8
9export default async function DocPage({ params }: { params: Promise<{ slug?: string[] }> }) {
10 const slug = (await params).slug || [];
11 const slugPath = slug.join('/');
12
13 // Pobierz kategorie dokumentacji do menu bocznego
14 const categories = await fetchDocCategories();
15
16 // Jeśli nie podano ścieżki, wyświetl stronę główną dokumentacji
17 if (slug.length === 0) {
18 return (
19 <div className="container mx-auto px-4 py-12">
20 <div className="flex flex-col lg:flex-row gap-8">
21 <DocSidebar categories={categories} currentSlug="" />
22
23 <main className="flex-1">
24 <h1 className="text-4xl font-bold mb-8">Dokumentacja Quantum Voyages</h1>
25
26 <div className="prose prose-indigo max-w-none">
27 <p>
28 Witaj w centrum dokumentacji Quantum Voyages. Znajdziesz tutaj wszystkie informacje
29 potrzebne do zaplanowania i odbycia podróży kosmicznej, od podstawowych przewodników
30 po zaawansowane instrukcje techniczne.
31 </p>
32
33 <h2>Popularne sekcje</h2>
34
35 <div className="grid grid-cols-1 md:grid-cols-2 gap-6 not-prose">
36 {categories.map(category => (
37 <div key={category.slug} className="border rounded-lg p-6 hover:shadow-md transition-shadow">
38 <h3 className="text-xl font-semibold mb-2">{category.title}</h3>
39 <p className="text-gray-600 mb-4">{category.description}</p>
40 <a href={`/docs/\${category.slug}\`} className="text-indigo-600 hover:underline">
41 Przeglądaj {category.title.toLowerCase()} →
42 </a>
43 </div>
44 ))}
45 </div>
46 </div>
47 </main>
48 </div>
49 </div>
50 );
51 }
52
53 // Pobierz dokument na podstawie ścieżki
54 const document = await fetchDocumentBySlug(slugPath);
55
56 // Jeśli dokument nie istnieje, wyświetl stronę 404
57 if (!document) {
58 notFound();
59 }
60
61 // Pobierz poprzedni i następny dokument dla nawigacji
62 const { previousDoc, nextDoc } = await fetchAdjacentDocs(slugPath);
63
64 return (
65 <div className="container mx-auto px-4 py-12">
66 <DocBreadcrumbs slug={slug} />
67
68 <div className="flex flex-col lg:flex-row gap-8">
69 <DocSidebar categories={categories} currentSlug={slugPath} />
70
71 <main className="flex-1">
72 <h1 className="text-4xl font-bold mb-8">{document.title}</h1>
73
74 <DocContent content={document.content} />
75
76 <DocPagination previousDoc={previousDoc} nextDoc={nextDoc} />
77 </main>
78 </div>
79 </div>
80 );
81}
82
83// Generowanie statycznych parametrów dla popularnych ścieżek dokumentacji
84export async function generateStaticParams() {
85 const popularDocs = await fetchPopularDocs();
86
87 return popularDocs.map(doc => ({
88 slug: doc.slug.split('/'),
89 }));
90}Next.js 15 pozwala na łączenie parametrów catch-all z innymi zaawansowanymi funkcjami routingu:
1app/
2 ├── (dashboard)/
3 │ └── admin/
4 │ └── users/
5 │ └── [...roles]/ # /admin/users/admin/editor/etc.
6 │ └── page.tsx1// app/(shop)/products/[...categories]/(.)[productId]/page.tsx
2// Interceptuje /products/category1/category2/productId jako modal
3// podczas nawigacji z /products/category1/category21app/
2 ├── trips/
3 │ ├── page.tsx # /trips
4 │ ├── @filters/
5 │ │ └── [...params]/ # Równoległa trasa dla filtrów
6 │ │ └── page.tsx
7 │ └── @map/
8 │ └── [...location]/ # Równoległa trasa dla mapy
9 │ └── page.tsx1'use client';
2
3import { useRouter } from 'next/navigation';
4
5export function CategoryNavigator({ categories, subcategories }) {
6 const router = useRouter();
7
8 const handleCategoryChange = (categoryId) => {
9 const selectedCategory = categories.find(c => c.id === categoryId);
10 router.push(\`/shop/\${selectedCategory.slug}`);
11 };
12
13 const handleSubcategoryChange = (categoryId, subcategoryId) => {
14 const selectedCategory = categories.find(c => c.id === categoryId);
15 const selectedSubcategory = subcategories.find(sc => sc.id === subcategoryId);
16 router.push(\`/shop/\${selectedCategory.slug}/\${selectedSubcategory.slug}`);
17 };
18
19 return (
20 <div className="space-y-4">
21 <div>
22 <label className="block mb-2 text-sm font-medium">Kategoria</label>
23 <select
24 className="w-full p-2 border rounded"
25 onChange={(e) => handleCategoryChange(e.target.value)}
26 >
27 <option value="">Wybierz kategorię</option>
28 {categories.map(category => (
29 <option key={category.id} value={category.id}>
30 {category.name}
31 </option>
32 ))}
33 </select>
34 </div>
35
36 <div>
37 <label className="block mb-2 text-sm font-medium">Podkategoria</label>
38 <select
39 className="w-full p-2 border rounded"
40 onChange={(e) => handleSubcategoryChange(selectedCategory, e.target.value)}
41 >
42 <option value="">Wybierz podkategorię</option>
43 {/* Opcje podkategorii */}
44 </select>
45 </div>
46 </div>
47 );
48}1// app/search/[[...query]]/page.tsx
2import { searchProducts } from '@/lib/api/search';
3
4export default async function SearchPage({ params, searchParams }) {
5 const query = (await params).query || [];
6 const searchTerm = query.join(' ');
7
8 // Pobieranie parametrów zapytania
9 const { category, sort, filters } = await searchParams;
10
11 // Wykonanie wyszukiwania
12 const results = await searchProducts({
13 searchTerm,
14 category,
15 sort,
16 filters: filters ? JSON.parse(filters) : undefined
17 });
18
19 return (
20 <div className="container mx-auto px-4 py-12">
21 <h1 className="text-3xl font-bold mb-8">
22 {searchTerm
23 ? `Wyniki wyszukiwania dla: "\${searchTerm}"`
24 : 'Wyszukiwanie produktów'
25 }
26 </h1>
27
28 {/* Filtry wyszukiwania */}
29 <SearchFilters
30 initialCategory={category}
31 initialSort={sort}
32 initialFilters={filters ? JSON.parse(filters) : {}}
33 />
34
35 {/* Wyniki wyszukiwania */}
36 <SearchResults results={results} />
37 </div>
38 );
39}Zagnieżdżone ścieżki dynamiczne i parametry catch-all w Next.js 15 oferują niezwykle elastyczny system routingu, który może obsłużyć praktycznie każdy scenariusz nawigacji w nowoczesnych aplikacjach webowych:
Zagnieżdżone parametry dynamiczne umożliwiają tworzenie złożonych hierarchicznych struktur URL, takich jak
/destinations/:destinationId/attractions/:attractionId.Parametry catch-all (
[...param]) pozwalają na dopasowanie dowolnej liczby segmentów URL, co jest idealne dla systemów dokumentacji, kategorii produktów i elastycznych interfejsów wyszukiwania.Opcjonalne parametry catch-all (
[[...param]]) dodatkowo obsługują przypadek braku segmentów, co umożliwia użycie tego samego komponentu dla ścieżki bazowej i wszystkich jej podścieżek.Obsługa błędów i brakujących zasobów dzięki plikom
error.js i not-found.js zapewnia eleganckie doświadczenie użytkownika nawet w przypadku problemów.Walidacja parametrów zapobiega błędom i zapewnia bezpieczeństwo aplikacji.
Łącząc te techniki z funkcjami takimi jak generowanie statycznych parametrów (
generateStaticParams), możemy tworzyć wydajne, dynamiczne i łatwe w utrzymaniu systemy nawigacji dla aplikacji o dowolnej skali i złożoności.W następnym rozdziale, zgłębimy zaawansowane techniki pracy z middlewarem w Next.js 15, co pozwoli nam implementować takie funkcje jak autentykacja, kontrola dostępu i przekierowania na poziomie routingu.