Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Pierwsza aplikacja "Hello World" - krok po kroku - prosta strona dla firmy kosmicznej

W Metropolii Quantum, każdy inżynier pamięta swój pierwszy projekt - moment, gdy po raz pierwszy uruchomił własny system kwantowy. W świecie deweloperów Next.js, tym pierwszym krokiem jest stworzenie aplikacji "Hello World". W tym module przeprowadzimy Cię krok po kroku przez proces tworzenia prostej strony internetowej dla fikcyjnej firmy kosmicznej "Quantum Voyages", wykorzystując najnowsze funkcje Next.js 15 i App Router.

Przygotowanie projektu

Najpierw musimy stworzyć nowy projekt Next.js. Otwórz terminal i wykonaj następujące polecenie:

1npx create-next-app@latest quantum-voyages

W trakcie procesu inicjalizacji, odpowiedz na pytania konfiguracyjne:

1Would you like to use TypeScript? Yes
2Would you like to use ESLint? Yes
3Would you like to use Tailwind CSS? Yes
4Would you like to use src/ directory? Yes
5Would you like to use App Router? (recommended) Yes
6Would you like to customize the default import alias (@/*)? Yes (Wprowadź @/ jako alias)

Gdy proces instalacji zakończy się, przejdź do katalogu projektu:

1cd quantum-voyages

Struktura projektu

Po utworzeniu projektu, zobaczysz strukturę katalogów podobną do tej:

1quantum-voyages/
2├── node_modules/
3├── public/
4├── src/
5│   ├── app/
6│   │   ├── favicon.ico
7│   │   ├── globals.css
8│   │   ├── layout.tsx
9│   │   └── page.tsx
10├── .eslintrc.json
11├── next.config.js
12├── package.json
13├── postcss.config.js
14├── tailwind.config.ts
15└── tsconfig.json

Tworzenie strony głównej

Zacznijmy od modyfikacji strony głównej. Otwórz plik

src/app/page.tsx
i zastąp jego zawartość poniższym kodem:

1// src/app/page.tsx
2import Link from 'next/link';
3
4export default function HomePage() {
5  return (
6    <main className="flex min-h-screen flex-col items-center justify-center p-24">
7      <h1 className="text-6xl font-bold text-center bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent mb-8">
8        Quantum Voyages
9      </h1>
10      <p className="text-xl text-center max-w-2xl mb-8">
11        Odkrywamy wszechświat używając najnowszych technologii kwantowych. 
12        Dołącz do naszej międzygwiezdnej podróży i razem przekraczajmy granice możliwości.
13      </p>
14      <div className="flex gap-4">
15        <Link 
16          href="/oferta" 
17          className="px-6 py-3 rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-colors"
18        >
19          Nasza oferta
20        </Link>
21        <Link 
22          href="/kontakt" 
23          className="px-6 py-3 rounded-full bg-purple-600 text-white hover:bg-purple-700 transition-colors"
24        >
25          Kontakt
26        </Link>
27      </div>
28    </main>
29  );
30}

Globalne style i layout

Następnie zmodyfikujmy globalny layout naszej aplikacji. Otwórz plik

src/app/layout.tsx
i zaktualizuj go:

1// src/app/layout.tsx
2import './globals.css';
3import type { Metadata } from 'next';
4import { Inter } from 'next/font/google';
5
6const inter = Inter({ subsets: ['latin'] });
7
8export const metadata: Metadata = {
9  title: 'Quantum Voyages - Podróże międzygwiezdne',
10  description: 'Firma kosmiczna specjalizująca się w międzygwiezdnych podróżach kwantowych',
11};
12
13export default function RootLayout({
14  children,
15}: {
16  children: React.ReactNode;
17}) {
18  return (
19    <html lang="pl">
20      <body className={inter.className}>
21        {children}
22      </body>
23    </html>
24  );
25}

Tworzenie komponentu nagłówka (header)

Teraz stwórzmy wspólny nagłówek, który będzie wyświetlany na wszystkich stronach. Najpierw utworzymy katalog dla współdzielonych komponentów:

1mkdir -p src/components/ui

Następnie utwórz plik

src/components/ui/Header.tsx
:

1// src/components/ui/Header.tsx
2"use client";
3
4import Link from 'next/link';
5import { useState } from 'react';
6
7export default function Header() {
8  const [isMenuOpen, setIsMenuOpen] = useState(false);
9  
10  return (
11    <header className="fixed w-full bg-black/80 backdrop-blur-md text-white z-10">
12      <div className="container mx-auto px-4 py-4 flex justify-between items-center">
13        <Link href="/" className="text-2xl font-bold">
14          Quantum Voyages
15        </Link>
16        
17        {/* Mobile Menu Button */}
18        <button 
19          className="md:hidden"
20          onClick={() => setIsMenuOpen(!isMenuOpen)}
21        >
22          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" className="w-6 h-6">
23            {isMenuOpen 
24              ? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
25              : <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16m-7 6h7" />
26            }
27          </svg>
28        </button>
29        
30        {/* Desktop Navigation */}
31        <nav className="hidden md:flex gap-6">
32          <Link href="/" className="hover:text-blue-400 transition-colors">
33            Strona główna
34          </Link>
35          <Link href="/oferta" className="hover:text-blue-400 transition-colors">
36            Nasza oferta
37          </Link>
38          <Link href="/o-nas" className="hover:text-blue-400 transition-colors">
39            O nas
40          </Link>
41          <Link href="/kontakt" className="hover:text-blue-400 transition-colors">
42            Kontakt
43          </Link>
44        </nav>
45      </div>
46      
47      {/* Mobile Navigation */}
48      {isMenuOpen && (
49        <nav className="flex flex-col md:hidden bg-black/90 py-4">
50          <Link 
51            href="/" 
52            className="px-4 py-2 hover:bg-blue-900/30"
53            onClick={() => setIsMenuOpen(false)}
54          >
55            Strona główna
56          </Link>
57          <Link 
58            href="/oferta" 
59            className="px-4 py-2 hover:bg-blue-900/30"
60            onClick={() => setIsMenuOpen(false)}
61          >
62            Nasza oferta
63          </Link>
64          <Link 
65            href="/o-nas" 
66            className="px-4 py-2 hover:bg-blue-900/30"
67            onClick={() => setIsMenuOpen(false)}
68          >
69            O nas
70          </Link>
71          <Link 
72            href="/kontakt" 
73            className="px-4 py-2 hover:bg-blue-900/30"
74            onClick={() => setIsMenuOpen(false)}
75          >
76            Kontakt
77          </Link>
78        </nav>
79      )}
80    </header>
81  );
82}

Zauważ, że użyliśmy dyrektywy

"use client"
na górze pliku, ponieważ ten komponent używa hooków React i obsługuje zdarzenia użytkownika.

Tworzenie komponentu stopki (footer)

Teraz stwórzmy stopkę, która również będzie obecna na wszystkich stronach. Utwórz plik

src/components/ui/Footer.tsx
:

1// src/components/ui/Footer.tsx
2import Link from 'next/link';
3
4export default function Footer() {
5  const currentYear = new Date().getFullYear();
6  
7  return (
8    <footer className="bg-black/80 backdrop-blur-md text-white py-8">
9      <div className="container mx-auto px-4">
10        <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
11          <div>
12            <h3 className="text-xl font-bold mb-4">Quantum Voyages</h3>
13            <p className="text-gray-300">
14              Odkrywamy wszechświat używając najnowszych technologii kwantowych.
15            </p>
16          </div>
17          
18          <div>
19            <h3 className="text-xl font-bold mb-4">Nawigacja</h3>
20            <ul className="space-y-2">
21              <li><Link href="/" className="text-gray-300 hover:text-white transition-colors">Strona główna</Link></li>
22              <li><Link href="/oferta" className="text-gray-300 hover:text-white transition-colors">Nasza oferta</Link></li>
23              <li><Link href="/o-nas" className="text-gray-300 hover:text-white transition-colors">O nas</Link></li>
24              <li><Link href="/kontakt" className="text-gray-300 hover:text-white transition-colors">Kontakt</Link></li>
25            </ul>
26          </div>
27          
28          <div>
29            <h3 className="text-xl font-bold mb-4">Kontakt</h3>
30            <address className="not-italic text-gray-300">
31              <p>Stacja Orbitalna Alfa</p>
32              <p>Orbita geostacjonarna, Ziemia</p>
33              <p>kontakt@quantum-voyages.space</p>
34            </address>
35          </div>
36        </div>
37        
38        <div className="border-t border-gray-800 mt-8 pt-8 text-center">
39          <p className="text-gray-400">© {currentYear} Quantum Voyages. Wszelkie prawa zastrzeżone.</p>
40        </div>
41      </div>
42    </footer>
43  );
44}

Aktualizacja głównego layoutu

Teraz zaktualizujmy główny layout aplikacji, aby zawierał utworzone przez nas nagłówek i stopkę:

1// src/app/layout.tsx
2import './globals.css';
3import type { Metadata } from 'next';
4import { Inter } from 'next/font/google';
5import Header from '@/components/ui/Header';
6import Footer from '@/components/ui/Footer';
7
8const inter = Inter({ subsets: ['latin'] });
9
10export const metadata: Metadata = {
11  title: 'Quantum Voyages - Podróże międzygwiezdne',
12  description: 'Firma kosmiczna specjalizująca się w międzygwiezdnych podróżach kwantowych',
13};
14
15export default function RootLayout({
16  children,
17}: {
18  children: React.ReactNode;
19}) {
20  return (
21    <html lang="pl">
22      <body className={inter.className + " bg-gradient-to-b from-gray-900 to-black text-white min-h-screen"}>
23        <Header />
24        <div className="pt-20">
25          {children}
26        </div>
27        <Footer />
28      </body>
29    </html>
30  );
31}

Aktualizacja strony głównej

Teraz dostosujmy stronę główną, aby lepiej pasowała do naszego nowego layoutu:

1// src/app/page.tsx
2import Link from 'next/link';
3import Image from 'next/image';
4
5export default function HomePage() {
6  return (
7    <>
8      {/* Hero Section */}
9      <section className="h-[90vh] flex flex-col items-center justify-center text-center p-4 relative overflow-hidden">
10        <div className="absolute inset-0 z-0">
11          <div className="absolute inset-0 bg-black/60 z-10"></div>
12          <Image 
13            src="/space.jpg" 
14            alt="Przestrzeń kosmiczna" 
15            fill 
16            className="object-cover"
17            priority
18          />
19        </div>
20        
21        <div className="relative z-20 max-w-4xl mx-auto">
22          <h1 className="text-6xl font-bold mb-6 bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
23            Quantum Voyages
24          </h1>
25          <p className="text-xl mb-8 text-gray-200">
26            Przełamujemy granice przestrzeni i czasu dzięki technologii kwantowej. 
27            Jesteśmy pionierami w dziedzinie międzygwiezdnej eksploracji.
28          </p>
29          <div className="flex flex-wrap gap-4 justify-center">
30            <Link 
31              href="/oferta" 
32              className="px-8 py-3 rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-colors"
33            >
34              Odkryj nasze misje
35            </Link>
36            <Link 
37              href="/kontakt" 
38              className="px-8 py-3 rounded-full bg-purple-600 text-white hover:bg-purple-700 transition-colors"
39            >
40              Dołącz do załogi
41            </Link>
42          </div>
43        </div>
44      </section>
45      
46      {/* Features Section */}
47      <section className="py-20 px-4">
48        <div className="container mx-auto">
49          <h2 className="text-4xl font-bold text-center mb-16">Nasze przewagi kwantowe</h2>
50          
51          <div className="grid grid-cols-1 md:grid-cols-3 gap-10">
52            <div className="p-6 bg-gradient-to-br from-blue-900/30 to-blue-800/10 rounded-xl backdrop-blur-sm">
53              <div className="w-16 h-16 bg-blue-600 rounded-lg flex items-center justify-center mb-4">
54                <svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
55                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
56                </svg>
57              </div>
58              <h3 className="text-xl font-semibold mb-2">Napęd kwantowy</h3>
59              <p className="text-gray-300">
60                Nasze statki kosmiczne wykorzystują najnowszą technologię napędu kwantowego, 
61                umożliwiającą podróże z prędkością przekraczającą prędkość światła.
62              </p>
63            </div>
64            
65            <div className="p-6 bg-gradient-to-br from-purple-900/30 to-purple-800/10 rounded-xl backdrop-blur-sm">
66              <div className="w-16 h-16 bg-purple-600 rounded-lg flex items-center justify-center mb-4">
67                <svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
68                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
69                </svg>
70              </div>
71              <h3 className="text-xl font-semibold mb-2">Osłony kwantowe</h3>
72              <p className="text-gray-300">
73                Zaawansowane osłony oparte na mechanice kwantowej zapewniają 
74                najwyższy poziom bezpieczeństwa podczas międzygwiezdnych podróży.
75              </p>
76            </div>
77            
78            <div className="p-6 bg-gradient-to-br from-teal-900/30 to-teal-800/10 rounded-xl backdrop-blur-sm">
79              <div className="w-16 h-16 bg-teal-600 rounded-lg flex items-center justify-center mb-4">
80                <svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
81                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
82                </svg>
83              </div>
84              <h3 className="text-xl font-semibold mb-2">Laboratoria kwantowe</h3>
85              <p className="text-gray-300">
86                Nasze statki wyposażone są w najnowocześniejsze laboratoria kwantowe, 
87                umożliwiające badania naukowe w przestrzeni kosmicznej.
88              </p>
89            </div>
90          </div>
91        </div>
92      </section>
93      
94      {/* Call to Action */}
95      <section className="py-16 px-4 bg-gradient-to-r from-blue-900/40 to-purple-900/40">
96        <div className="container mx-auto text-center">
97          <h2 className="text-3xl font-bold mb-4">Gotowy na międzygwiezdną przygodę?</h2>
98          <p className="text-xl text-gray-300 mb-8 max-w-2xl mx-auto">
99            Dołącz do społeczności odkrywców kwantowych i bądź częścią 
100            następnego wielkiego kroku ludzkości w eksploracji kosmosu.
101          </p>
102          <Link 
103            href="/kontakt" 
104            className="px-8 py-3 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 text-white hover:from-blue-700 hover:to-purple-700 transition-colors"
105          >
106            Dowiedz się więcej
107          </Link>
108        </div>
109      </section>
110    </>
111  );
112}

Przygotowanie zasobów

Aby nasza strona wyglądała atrakcyjnie, potrzebujemy obrazu tła dla sekcji hero. Pobierz zdjęcie przestrzeni kosmicznej (lub użyj własnego) i umieść je w katalogu

public/
jako
space.jpg
.

Tworzenie podstrony "Oferta"

Teraz stwórzmy podstronę z ofertą firmy. Utwórz katalog

src/app/oferta
i plik
page.tsx
wewnątrz:

1// src/app/oferta/page.tsx
2import Image from 'next/image';
3
4interface MissionProps {
5  title: string;
6  description: string;
7  image: string;
8  destination: string;
9  duration: string;
10  price: string;
11}
12
13function Mission({ title, description, image, destination, duration, price }: MissionProps) {
14  return (
15    <div className="bg-gradient-to-br from-gray-800/50 to-gray-900/50 rounded-xl overflow-hidden backdrop-blur-sm">
16      <div className="relative h-64">
17        <Image 
18          src={image} 
19          alt={title} 
20          fill
21          className="object-cover"
22        />
23      </div>
24      <div className="p-6">
25        <h3 className="text-2xl font-bold mb-2">{title}</h3>
26        <p className="text-gray-300 mb-4">{description}</p>
27        <div className="grid grid-cols-2 gap-4 text-sm">
28          <div>
29            <p className="text-gray-400">Cel podróży</p>
30            <p className="font-medium">{destination}</p>
31          </div>
32          <div>
33            <p className="text-gray-400">Czas trwania</p>
34            <p className="font-medium">{duration}</p>
35          </div>
36          <div className="col-span-2">
37            <p className="text-gray-400">Koszt</p>
38            <p className="font-bold text-xl text-blue-400">{price}</p>
39          </div>
40        </div>
41      </div>
42    </div>
43  );
44}
45
46export default function OfertaPage() {
47  const missions = [
48    {
49      title: "Wycieczka Orbitalna",
50      description: "Idealna dla początkujących odkrywców kosmosu. Doświadcz nieważkości i podziwiaj Ziemię z orbity.",
51      image: "/mission-orbit.jpg",
52      destination: "Niska orbita Ziemi",
53      duration: "3 dni",
54      price: "250,000 ¤"
55    },
56    {
57      title: "Weekend na Księżycu",
58      description: "Spędź niezapomniany weekend w luksusowym kompleksie na powierzchni Księżyca.",
59      image: "/mission-moon.jpg",
60      destination: "Księżyc",
61      duration: "5 dni",
62      price: "750,000 ¤"
63    },
64    {
65      title: "Ekspedycja na Marsa",
66      description: "Odkryj czerwoną planetę i bądź jednym z pierwszych ludzi, którzy postawili na niej stopę.",
67      image: "/mission-mars.jpg",
68      destination: "Mars",
69      duration: "3 miesiące",
70      price: "5,000,000 ¤"
71    },
72    {
73      title: "Podróż do Proximy Centauri",
74      description: "Międzygwiezdna ekspedycja do najbliższej gwiazdy poza naszym Układem Słonecznym.",
75      image: "/mission-proxima.jpg",
76      destination: "Proxima Centauri",
77      duration: "5 lat (czas na pokładzie: 6 miesięcy)",
78      price: "25,000,000 ¤"
79    }
80  ];
81
82  return (
83    <div className="container mx-auto px-4 py-16">
84      <div className="text-center mb-16">
85        <h1 className="text-5xl font-bold mb-4">Nasze misje kosmiczne</h1>
86        <p className="text-xl text-gray-300 max-w-3xl mx-auto">
87          Wybierz swoją podróż kosmiczną spośród naszych ofert. Od krótkiej wycieczki orbitalnej 
88          po międzygwiezdne ekspedycje - każda z nich zmieni na zawsze Twoje postrzeganie wszechświata.
89        </p>
90      </div>
91      
92      <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
93        {missions.map((mission, index) => (
94          <Mission key={index} {...mission} />
95        ))}
96      </div>
97      
98      <div className="mt-16 p-8 bg-gradient-to-r from-blue-900/30 to-purple-900/30 rounded-xl backdrop-blur-sm">
99        <h2 className="text-3xl font-bold mb-4">Misje niestandardowe</h2>
100        <p className="text-gray-300 mb-6">
101          Poszukujesz czegoś wyjątkowego? Oferujemy ekskluzywne, niestandardowe misje 
102          dostosowane do Twoich konkretnych oczekiwań i potrzeb badawczych.
103        </p>
104        <button className="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 rounded-full hover:from-blue-700 hover:to-purple-700 transition-colors">
105          Skontaktuj się z nami
106        </button>
107      </div>
108    </div>
109  );
110}

Dla tej podstrony potrzebne będą także obrazy. Możesz dodać własne pliki do katalogu

public/
lub użyć istniejących obrazów.

Tworzenie podstrony "Kontakt"

Teraz stwórzmy podstronę kontaktową z formularzem. Utwórz katalog

src/app/kontakt
i plik
page.tsx
wewnątrz:

1// src/app/kontakt/page.tsx
2import ContactForm from './ContactForm';
3
4export default function KontaktPage() {
5  return (
6    <div className="container mx-auto px-4 py-16">
7      <div className="text-center mb-16">
8        <h1 className="text-5xl font-bold mb-4">Skontaktuj się z nami</h1>
9        <p className="text-xl text-gray-300 max-w-3xl mx-auto">
10          Masz pytania dotyczące naszych misji kosmicznych lub chcesz dołączyć do załogi? 
11          Skorzystaj z poniższego formularza lub odwiedź nasze biuro.
12        </p>
13      </div>
14      
15      <div className="grid grid-cols-1 lg:grid-cols-2 gap-16">
16        <div>
17          <h2 className="text-3xl font-semibold mb-6">Wyślij wiadomość</h2>
18          <ContactForm />
19        </div>
20        
21        <div>
22          <h2 className="text-3xl font-semibold mb-6">Dane kontaktowe</h2>
23          
24          <div className="space-y-6">
25            <div className="flex items-start">
26              <div className="bg-blue-600 p-3 rounded-lg mr-4">
27                <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
28                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
29                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
30                </svg>
31              </div>
32              <div>
33                <h3 className="text-xl font-medium mb-1">Adres</h3>
34                <address className="not-italic text-gray-300">
35                  <p>Stacja Orbitalna Alfa</p>
36                  <p>Orbita geostacjonarna, Ziemia</p>
37                </address>
38              </div>
39            </div>
40            
41            <div className="flex items-start">
42              <div className="bg-blue-600 p-3 rounded-lg mr-4">
43                <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
44                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
45                </svg>
46              </div>
47              <div>
48                <h3 className="text-xl font-medium mb-1">Email</h3>
49                <p className="text-gray-300">kontakt@quantum-voyages.space</p>
50              </div>
51            </div>
52            
53            <div className="flex items-start">
54              <div className="bg-blue-600 p-3 rounded-lg mr-4">
55                <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
56                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
57                </svg>
58              </div>
59              <div>
60                <h3 className="text-xl font-medium mb-1">Telefon</h3>
61                <p className="text-gray-300">+0 (123) 456-7890</p>
62              </div>
63            </div>
64          </div>
65          
66          <div className="mt-10">
67            <h2 className="text-3xl font-semibold mb-6">Godziny pracy</h2>
68            <div className="bg-gradient-to-br from-gray-800/50 to-gray-900/50 rounded-xl p-6 backdrop-blur-sm">
69              <div className="flex justify-between py-2 border-b border-gray-700">
70                <span>Poniedziałek - Piątek:</span>
71                <span>09:00 - 18:00</span>
72              </div>
73              <div className="flex justify-between py-2 border-b border-gray-700">
74                <span>Sobota:</span>
75                <span>10:00 - 15:00</span>
76              </div>
77              <div className="flex justify-between py-2">
78                <span>Niedziela:</span>
79                <span>Zamknięte</span>
80              </div>
81            </div>
82          </div>
83        </div>
84      </div>
85    </div>
86  );
87}

Teraz utwórzmy komponent formularza kontaktowego w pliku

src/app/kontakt/ContactForm.tsx
:

1// src/app/kontakt/ContactForm.tsx
2"use client";
3
4import { useState } from 'react';
5
6export default function ContactForm() {
7  const [formData, setFormData] = useState({
8    name: '',
9    email: '',
10    subject: '',
11    message: ''
12  });
13  
14  const [formStatus, setFormStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
15  
16  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
17    const { name, value } = e.target;
18    setFormData(prevData => ({
19      ...prevData,
20      [name]: value
21    }));
22  };
23  
24  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
25    e.preventDefault();
26    setFormStatus('loading');
27    
28    // W rzeczywistej aplikacji tutaj byłaby obsługa formularza, np. wysłanie danych do API
29    try {
30      // Symulacja opóźnienia odpowiedzi serwera
31      await new Promise(resolve => setTimeout(resolve, 1500));
32      
33      // Symulacja sukcesu
34      setFormStatus('success');
35      setFormData({
36        name: '',
37        email: '',
38        subject: '',
39        message: ''
40      });
41    } catch (error) {
42      setFormStatus('error');
43    }
44  };
45  
46  return (
47    <form onSubmit={handleSubmit} className="space-y-6">
48      <div>
49        <label htmlFor="name" className="block text-sm font-medium mb-1">
50          Imię i nazwisko <span className="text-red-500">*</span>
51        </label>
52        <input
53          type="text"
54          id="name"
55          name="name"
56          value={formData.name}
57          onChange={handleChange}
58          required
59          className="w-full px-4 py-2 bg-gray-800/60 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
60        />
61      </div>
62      
63      <div>
64        <label htmlFor="email" className="block text-sm font-medium mb-1">
65          Email <span className="text-red-500">*</span>
66        </label>
67        <input
68          type="email"
69          id="email"
70          name="email"
71          value={formData.email}
72          onChange={handleChange}
73          required
74          className="w-full px-4 py-2 bg-gray-800/60 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
75        />
76      </div>
77      
78      <div>
79        <label htmlFor="subject" className="block text-sm font-medium mb-1">
80          Temat <span className="text-red-500">*</span>
81        </label>
82        <select
83          id="subject"
84          name="subject"
85          value={formData.subject}
86          onChange={handleChange}
87          required
88          className="w-full px-4 py-2 bg-gray-800/60 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
89        >
90          <option value="">Wybierz temat</option>
91          <option value="rezerwacja">Rezerwacja misji</option>
92          <option value="informacje">Informacje o misjach</option>
93          <option value="praca">Dołączenie do załogi</option>
94          <option value="inne">Inne</option>
95        </select>
96      </div>
97      
98      <div>
99        <label htmlFor="message" className="block text-sm font-medium mb-1">
100          Wiadomość <span className="text-red-500">*</span>
101        </label>
102        <textarea
103          id="message"
104          name="message"
105          value={formData.message}
106          onChange={handleChange}
107          required
108          rows={5}
109          className="w-full px-4 py-2 bg-gray-800/60 border border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
110        ></textarea>
111      </div>
112      
113      <button
114        type="submit"
115        disabled={formStatus === 'loading'}
116        className="w-full px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg hover:from-blue-700 hover:to-purple-700 transition-colors disabled:opacity-70"
117      >
118        {formStatus === 'loading' ? 'Wysyłanie...' : 'Wyślij wiadomość'}
119      </button>
120      
121      {formStatus === 'success' && (
122        <div className="p-4 bg-green-900/40 border border-green-500 rounded-lg text-green-200">
123          Dziękujemy za wiadomość! Odpowiemy tak szybko, jak to możliwe.
124        </div>
125      )}
126      
127      {formStatus === 'error' && (
128        <div className="p-4 bg-red-900/40 border border-red-500 rounded-lg text-red-200">
129          Wystąpił błąd podczas wysyłania wiadomości. Proszę spróbować ponownie później.
130        </div>
131      )}
132    </form>
133  );
134}

Tworzenie strony 404 (Not Found)

Dodajmy stronę 404, która będzie wyświetlana, gdy użytkownik wejdzie na nieistniejącą ścieżkę. Utwórz plik

src/app/not-found.tsx
:

1// src/app/not-found.tsx
2import Link from 'next/link';
3
4export default function NotFound() {
5  return (
6    <div className="min-h-[80vh] flex flex-col items-center justify-center p-4 text-center">
7      <h1 className="text-9xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">
8        404
9      </h1>
10      <h2 className="text-4xl font-bold mt-4 mb-6">Zagubiony w kosmosie</h2>
11      <p className="text-xl text-gray-300 mb-8 max-w-xl">
12        Wygląda na to, że zabłądziłeś w głębokim kosmosie. Niestety, strona której 
13        szukasz znajduje się poza naszym zasięgiem kwantowym.
14      </p>
15      <Link 
16        href="/" 
17        className="px-8 py-3 rounded-full bg-gradient-to-r from-blue-600 to-purple-600 text-white hover:from-blue-700 hover:to-purple-700 transition-colors"
18      >
19        Wróć na Ziemię
20      </Link>
21    </div>
22  );
23}

Uruchomienie aplikacji

Teraz możemy uruchomić naszą aplikację lokalnie za pomocą polecenia:

1npm run dev

Po uruchomieniu serwera deweloperskiego, nawiguj do http://localhost:3000 w przeglądarce, aby zobaczyć swoją stronę "Hello World" dla firmy kosmicznej.

Budowanie i wdrażanie

Gdy jesteś zadowolony z działania aplikacji lokalnie, możesz ją zbudować i przygotować do wdrożenia:

1npm run build

Polecenie to wygeneruje zoptymalizowaną wersję produkcyjną aplikacji w katalogu

.next/
. Możesz także przetestować tę wersję lokalnie, uruchamiając:

1npm run start

Wdrażanie na Vercel

Next.js jest tworzony i utrzymywany przez Vercel, więc wdrożenie na tej platformie jest niezwykle proste:

  1. Utwórz konto na Vercel (jeśli jeszcze go nie masz)
  2. Połącz swoje repozytorium z Vercelem (GitHub, GitLab lub Bitbucket)
  3. Wybierz swój projekt i kliknij "Import"
  4. Vercel automatycznie wykryje, że jest to aplikacja Next.js i skonfiguruje wdrożenie
  5. Kliknij "Deploy" i poczekaj na zakończenie procesu

Po zakończeniu wdrożenia, Vercel dostarczy Ci URL, pod którym Twoja aplikacja jest dostępna publicznie.

Co dalej?

Stworzyliśmy prostą, ale elegancką i funkcjonalną stronę dla firmy kosmicznej z wykorzystaniem Next.js 15 i App Router. Oto kilka sugestii do dalszego rozwijania aplikacji:

  1. Dodaj podstronę "O nas" z historią i misją firmy
  2. Zaimplementuj animacje za pomocą bibliotek takich jak Framer Motion
  3. Dodaj autentyczną obsługę formularza z wykorzystaniem Server Actions
  4. Zintegruj CMS (np. Contentful, Sanity) do zarządzania treścią
  5. Dodaj międzynarodowość (i18n) do obsługi wielu języków
  6. Zaimplementuj system uwierzytelniania dla klientów i członków załogi

W miarę rozbudowy aplikacji, pamiętaj o zasadach i najlepszych praktykach poznanych w poprzednich modułach, szczególnie o właściwym podziale na Server i Client Components.

Gratulacje! Właśnie stworzyłeś swoją pierwszą aplikację Next.js 15 z wykorzystaniem App Router.

Vai a CodeWorlds