Skip to content

Kapitel 12: Praxisprojekt - E-Commerce (Fortgeschritten)

📖 Lernziele

In diesem Kapitel lernen Sie:

  • ✅ Eine E-Commerce-Startseite mit Next.js 14 erstellen
  • ✅ Layout aufteilen (Header, Sidebar, Inhalt)
  • ✅ Hauptfunktionen implementieren (Slider, Produktliste, Warenkorb)
  • ✅ State Management mit Zustand (Warenkorb)
  • ✅ Datenabfrage (SSR für Produkte, Client für Warenkorb)
  • ✅ Performance-Optimierung (Bild-Lazy-Loading, Code-Splitting)

12.1 Projektarchitektur

🎯 Projektziel

Eine E-Commerce-Startseite mit folgenden Funktionen:

  • ✅ Startseite mit Slider und Produktliste
  • ✅ Produktdetailseite
  • ✅ Warenkorb (Hinzufügen, Entfernen, Aktualisieren)
  • ✅ Zustandsverwaltung mit Zustand
  • ✅ Responsive Design

📂 Verzeichnisstruktur

app/
├── layout.js                    # Globales Layout
├── page.js                      # Startseite (/)
├── produkte/
│   ├── page.js                # Produktliste (/produkte)
│   └── [id]/
│       └── page.js            # Produktdetail (/produkte/[id])
├── warenkorb/
│   └── page.js                # Warenkorb-Seite (/warenkorb)
└── components/                 # Wiederverwendbare Komponenten
    ├── Header.js
    ├── Slider.js
    ├── ProduktKarte.js
    └── WarenkorbIcon.js

12.2 Layout & Komponenten aufbauen

🎨 Globales Layout (app/layout.js)

jsx
import './globals.css';
import Header from '@/components/Header';
import { WarenkorbProvider } from '@/context/WarenkorbContext';

export const metadata = {
  title: 'Mein E-Commerce Shop | Next.js 14',
  description: 'Eine moderne E-Commerce-Webseite mit Next.js 14',
};

export default function RootLayout({ children }) {
  return (
    <html lang="de">
      <body>
        <WarenkorbProvider>
          <Header />
          <main className="container mx-auto px-4 py-8">
            {children}
          </main>
          <footer className="bg-gray-800 text-white py-8">
            <div className="container mx-auto px-4 text-center">
              <p>&copy; 2024 Mein E-Commerce Shop. Alle Rechte vorbehalten.</p>
            </div>
          </footer>
        </WarenkorbProvider>
      </body>
    </html>
  );
}

🏠 Header-Komponente (components/Header.js)

jsx
'use client';

import Link from 'next/link';
import WarenkorbIcon from './WarenkorbIcon';

export default function Header() {
  return (
    <header className="bg-white shadow">
      <nav className="container mx-auto px-4 py-6">
        <div className="flex flex-col md:flex-row justify-between items-center">
          <Link href="/" className="text-3xl font-bold text-gray-800 mb-4 md:mb-0">
            Mein Shop
          </Link>
          <div className="flex items-center space-x-8">
            <Link href="/produkte" className="text-gray-600 hover:text-gray-900">
              Produkte
            </Link>
            <Link href="/warenkorb" className="relative">
              <WarenkorbIcon />
            </Link>
          </div>
        </div>
      </nav>
    </header>
  );
}

12.3 Hauptfunktionen implementieren

🖼️ Slider-Komponente (components/Slider.js)

jsx
'use client';

import { useState, useEffect } from 'react';

const slides = [
  { id: 1, image: '/slider1.jpg', title: 'Sonderangebot', subtitle: 'Bis zu 50% Rabatt' },
  { id: 2, image: '/slider2.jpg', title: 'Neue Kollektion', subtitle: 'Entdecken Sie die neuesten Trends' },
  { id: 3, image: '/slider3.jpg', title: 'Kostenloser Versand', subtitle: 'Für Bestellungen über 50€' },
];

export default function Slider() {
  const [currentSlide, setCurrentSlide] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setCurrentSlide((prev) => (prev + 1) % slides.length);
    }, 5000);
    return () => clearInterval(timer);
  }, []);

  return (
    <div className="relative h-96 overflow-hidden rounded-lg mb-12">
      {slides.map((slide, index) => (
        <div
          key={slide.id}
          className={`absolute inset-0 transition-opacity duration-1000 ${
            index === currentSlide ? 'opacity-100' : 'opacity-0'
          }`}
        >
          <img src={slide.image} alt={slide.title} className="w-full h-full object-cover" />
          <div className="absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center">
            <div className="text-center text-white">
              <h2 className="text-5xl font-bold mb-4">{slide.title}</h2>
              <p className="text-2xl">{slide.subtitle}</p>
            </div>
          </div>
        </div>
      ))}
      <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex space-x-2">
        {slides.map((_, index) => (
          <button
            key={index}
            onClick={() => setCurrentSlide(index)}
            className={`w-3 h-3 rounded-full ${index === currentSlide ? 'bg-white' : 'bg-white bg-opacity-50'}`}
          />
        ))}
      </div>
    </div>
  );
}

🛍️ Produktkarte (components/ProduktKarte.js)

jsx
import Image from 'next/image';
import { useWarenkorb } from '@/context/WarenkorbContext';

export default function ProduktKarte({ produkt }) {
  const { addToWarenkorb } = useWarenkorb();

  return (
    <div className="border rounded-lg overflow-hidden shadow hover:shadow-lg transition">
      <img src={produkt.bild} alt={produkt.name} className="w-full h-48 object-cover" />
      <div className="p-6">
        <h3 className="text-xl font-semibold mb-2">{produkt.name}</h3>
        <p className="text-gray-600 mb-4">{produkt.beschreibung}</p>
        <div className="flex justify-between items-center">
          <span className="text-2xl font-bold text-blue-600">{produkt.preis.toFixed(2)} €</span>
          <button
            onClick={() => addToWarenkorb(produkt)}
            className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
          >
            In den Warenkorb
          </button>
        </div>
      </div>
    </div>
  );
}

12.4 State Management mit Zustand

🏪 Warenkorb-Kontext (context/WarenkorbContext.js)

javascript
'use client';

import { createContext, useContext, useState } from 'react';

const WarenkorbContext = createContext();

export function WarenkorbProvider({ children }) {
  const [warenkorb, setWarenkorb] = useState([]);

  const addToWarenkorb = (produkt) => {
    setWarenkorb((prev) => {
      const existiert = prev.find((item) => item.id === produkt.id);
      if (existiert) {
        return prev.map((item) =>
          item.id === produkt.id ? { ...item, menge: item.menge + 1 } : item
        );
      }
      return [...prev, { ...produkt, menge: 1 }];
    });
  };

  const removeFromWarenkorb = (produktId) => {
    setWarenkorb((prev) => prev.filter((item) => item.id !== produktId));
  };

  const updateMenge = (produktId, menge) => {
    if (menge < 1) return;
    setWarenkorb((prev) =>
      prev.map((item) => (item.id === produktId ? { ...item, menge } : item))
    );
  };

  const getGesamtpreis = () => {
    return warenkorb.reduce((sum, item) => sum + item.preis * item.menge, 0);
  };

  return (
    <WarenkorbContext.Provider
      value={{
        warenkorb,
        addToWarenkorb,
        removeFromWarenkorb,
        updateMenge,
        getGesamtpreis,
      }}
    >
      {children}
    </WarenkorbContext.Provider>
  );
}

export function useWarenkorb() {
  const context = useContext(WarenkorbContext);
  if (!context) {
    throw new Error('useWarenkorb muss innerhalb von WarenkorbProvider verwendet werden');
  }
  return context;
}

12.5 Datenabfrage

📦 Produktdaten (lib/produkte.js)

javascript
export const produkte = [
  {
    id: 1,
    name: 'Smartphone XYZ',
    beschreibung: 'Hochleistungs-Smartphone mit toller Kamera',
    preis: 599.99,
    bild: '/produkt1.jpg',
    kategorie: 'Elektronik',
  },
  {
    id: 2,
    name: 'Laptop ABC',
    beschreibung: 'Leichter und leistungsstarker Laptop',
    preis: 1299.99,
    bild: '/produkt2.jpg',
    kategorie: 'Elektronik',
  },
  // Weitere Produkte...
];

export function getProdukte() {
  return produkte;
}

export function getProduktById(id) {
  return produkte.find((p) => p.id === parseInt(id));
}

🏠 Startseite (app/page.js) - Server Component

jsx
import Slider from '@/components/Slider';
import ProduktKarte from '@/components/ProduktKarte';
import { getProdukte } from '@/lib/produkte';

export default function HomePage() {
  const produkte = getProdukte();

  return (
    <div>
      <Slider />
      <section className="mb-12">
        <h2 className="text-3xl font-bold mb-8">Beliebte Produkte</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
          {produkte.slice(0, 8).map((produkt) => (
            <ProduktKarte key={produkt.id} produkt={produkt} />
          ))}
        </div>
      </section>
    </div>
  );
}

12.6 Performance-Optimierung

⚡ Bild-Lazy-Loading

jsx
import Image from 'next/image';

<Image
  src={produkt.bild}
  alt={produkt.name}
  width={500}
  height={300}
  loading="lazy"  // Lazy Loading
  className="w-full h-48 object-cover"
/>

📦 Code-Splitting (Dynamische Importe)

jsx
import dynamic from 'next/dynamic';

const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'), {
  loading: () => <p>Lädt...</p>,
  ssr: false, // Nur Client-seitig
});

📝 Zusammenfassung

In diesem Kapitel haben Sie gelernt:

KonzeptErklärung
ProjektarchitekturStartseite, Produktliste, Warenkorb
KomponentenHeader, Slider, Produktkarte
State ManagementZustand für Warenkorb
DatenabfrageServer Components für Produkte
PerformanceLazy Loading, Code-Splitting

✅ Nächste Schritte

  1. Übung: Fügen Sie eine Kategorieseite hinzu
  2. Übung: Implementieren Sie eine Suche
  3. Weiter geht's: Kapitel 13 - Erweiterte Konfiguration & Optimierung

🎯 Selbsttest

Frage 1: Welches Werkzeug wird für State Management in diesem Projekt verwendet?

Antwort anzeigen Zustand (über Context API)

Frage 2: Was ist der Vorteil von Server Components bei der Datenabfrage?

Antwort anzeigen Bessere SEO, schnellere Ladezeiten, da Daten auf dem Server abgerufen werden.

Frage 3: Wie optimiert man Bilder in Next.js?

Antwort anzeigen Verwenden Sie die `next/image` Komponente mit `loading="lazy"` für Lazy Loading.

🚀 Weiter zu Kapitel 13: Erweiterte Konfiguration & Optimierung

Frei für alle Anfänger