Next.js course Β· Module 1: Next.js Configuration
First "Hello World" Application - Step by Step - A Simple Page for a Space Company
In this lesson16
In Quantum Metropolis, every engineer remembers their first project - the moment they first launched their own quantum system. In the world of Next.js developers, that first step is creating a "Hello World" application. In this lesson, we'll walk you through the process of creating a simple website for the fictional space company "Quantum Voyages", using the latest features of Next.js 16 and App Router.
Project Setup
First, we need to create a new Next.js project. Open your terminal and run the following command:
1npx create-next-app@latest quantum-voyagesDuring the initialization process, answer the configuration questions:
1Would you like to use the recommended Next.js defaults? No, customize settings
2Would you like to use TypeScript? Yes
3Which linter would you like to use? ESLint
4Would you like to use React Compiler? No
5Would you like to use Tailwind CSS? Yes
6Would you like your code inside a `src/` directory? Yes
7Would you like to use App Router? (recommended) Yes
8Would you like to customize the import alias (`@/*` by default)? No (the default @/* alias is enough)
9Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? YesWhen the installation process is complete, navigate to the project directory:
1cd quantum-voyagesProject Structure
After creating the project, you'll see a directory structure similar to this:
1quantum-voyages/
2βββ node_modules/
3βββ public/
4βββ src/
5β βββ app/
6β β βββ favicon.ico
7β β βββ globals.css
8β β βββ layout.tsx
9β β βββ page.tsx
10βββ AGENTS.md
11βββ CLAUDE.md
12βββ eslint.config.mjs
13βββ next-env.d.ts
14βββ next.config.ts
15βββ package.json
16βββ postcss.config.mjs
17βββ tsconfig.jsonCreating the Home Page
Let's start by modifying the home page. Open the src/app/page.tsx file and replace its content with the following code:
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 We explore the universe using the latest quantum technologies.
12 Join our interstellar journey and let's push the boundaries of possibility together.
13 </p>
14 <div className="flex gap-4">
15 <Link
16 href="/offer"
17 className="px-6 py-3 rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-colors"
18 >
19 Our offer
20 </Link>
21 <Link
22 href="/contact"
23 className="px-6 py-3 rounded-full bg-purple-600 text-white hover:bg-purple-700 transition-colors"
24 >
25 Contact
26 </Link>
27 </div>
28 </main>
29 );
30}Global Styles and Layout
Next, let's modify the global layout of our application. Open the src/app/layout.tsx file and update it:
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 - Interstellar Travel',
10 description: 'A space company specializing in interstellar quantum travel',
11};
12
13export default function RootLayout({
14 children,
15}: {
16 children: React.ReactNode;
17}) {
18 return (
19 <html lang="en">
20 <body className={inter.className}>
21 {children}
22 </body>
23 </html>
24 );
25}Creating the Header Component
Now let's create a shared header that will be displayed on all pages. First, we'll create a directory for shared components:
1mkdir -p src/components/uiThen create the file 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 Home
34 </Link>
35 <Link href="/offer" className="hover:text-blue-400 transition-colors">
36 Our offer
37 </Link>
38 <Link href="/about" className="hover:text-blue-400 transition-colors">
39 About us
40 </Link>
41 <Link href="/contact" className="hover:text-blue-400 transition-colors">
42 Contact
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 Home
56 </Link>
57 <Link
58 href="/offer"
59 className="px-4 py-2 hover:bg-blue-900/30"
60 onClick={() => setIsMenuOpen(false)}
61 >
62 Our offer
63 </Link>
64 <Link
65 href="/about"
66 className="px-4 py-2 hover:bg-blue-900/30"
67 onClick={() => setIsMenuOpen(false)}
68 >
69 About us
70 </Link>
71 <Link
72 href="/contact"
73 className="px-4 py-2 hover:bg-blue-900/30"
74 onClick={() => setIsMenuOpen(false)}
75 >
76 Contact
77 </Link>
78 </nav>
79 )}
80 </header>
81 );
82}Note that we used the "use client" directive at the top of the file, because this component uses React hooks and handles user events.
Creating the Footer Component
Now let's create a footer that will also be present on all pages. Create the file 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 We explore the universe using the latest quantum technologies.
15 </p>
16 </div>
17
18 <div>
19 <h3 className="text-xl font-bold mb-4">Navigation</h3>
20 <ul className="space-y-2">
21 <li><Link href="/" className="text-gray-300 hover:text-white transition-colors">Home</Link></li>
22 <li><Link href="/offer" className="text-gray-300 hover:text-white transition-colors">Our offer</Link></li>
23 <li><Link href="/about" className="text-gray-300 hover:text-white transition-colors">About us</Link></li>
24 <li><Link href="/contact" className="text-gray-300 hover:text-white transition-colors">Contact</Link></li>
25 </ul>
26 </div>
27
28 <div>
29 <h3 className="text-xl font-bold mb-4">Contact</h3>
30 <address className="not-italic text-gray-300">
31 <p>Alpha Orbital Station</p>
32 <p>Geostationary orbit, Earth</p>
33 <p>contact@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. All rights reserved.</p>
40 </div>
41 </div>
42 </footer>
43 );
44}Updating the Main Layout
Now let's update the main application layout to include our header and footer:
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 - Interstellar Travel',
12 description: 'A space company specializing in interstellar quantum travel',
13};
14
15export default function RootLayout({
16 children,
17}: {
18 children: React.ReactNode;
19}) {
20 return (
21 <html lang="en">
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}Updating the Home Page
Now let's adjust the home page to better fit our new layout:
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="Outer space"
15 fill
16 className="object-cover"
17 preload
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 We break the boundaries of space and time through quantum technology.
27 We are pioneers in the field of interstellar exploration.
28 </p>
29 <div className="flex flex-wrap gap-4 justify-center">
30 <Link
31 href="/offer"
32 className="px-8 py-3 rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-colors"
33 >
34 Discover our missions
35 </Link>
36 <Link
37 href="/contact"
38 className="px-8 py-3 rounded-full bg-purple-600 text-white hover:bg-purple-700 transition-colors"
39 >
40 Join the crew
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">Our quantum advantages</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">Quantum drive</h3>
59 <p className="text-gray-300">
60 Our spaceships use the latest quantum drive technology,
61 enabling travel at speeds exceeding the speed of light.
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">Quantum shields</h3>
72 <p className="text-gray-300">
73 Advanced shields based on quantum mechanics provide
74 the highest level of safety during interstellar travel.
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">Quantum laboratories</h3>
85 <p className="text-gray-300">
86 Our ships are equipped with state-of-the-art quantum laboratories,
87 enabling scientific research in outer space.
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">Ready for an interstellar adventure?</h2>
98 <p className="text-xl text-gray-300 mb-8 max-w-2xl mx-auto">
99 Join the community of quantum explorers and be part of
100 humanity's next great step in space exploration.
101 </p>
102 <Link
103 href="/contact"
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 Learn more
107 </Link>
108 </div>
109 </section>
110 </>
111 );
112}Preparing Assets
To make our page look attractive, we need a background image for the hero section. Download a space photo (or use your own) and place it in the public/ directory as space.jpg.
Creating the "Offer" Subpage
Now let's create the offer subpage. Create the directory src/app/offer and the file page.tsx inside:
1// src/app/offer/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">Destination</p>
30 <p className="font-medium">{destination}</p>
31 </div>
32 <div>
33 <p className="text-gray-400">Duration</p>
34 <p className="font-medium">{duration}</p>
35 </div>
36 <div className="col-span-2">
37 <p className="text-gray-400">Cost</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 OfferPage() {
47 const missions = [
48 {
49 title: "Orbital Tour",
50 description: "Perfect for beginner space explorers. Experience weightlessness and admire Earth from orbit.",
51 image: "/mission-orbit.jpg",
52 destination: "Low Earth Orbit",
53 duration: "3 days",
54 price: "250,000 credits"
55 },
56 {
57 title: "Moon Weekend",
58 description: "Spend an unforgettable weekend at the luxury complex on the Moon's surface.",
59 image: "/mission-moon.jpg",
60 destination: "Moon",
61 duration: "5 days",
62 price: "750,000 credits"
63 },
64 {
65 title: "Mars Expedition",
66 description: "Discover the red planet and be one of the first humans to set foot on it.",
67 image: "/mission-mars.jpg",
68 destination: "Mars",
69 duration: "3 months",
70 price: "5,000,000 credits"
71 },
72 {
73 title: "Journey to Proxima Centauri",
74 description: "An interstellar expedition to the nearest star beyond our Solar System.",
75 image: "/mission-proxima.jpg",
76 destination: "Proxima Centauri",
77 duration: "5 years (onboard time: 6 months)",
78 price: "25,000,000 credits"
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">Our space missions</h1>
86 <p className="text-xl text-gray-300 max-w-3xl mx-auto">
87 Choose your space journey from our offerings. From a short orbital tour
88 to interstellar expeditions - each one will forever change your perception of the universe.
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">Custom missions</h2>
100 <p className="text-gray-300 mb-6">
101 Looking for something unique? We offer exclusive, custom missions
102 tailored to your specific expectations and research needs.
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 Contact us
106 </button>
107 </div>
108 </div>
109 );
110}For this subpage, you'll also need images. You can add your own files to the public/ directory or use existing images.
Creating the "Contact" Subpage
Now let's create a contact subpage with a form. Create the directory src/app/contact and the file page.tsx inside:
1// src/app/contact/page.tsx
2import ContactForm from './ContactForm';
3
4export default function ContactPage() {
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">Get in touch</h1>
9 <p className="text-xl text-gray-300 max-w-3xl mx-auto">
10 Have questions about our space missions or want to join the crew?
11 Use the form below or visit our office.
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">Send a message</h2>
18 <ContactForm />
19 </div>
20
21 <div>
22 <h2 className="text-3xl font-semibold mb-6">Contact details</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">Address</h3>
34 <address className="not-italic text-gray-300">
35 <p>Alpha Orbital Station</p>
36 <p>Geostationary orbit, Earth</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">contact@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">Phone</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">Working hours</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>Monday - Friday:</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>Saturday:</span>
75 <span>10:00 - 15:00</span>
76 </div>
77 <div className="flex justify-between py-2">
78 <span>Sunday:</span>
79 <span>Closed</span>
80 </div>
81 </div>
82 </div>
83 </div>
84 </div>
85 </div>
86 );
87}Now let's create the contact form component in the file src/app/contact/ContactForm.tsx:
1// src/app/contact/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 // In a real application, this would handle the form, e.g., send data to an API
29 try {
30 // Simulating server response delay
31 await new Promise(resolve => setTimeout(resolve, 1500));
32
33 // Simulating success
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 Full name <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 Subject <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="">Select a subject</option>
91 <option value="booking">Mission booking</option>
92 <option value="info">Mission information</option>
93 <option value="career">Joining the crew</option>
94 <option value="other">Other</option>
95 </select>
96 </div>
97
98 <div>
99 <label htmlFor="message" className="block text-sm font-medium mb-1">
100 Message <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' ? 'Sending...' : 'Send message'}
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 Thank you for your message! We will respond as soon as possible.
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 An error occurred while sending the message. Please try again later.
130 </div>
131 )}
132 </form>
133 );
134}Creating the 404 (Not Found) Page
Let's add a 404 page that will be displayed when a user navigates to a non-existent path. Create the file 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">Lost in space</h2>
11 <p className="text-xl text-gray-300 mb-8 max-w-xl">
12 It looks like you've wandered into deep space. Unfortunately, the page you're
13 looking for is beyond our quantum range.
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 Return to Earth
20 </Link>
21 </div>
22 );
23}Running the Application
Now we can run our application locally using the command:
1npm run devAfter starting the development server, navigate to http://localhost:3000 in your browser to see your "Hello World" page for the space company.
Building and Deploying
When you're satisfied with the application running locally, you can build and prepare it for deployment:
1npm run buildThis command will generate an optimized production version of the application in the .next/ directory. You can also test this version locally by running:
1npm run startDeploying to Vercel
Next.js is created and maintained by Vercel, so deploying on this platform is extremely simple:
- Create an account on Vercel (if you don't have one yet)
- Connect your repository to Vercel (GitHub, GitLab, or Bitbucket)
- Select your project and click "Import"
- Vercel will automatically detect that it's a Next.js application and configure the deployment
- Click "Deploy" and wait for the process to complete
After deployment, Vercel will provide you with a URL where your application is publicly accessible.
What's Next?
We've created a simple but elegant and functional website for a space company using Next.js 16 and App Router. Here are some suggestions for further development:
- Add an "About Us" subpage with the company's history and mission
- Implement animations using libraries like Framer Motion
- Add real form handling using Server Actions
- Integrate a CMS (e.g., Contentful, Sanity) for content management
- Add internationalization (i18n) for multiple language support
- Implement an authentication system for customers and crew members
As you expand the application, remember the principles and best practices learned in previous lessons, especially about proper separation into Server and Client Components.
Congratulations! You've just created your first Next.js 16 application using the App Router.
Code for this lesson: App.tsx
1// State Management - Metropolis Quantum
2import React, { useState, createContext, useContext } from 'react';
3
4console.log("State Management in React");
5console.log("Context API & Lifting State Up\n");
6
7// ==========================================
8// 1. Lifting State Up
9// ==========================================
10
11function TemperatureInput({ scale, temperature, onTemperatureChange }: any) {
12 return (
13 <div style={{ marginBottom: '15px' }}>
14 <label style={{ display: 'block', marginBottom: '5px', color: '#64ffda' }}>
15 Temperature in {scale === 'celsius' ? 'Celsius' : 'Fahrenheit'}:
16 </label>
17 <input
18 type="number"
19 value={temperature}
20 onChange={(e) => onTemperatureChange(e.target.value)}
21 style={{
22 width: '100%',
23 padding: '10px',
24 borderRadius: '6px',
25 border: '1px solid #64ffda',
26 background: 'rgba(255, 255, 255, 0.1)',
27 color: 'white',
28 fontSize: '1rem'
29 }}
30 />
31 </div>
32 );
33}
34
35function TemperatureCalculator() {
36 const [temperature, setTemperature] = useState('');
37 const [scale, setScale] = useState('celsius');
38
39 const handleCelsiusChange = (temp: string) => {
40 setTemperature(temp);
41 setScale('celsius');
42 };
43
44 const handleFahrenheitChange = (temp: string) => {
45 setTemperature(temp);
46 setScale('fahrenheit');
47 };
48
49 const toCelsius = (fahrenheit: number) => (fahrenheit - 32) * 5 / 9;
50 const toFahrenheit = (celsius: number) => (celsius * 9 / 5) + 32;
51
52 const celsius = scale === 'fahrenheit' ? toCelsius(parseFloat(temperature)) : parseFloat(temperature);
53 const fahrenheit = scale === 'celsius' ? toFahrenheit(parseFloat(temperature)) : parseFloat(temperature);
54
55 return (
56 <div style={{
57 background: 'rgba(255, 255, 255, 0.1)',
58 padding: '20px',
59 borderRadius: '12px',
60 border: '2px solid #7c4dff'
61 }}>
62 <h3 style={{ color: '#7c4dff' }}>Temperature Converter (Lifting State Up)</h3>
63
64 <TemperatureInput
65 scale="celsius"
66 temperature={scale === 'celsius' ? temperature : isNaN(celsius) ? '' : celsius.toFixed(1)}
67 onTemperatureChange={handleCelsiusChange}
68 />
69
70 <TemperatureInput
71 scale="fahrenheit"
72 temperature={scale === 'fahrenheit' ? temperature : isNaN(fahrenheit) ? '' : fahrenheit.toFixed(1)}
73 onTemperatureChange={handleFahrenheitChange}
74 />
75
76 <div style={{
77 marginTop: '15px',
78 padding: '15px',
79 background: celsius >= 100 ? 'rgba(244, 67, 54, 0.3)' : 'rgba(33, 150, 243, 0.3)',
80 borderRadius: '8px',
81 textAlign: 'center'
82 }}>
83 {celsius >= 100 ? 'Water is boiling!' : 'Water is not boiling'}
84 </div>
85 </div>
86 );
87}
88
89// ==========================================
90// 2. Context API
91// ==========================================
92
93interface ThemeContextType {
94 theme: 'dark' | 'light';
95 toggleTheme: () => void;
96}
97
98const ThemeContext = createContext<ThemeContextType>({
99 theme: 'dark',
100 toggleTheme: () => {}
101});
102
103function ThemeButton() {
104 const { theme, toggleTheme } = useContext(ThemeContext);
105
106 return (
107 <button
108 onClick={toggleTheme}
109 style={{
110 background: theme === 'dark'
111 ? 'linear-gradient(45deg, #7c4dff, #e91e63)'
112 : 'linear-gradient(45deg, #ff9800, #ffeb3b)',
113 color: 'white',
114 border: 'none',
115 padding: '12px 24px',
116 borderRadius: '8px',
117 cursor: 'pointer',
118 fontWeight: 'bold',
119 fontSize: '1rem'
120 }}
121 >
122 {theme === 'dark' ? 'Dark Mode' : 'Light Mode'}
123 </button>
124 );
125}
126
127function ThemedPanel() {
128 const { theme } = useContext(ThemeContext);
129
130 return (
131 <div style={{
132 background: theme === 'dark'
133 ? 'rgba(255, 255, 255, 0.1)'
134 : 'rgba(255, 152, 0, 0.2)',
135 padding: '20px',
136 borderRadius: '12px',
137 border: `2px solid ${theme === 'dark' ? '#64ffda' : '#ff9800'}`,
138 marginTop: '20px'
139 }}>
140 <h3 style={{ color: theme === 'dark' ? '#64ffda' : '#ff9800' }}>
141 Themed Panel
142 </h3>
143 <p style={{ color: theme === 'dark' ? '#b0bec5' : '#5d4037' }}>
144 This panel automatically adjusts to the current theme thanks to Context API!
145 </p>
146 <p style={{ color: theme === 'dark' ? '#b0bec5' : '#5d4037' }}>
147 Current theme: <strong>{theme === 'dark' ? 'Dark' : 'Light'}</strong>
148 </p>
149 </div>
150 );
151}
152
153// ==========================================
154// 3. User Context Example
155// ==========================================
156
157interface User {
158 name: string;
159 role: string;
160 level: number;
161}
162
163interface UserContextType {
164 user: User | null;
165 login: (user: User) => void;
166 logout: () => void;
167}
168
169const UserContext = createContext<UserContextType>({
170 user: null,
171 login: () => {},
172 logout: () => {}
173});
174
175function UserProfile() {
176 const { user, logout } = useContext(UserContext);
177
178 if (!user) return null;
179
180 return (
181 <div style={{
182 background: 'rgba(76, 175, 80, 0.2)',
183 padding: '15px',
184 borderRadius: '10px',
185 border: '2px solid #4caf50',
186 display: 'flex',
187 justifyContent: 'space-between',
188 alignItems: 'center'
189 }}>
190 <div>
191 <h4 style={{ color: '#4caf50', margin: '0 0 5px 0' }}>{user.name}</h4>
192 <p style={{ margin: '5px 0', color: '#b0bec5' }}>Role: {user.role}</p>
193 <p style={{ margin: '5px 0', color: '#ff9800' }}>Level: {user.level}</p>
194 </div>
195 <button
196 onClick={logout}
197 style={{
198 background: 'linear-gradient(45deg, #f44336, #e91e63)',
199 color: 'white',
200 border: 'none',
201 padding: '8px 16px',
202 borderRadius: '6px',
203 cursor: 'pointer',
204 fontWeight: 'bold'
205 }}
206 >
207 Logout
208 </button>
209 </div>
210 );
211}
212
213function LoginForm() {
214 const { login } = useContext(UserContext);
215 const [name, setName] = useState('');
216
217 const handleLogin = () => {
218 if (name.trim()) {
219 login({
220 name: name,
221 role: 'Quantum Engineer',
222 level: Math.floor(Math.random() * 50) + 1
223 });
224 }
225 };
226
227 return (
228 <div style={{
229 background: 'rgba(33, 150, 243, 0.2)',
230 padding: '20px',
231 borderRadius: '12px',
232 border: '2px solid #2196f3'
233 }}>
234 <h3 style={{ color: '#2196f3' }}>Login to Quantum System</h3>
235 <input
236 type="text"
237 placeholder="Enter your name..."
238 value={name}
239 onChange={(e) => setName(e.target.value)}
240 style={{
241 width: '100%',
242 padding: '12px',
243 marginTop: '15px',
244 borderRadius: '6px',
245 border: '1px solid #2196f3',
246 background: 'rgba(255, 255, 255, 0.1)',
247 color: 'white',
248 fontSize: '1rem'
249 }}
250 />
251 <button
252 onClick={handleLogin}
253 style={{
254 marginTop: '15px',
255 width: '100%',
256 background: 'linear-gradient(45deg, #2196f3, #21cbf3)',
257 color: 'white',
258 border: 'none',
259 padding: '12px',
260 borderRadius: '6px',
261 cursor: 'pointer',
262 fontWeight: 'bold',
263 fontSize: '1rem'
264 }}
265 >
266 Login
267 </button>
268 </div>
269 );
270}
271
272// ==========================================
273// Main App with Providers
274// ==========================================
275function App() {
276 const [theme, setTheme] = useState<'dark' | 'light'>('dark');
277 const [user, setUser] = useState<User | null>(null);
278
279 const toggleTheme = () => {
280 setTheme(prev => prev === 'dark' ? 'light' : 'dark');
281 console.log(`Theme changed to: ${theme === 'dark' ? 'light' : 'dark'}`);
282 };
283
284 const login = (newUser: User) => {
285 setUser(newUser);
286 console.log(`User logged in: ${newUser.name}`);
287 };
288
289 const logout = () => {
290 setUser(null);
291 console.log("User logged out");
292 };
293
294 return (
295 <ThemeContext.Provider value={{ theme, toggleTheme }}>
296 <UserContext.Provider value={{ user, login, logout }}>
297 <div style={{
298 padding: '20px',
299 background: theme === 'dark'
300 ? 'linear-gradient(135deg, #0f0f23 0%, #1a1a2e 100%)'
301 : 'linear-gradient(135deg, #fafafa 0%, #e0e0e0 100%)',
302 minHeight: '100vh',
303 color: theme === 'dark' ? 'white' : '#333',
304 transition: 'all 0.3s ease'
305 }}>
306 <h1 style={{
307 textAlign: 'center',
308 color: theme === 'dark' ? '#64ffda' : '#ff9800'
309 }}>
310 State Management Demo
311 </h1>
312
313 <div style={{ display: 'flex', justifyContent: 'center', marginTop: '20px' }}>
314 <ThemeButton />
315 </div>
316
317 <div style={{ marginTop: '30px' }}>
318 <TemperatureCalculator />
319 </div>
320
321 <ThemedPanel />
322
323 <div style={{ marginTop: '30px' }}>
324 {user ? <UserProfile /> : <LoginForm />}
325 </div>
326
327 <div style={{
328 marginTop: '30px',
329 background: theme === 'dark'
330 ? 'rgba(100, 255, 218, 0.1)'
331 : 'rgba(255, 152, 0, 0.2)',
332 padding: '20px',
333 borderRadius: '10px',
334 borderLeft: `4px solid ${theme === 'dark' ? '#64ffda' : '#ff9800'}`
335 }}>
336 <h3>State Management Patterns:</h3>
337 <ul style={{ lineHeight: '1.8' }}>
338 <li><strong>Lifting State Up</strong>: Move state to common parent</li>
339 <li><strong>Context API</strong>: Global state without prop drilling</li>
340 <li><strong>Custom Hooks</strong>: Reusable state logic</li>
341 <li><strong>Redux/Zustand</strong>: For larger applications</li>
342 </ul>
343 </div>
344 </div>
345 </UserContext.Provider>
346 </ThemeContext.Provider>
347 );
348}
349
350export default App;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which of the following is a correct method for rendering a list in React?
2. The 'key' attribute in React is needed when:
3. What does the
npm run build(next build) command do in a Next.js project?
Hands-on tasks in the game
- Code editor
Create a LoginStatus component that takes an isLoggedIn prop (true or false) and displays "Logged in" when isLoggedIn is true, or "Not logged in" when it is false. Use conditional rendering (the ? : operator or if).
- Code editor
In the App component, create a <button> that shows an alert with the message 'Button was clicked!' when it is clicked (onClick).
- Click in order
Arrange the syntax for declaring a Client Component in Next.js
- Code editor
Create a DroneControlPanel component (export default) with an <h1> heading "Drone panel" and a <nav> element with three <Link> links from next/link: "Map" (/map), "Status" (/status) and "Settings" (/settings).
- Vertical ordering
Arrange the path to a Route Handler file in Next.js, followed by the function the file exports