Cours Next.js · Module 1: Configuration de Next.js

Première application « Hello World » - étape par étape - une page simple pour une entreprise spatiale

16 min de lecture
Dans cette leçon16

À Quantum Metropolis, chaque ingénieur se souvient de son premier projet - le moment où il a lancé pour la première fois son propre système quantique. Dans le monde des développeurs Next.js, ce premier pas, c'est la création d'une application « Hello World ». Dans cette leçon, nous te guiderons à travers le processus de création d'un site simple pour l'entreprise spatiale fictive « Quantum Voyages », en utilisant les dernières fonctionnalités de Next.js 16 et de l'App Router.

Mise en place du projet

D'abord, nous devons créer un nouveau projet Next.js. Ouvre ton terminal et exécute la commande suivante :

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

Pendant le processus d'initialisation, réponds aux questions de configuration :

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? Yes

Une fois le processus d'installation terminé, place-toi dans le répertoire du projet :

1cd quantum-voyages

Structure du projet

Après avoir créé le projet, tu verras une structure de répertoires semblable à celle-ci :

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.json

Créer la page d'accueil

Commençons par modifier la page d'accueil. Ouvre le fichier src/app/page.tsx et remplace son contenu par le code suivant :

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}

Styles globaux et layout

Ensuite, modifions le layout global de notre application. Ouvre le fichier src/app/layout.tsx et mets-le à jour :

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}

Créer le composant Header

Maintenant, créons un en-tête partagé qui sera affiché sur toutes les pages. D'abord, nous créons un répertoire pour les composants partagés :

1mkdir -p src/components/ui

Crée ensuite le fichier 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}

Remarque que nous avons utilisé la directive "use client" en haut du fichier, car ce composant utilise des hooks React et gère des événements utilisateur.

Créons maintenant un pied de page qui sera également présent sur toutes les pages. Crée le fichier 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">&copy; {currentYear} Quantum Voyages. All rights reserved.</p>
40        </div>
41      </div>
42    </footer>
43  );
44}

Mettre à jour le layout principal

Mettons maintenant à jour le layout principal de l'application pour y inclure notre header et notre 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}

Mettre à jour la page d'accueil

Adaptons maintenant la page d'accueil pour qu'elle s'intègre mieux à notre nouveau 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}

Préparer les ressources

Pour que notre page soit attrayante, nous avons besoin d'une image de fond pour la section hero. Télécharge une photo de l'espace (ou utilise la tienne) et place-la dans le répertoire public/ sous le nom space.jpg.

Créer la sous-page « Offer »

Créons maintenant la sous-page d'offre. Crée le répertoire src/app/offer et le fichier page.tsx à l'intérieur :

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}

Pour cette sous-page, tu auras également besoin d'images. Tu peux ajouter tes propres fichiers dans le répertoire public/ ou utiliser des images existantes.

Créer la sous-page « Contact »

Créons maintenant une sous-page de contact avec un formulaire. Crée le répertoire src/app/contact et le fichier page.tsx à l'intérieur :

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}

Créons maintenant le composant de formulaire de contact dans le fichier 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}

Créer la page 404 (Not Found)

Ajoutons une page 404 qui s'affichera lorsqu'un utilisateur accède à un chemin inexistant. Crée le fichier 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}

Lancer l'application

Nous pouvons maintenant lancer notre application localement avec la commande :

1npm run dev

Après avoir démarré le serveur de développement, ouvre http://localhost:3000 dans ton navigateur pour voir ta page « Hello World » de l'entreprise spatiale.

Construire et déployer

Quand tu es satisfait de l'application en local, tu peux la construire et la préparer pour le déploiement :

1npm run build

Cette commande génère une version de production optimisée de l'application dans le répertoire .next/. Tu peux aussi tester cette version localement en exécutant :

1npm run start

Déployer sur Vercel

Next.js est créé et maintenu par Vercel, le déploiement sur cette plateforme est donc extrêmement simple :

  1. Crée un compte sur Vercel (si tu n'en as pas encore)
  2. Connecte ton dépôt à Vercel (GitHub, GitLab ou Bitbucket)
  3. Sélectionne ton projet et clique sur « Import »
  4. Vercel détectera automatiquement qu'il s'agit d'une application Next.js et configurera le déploiement
  5. Clique sur « Deploy » et attends la fin du processus

Après le déploiement, Vercel te fournira une URL où ton application est accessible publiquement.

Et ensuite ?

Nous avons créé un site simple mais élégant et fonctionnel pour une entreprise spatiale avec Next.js 16 et l'App Router. Voici quelques suggestions pour aller plus loin :

  1. Ajouter une sous-page « À propos » avec l'histoire et la mission de l'entreprise
  2. Implémenter des animations à l'aide de bibliothèques comme Framer Motion
  3. Ajouter une vraie gestion de formulaire à l'aide des Server Actions
  4. Intégrer un CMS (par ex. Contentful, Sanity) pour la gestion du contenu
  5. Ajouter l'internationalisation (i18n) pour la prise en charge de plusieurs langues
  6. Implémenter un système d'authentification pour les clients et les membres d'équipage

À mesure que tu développes l'application, souviens-toi des principes et des bonnes pratiques appris dans les leçons précédentes, en particulier concernant la bonne séparation entre Server et Client Components.

Félicitations ! Tu viens de créer ta première application Next.js 16 en utilisant l'App Router.

Code de cette leçon : App.tsx
1// Gestion de l'état - Metropolis Quantum
2import React, { useState, createContext, useContext } from 'react';
3
4console.log("Gestion de l'état dans React");
5console.log("Context API et remontée de l'état\n");
6
7// ==========================================
8// 1. Faire remonter l'état (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        Température en {scale === 'celsius' ? 'degrés Celsius' : 'degrés 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' }}>Convertisseur de température (remontée de l'état)</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 ? 'L\'eau bout !' : 'L\'eau ne bout pas'}
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' ? 'Mode sombre' : 'Mode clair'}
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        Panneau thématique
142      </h3>
143      <p style={{ color: theme === 'dark' ? '#b0bec5' : '#5d4037' }}>
144        Ce panneau s'adapte automatiquement au thème actuel grâce à la Context API !
145      </p>
146      <p style={{ color: theme === 'dark' ? '#b0bec5' : '#5d4037' }}>
147        Thème actuel : <strong>{theme === 'dark' ? 'Sombre' : 'Clair'}</strong>
148      </p>
149    </div>
150  );
151}
152
153// ==========================================
154// 3. Exemple de contexte utilisateur
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' }}>Rôle : {user.role}</p>
193        <p style={{ margin: '5px 0', color: '#ff9800' }}>Niveau : {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        Se déconnecter
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: 'Ingénieur quantique',
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' }}>Connexion au système quantique</h3>
235      <input
236        type="text"
237        placeholder="Saisis ton nom..."
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        Se connecter
267      </button>
268    </div>
269  );
270}
271
272// ==========================================
273// App principale avec les 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(`Thème changé en : ${theme === 'dark' ? 'clair' : 'sombre'}`);
282  };
283
284  const login = (newUser: User) => {
285    setUser(newUser);
286    console.log(`Utilisateur connecté : ${newUser.name}`);
287  };
288
289  const logout = () => {
290    setUser(null);
291    console.log("Utilisateur déconnecté");
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            Démo de gestion de l'état
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>Modèles de gestion de l'état :</h3>
337            <ul style={{ lineHeight: '1.8' }}>
338              <li><strong>Remonter l'état</strong> : déplace l'état vers le parent commun</li>
339              <li><strong>Context API</strong> : état global sans prop drilling</li>
340              <li><strong>Hooks personnalisés</strong> : logique d'état réutilisable</li>
341              <li><strong>Redux/Zustand</strong> : pour les applications plus grandes</li>
342            </ul>
343          </div>
344        </div>
345      </UserContext.Provider>
346    </ThemeContext.Provider>
347  );
348}
349
350export default App;

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Laquelle des méthodes suivantes est correcte pour rendre une liste en React ?

  2. 2. L'attribut 'key' dans React est nécessaire lorsque :

  3. 3. Que fait la commande npm run build (next build) dans un projet Next.js ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée un composant LoginStatus qui reçoit une prop isLoggedIn (true ou false) et affiche « Connecté » quand isLoggedIn vaut true, ou « Non connecté » quand elle vaut false. Utilise le rendu conditionnel (l'opérateur ? : ou if).

  • Éditeur de code

    Dans le composant App, crée un bouton <button> qui affiche une alerte avec le message 'Le bouton a été cliqué !' au clic (onClick).

  • Clique dans l'ordre

    Ordonne la syntaxe pour déclarer un Client Component dans Next.js

  • Éditeur de code

    Crée un composant DroneControlPanel (export default) avec un titre <h1> « Panneau du drone » et un élément <nav> contenant trois liens <Link> de next/link : « Carte » (/map), « Statut » (/status) et « Paramètres » (/settings).

  • Tri vertical

    Ordonne le chemin vers un fichier Route Handler dans Next.js, puis la fonction que ce fichier exporte

Articles utiles