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.
Najpierw musimy stworzyć nowy projekt Next.js. Otwórz terminal i wykonaj następujące polecenie:
1npx create-next-app@latest quantum-voyagesW 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-voyagesPo 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.jsonZacznijmy 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}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}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/uiNastę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.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}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}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}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.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.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}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}Teraz możemy uruchomić naszą aplikację lokalnie za pomocą polecenia:
1npm run devPo uruchomieniu serwera deweloperskiego, nawiguj do http://localhost:3000 w przeglądarce, aby zobaczyć swoją stronę "Hello World" dla firmy kosmicznej.
Gdy jesteś zadowolony z działania aplikacji lokalnie, możesz ją zbudować i przygotować do wdrożenia:
1npm run buildPolecenie to wygeneruje zoptymalizowaną wersję produkcyjną aplikacji w katalogu
.next/. Możesz także przetestować tę wersję lokalnie, uruchamiając:1npm run startNext.js jest tworzony i utrzymywany przez Vercel, więc wdrożenie na tej platformie jest niezwykle proste:
Po zakończeniu wdrożenia, Vercel dostarczy Ci URL, pod którym Twoja aplikacja jest dostępna publicznie.
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:
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.