Projekt: Online-Shop
Baue einen vollständigen Online-Shop mit React. Produktliste, Warenkorb, Filter und Checkout - alles mit modernem State Management.
Projekt: Online-Shop
In diesem Projekt baust du einen vollstaendigen Online-Shop mit React. Du lernst, wie du Produktlisten, einen Warenkorb, Filter und einen Checkout-Prozess implementierst – mit sauberem State Management.
Was wir bauen
- Produktliste mit Bildern, Preisen und Kategorien
- Warenkorb mit Hinzufuegen, Entfernen und Mengensteuerung
- Produktfilter nach Kategorie und Preis
- Produktsuche mit Live-Ergebnissen
- Checkout-Seite mit Bestelluebersicht
- Responsive Layout mit React Router
Projekt aufsetzen
npm create vite@latest react-shop -- --template react
cd react-shop
npm install react-router-dom
npm run dev
Projektstruktur
src/
├── components/
│ ├── Layout.jsx
│ ├── ProductCard.jsx
│ ├── CartItem.jsx
│ ├── SearchBar.jsx
│ └── CategoryFilter.jsx
├── context/
│ └── CartContext.jsx
├── data/
│ └── products.js
├── pages/
│ ├── Home.jsx
│ ├── ProductDetail.jsx
│ ├── Cart.jsx
│ └── Checkout.jsx
├── App.jsx
└── main.jsx
Schritt 1: Produktdaten
// src/data/products.js
export const products = [
{
id: 1,
name: 'Wireless Kopfhoerer',
price: 79.99,
category: 'elektronik',
image: 'https://picsum.photos/seed/headphones/400/400',
description: 'Kabellose Bluetooth-Kopfhoerer mit aktiver Geraeuschdaempfung und 30 Stunden Akkulaufzeit.',
rating: 4.5,
inStock: true
},
{
id: 2,
name: 'Mechanische Tastatur',
price: 129.99,
category: 'elektronik',
image: 'https://picsum.photos/seed/keyboard/400/400',
description: 'RGB mechanische Gaming-Tastatur mit Cherry MX Switches und Handballenauflage.',
rating: 4.8,
inStock: true
},
{
id: 3,
name: 'React-Handbuch',
price: 34.99,
category: 'buecher',
image: 'https://picsum.photos/seed/reactbook/400/400',
description: 'Umfassendes Handbuch fuer React 19 mit praktischen Projekten und Best Practices.',
rating: 4.7,
inStock: true
},
{
id: 4,
name: 'Laptop-Rucksack',
price: 49.99,
category: 'zubehoer',
image: 'https://picsum.photos/seed/backpack/400/400',
description: 'Wasserabweisender Rucksack mit gepolstertem Laptopfach fuer bis zu 15.6 Zoll.',
rating: 4.3,
inStock: true
},
{
id: 5,
name: 'USB-C Hub',
price: 39.99,
category: 'elektronik',
image: 'https://picsum.photos/seed/usbhub/400/400',
description: '7-in-1 USB-C Hub mit HDMI, USB 3.0, SD-Kartenleser und Ethernet.',
rating: 4.4,
inStock: false
},
{
id: 6,
name: 'JavaScript Guide',
price: 29.99,
category: 'buecher',
image: 'https://picsum.photos/seed/jsbook/400/400',
description: 'Von Grundlagen bis zu fortgeschrittenen Konzepten – alles ueber modernes JavaScript.',
rating: 4.6,
inStock: true
},
{
id: 7,
name: 'Webcam HD',
price: 59.99,
category: 'elektronik',
image: 'https://picsum.photos/seed/webcam/400/400',
description: '1080p Webcam mit Mikrofon und Autofokus. Perfekt fuer Videocalls.',
rating: 4.2,
inStock: true
},
{
id: 8,
name: 'Mousepad XL',
price: 19.99,
category: 'zubehoer',
image: 'https://picsum.photos/seed/mousepad/400/400',
description: 'Grosses Gaming-Mousepad mit rutschfester Unterseite. 80x30cm.',
rating: 4.5,
inStock: true
}
];
Schritt 2: Cart-Context mit useReducer
// src/context/CartContext.jsx
import { createContext, useContext, useReducer } from 'react';
const CartContext = createContext();
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_TO_CART': {
const existing = state.find(item => item.id === action.payload.id);
if (existing) {
return state.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
);
}
return [...state, { ...action.payload, quantity: 1 }];
}
case 'REMOVE_FROM_CART':
return state.filter(item => item.id !== action.payload);
case 'UPDATE_QUANTITY':
if (action.payload.quantity <= 0) {
return state.filter(item => item.id !== action.payload.id);
}
return state.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
);
case 'CLEAR_CART':
return [];
default:
return state;
}
}
export function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, []);
const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
function addToCart(product) {
dispatch({ type: 'ADD_TO_CART', payload: product });
}
function removeFromCart(id) {
dispatch({ type: 'REMOVE_FROM_CART', payload: id });
}
function updateQuantity(id, quantity) {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
}
function clearCart() {
dispatch({ type: 'CLEAR_CART' });
}
return (
<CartContext.Provider value={{
cart, totalItems, totalPrice,
addToCart, removeFromCart, updateQuantity, clearCart
}}>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) throw new Error('useCart muss in CartProvider verwendet werden');
return context;
}
Schritt 3: Layout mit Navigation
// src/components/Layout.jsx
import { Outlet, NavLink, Link } from 'react-router-dom';
import { useCart } from '../context/CartContext';
function Layout() {
const { totalItems } = useCart();
const linkStyle = ({ isActive }) => ({
textDecoration: 'none',
color: isActive ? '#2563eb' : '#475569',
fontWeight: isActive ? 600 : 400
});
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<header style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '1rem 2rem',
borderBottom: '1px solid #e2e8f0',
backgroundColor: 'white',
position: 'sticky',
top: 0,
zIndex: 100
}}>
<Link to="/" style={{
fontSize: '1.5rem',
fontWeight: 800,
textDecoration: 'none',
color: '#2563eb'
}}>
ReactShop
</Link>
<nav style={{ display: 'flex', gap: '1.5rem', alignItems: 'center' }}>
<NavLink to="/" end style={linkStyle}>Produkte</NavLink>
<NavLink to="/cart" style={linkStyle}>
Warenkorb
{totalItems > 0 && (
<span style={{
marginLeft: '0.25rem',
padding: '2px 8px',
backgroundColor: '#2563eb',
color: 'white',
borderRadius: '12px',
fontSize: '0.75rem'
}}>
{totalItems}
</span>
)}
</NavLink>
</nav>
</header>
<main style={{ flex: 1, padding: '2rem', maxWidth: '1200px', margin: '0 auto', width: '100%' }}>
<Outlet />
</main>
<footer style={{
padding: '1.5rem',
textAlign: 'center',
borderTop: '1px solid #e2e8f0',
color: '#94a3b8'
}}>
2026 ReactShop – Ein React-Lernprojekt
</footer>
</div>
);
}
export default Layout;
Schritt 4: Produktkarte
// src/components/ProductCard.jsx
import { Link } from 'react-router-dom';
import { useCart } from '../context/CartContext';
function ProductCard({ product }) {
const { addToCart } = useCart();
return (
<div style={{
border: '1px solid #e2e8f0',
borderRadius: '12px',
overflow: 'hidden',
transition: 'transform 0.2s, box-shadow 0.2s',
backgroundColor: 'white'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.08)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = '';
e.currentTarget.style.boxShadow = '';
}}>
<Link to={`/product/${product.id}`}>
<img src={product.image} alt={product.name}
style={{ width: '100%', height: '200px', objectFit: 'cover' }} />
</Link>
<div style={{ padding: '1rem' }}>
<span style={{
fontSize: '0.75rem',
textTransform: 'uppercase',
color: '#64748b',
letterSpacing: '0.05em'
}}>
{product.category}
</span>
<Link to={`/product/${product.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<h3 style={{ margin: '0.25rem 0 0.5rem', fontSize: '1.1rem' }}>
{product.name}
</h3>
</Link>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span style={{ fontSize: '1.25rem', fontWeight: 700, color: '#2563eb' }}>
{product.price.toFixed(2)} €
</span>
<button
onClick={() => addToCart(product)}
disabled={!product.inStock}
style={{
padding: '8px 16px',
backgroundColor: product.inStock ? '#2563eb' : '#94a3b8',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: product.inStock ? 'pointer' : 'not-allowed',
fontWeight: 600,
fontSize: '0.85rem'
}}
>
{product.inStock ? 'In den Warenkorb' : 'Ausverkauft'}
</button>
</div>
</div>
</div>
);
}
export default ProductCard;
Schritt 5: Startseite mit Filter und Suche
// src/pages/Home.jsx
import { useState, useMemo } from 'react';
import { products } from '../data/products';
import ProductCard from '../components/ProductCard';
function Home() {
const [search, setSearch] = useState('');
const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('name');
const categories = ['all', ...new Set(products.map(p => p.category))];
const filteredProducts = useMemo(() => {
return products
.filter(p => {
const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase());
const matchesCategory = category === 'all' || p.category === category;
return matchesSearch && matchesCategory;
})
.sort((a, b) => {
if (sortBy === 'price-asc') return a.price - b.price;
if (sortBy === 'price-desc') return b.price - a.price;
if (sortBy === 'rating') return b.rating - a.rating;
return a.name.localeCompare(b.name);
});
}, [search, category, sortBy]);
return (
<div>
<h1 style={{ marginBottom: '0.5rem' }}>Unsere Produkte</h1>
<p style={{ color: '#64748b', marginBottom: '1.5rem' }}>
{filteredProducts.length} Produkt{filteredProducts.length !== 1 ? 'e' : ''} gefunden
</p>
{/* Filter-Leiste */}
<div style={{
display: 'flex',
gap: '1rem',
marginBottom: '2rem',
flexWrap: 'wrap',
alignItems: 'center'
}}>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Produkte suchen..."
style={{
padding: '8px 12px',
border: '1px solid #e2e8f0',
borderRadius: '6px',
flex: '1',
minWidth: '200px'
}}
/>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
{categories.map(cat => (
<button
key={cat}
onClick={() => setCategory(cat)}
style={{
padding: '6px 14px',
border: category === cat ? 'none' : '1px solid #e2e8f0',
borderRadius: '20px',
backgroundColor: category === cat ? '#2563eb' : 'white',
color: category === cat ? 'white' : '#475569',
cursor: 'pointer',
textTransform: 'capitalize',
fontSize: '0.85rem'
}}
>
{cat === 'all' ? 'Alle' : cat}
</button>
))}
</div>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
style={{
padding: '8px 12px',
border: '1px solid #e2e8f0',
borderRadius: '6px'
}}
>
<option value="name">Name A-Z</option>
<option value="price-asc">Preis aufsteigend</option>
<option value="price-desc">Preis absteigend</option>
<option value="rating">Beste Bewertung</option>
</select>
</div>
{/* Produkt-Grid */}
{filteredProducts.length > 0 ? (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))',
gap: '1.5rem'
}}>
{filteredProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
) : (
<div style={{ textAlign: 'center', padding: '3rem', color: '#64748b' }}>
<p>Keine Produkte gefunden.</p>
<button onClick={() => { setSearch(''); setCategory('all'); }}
style={{ marginTop: '1rem', padding: '8px 16px', cursor: 'pointer' }}>
Filter zuruecksetzen
</button>
</div>
)}
</div>
);
}
export default Home;
Schritt 6: Warenkorb-Seite
// src/pages/Cart.jsx
import { Link } from 'react-router-dom';
import { useCart } from '../context/CartContext';
function Cart() {
const { cart, totalPrice, updateQuantity, removeFromCart, clearCart } = useCart();
if (cart.length === 0) {
return (
<div style={{ textAlign: 'center', padding: '4rem 0' }}>
<h2>Dein Warenkorb ist leer</h2>
<p style={{ color: '#64748b', marginBottom: '1.5rem' }}>
Fuege Produkte hinzu, um loszulegen.
</p>
<Link to="/" style={{
padding: '10px 24px',
backgroundColor: '#2563eb',
color: 'white',
textDecoration: 'none',
borderRadius: '6px'
}}>
Zu den Produkten
</Link>
</div>
);
}
return (
<div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '2rem'
}}>
<h1>Warenkorb</h1>
<button onClick={clearCart} style={{
padding: '6px 14px',
border: '1px solid #fca5a5',
color: '#ef4444',
backgroundColor: 'white',
borderRadius: '6px',
cursor: 'pointer'
}}>
Warenkorb leeren
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 320px', gap: '2rem' }}>
{/* Warenkorb-Artikel */}
<div>
{cart.map(item => (
<div key={item.id} style={{
display: 'flex',
gap: '1rem',
padding: '1rem',
borderBottom: '1px solid #e2e8f0',
alignItems: 'center'
}}>
<img src={item.image} alt={item.name}
style={{ width: '80px', height: '80px', objectFit: 'cover', borderRadius: '8px' }} />
<div style={{ flex: 1 }}>
<h3 style={{ fontSize: '1rem', marginBottom: '0.25rem' }}>{item.name}</h3>
<p style={{ color: '#2563eb', fontWeight: 600 }}>{item.price.toFixed(2)} €</p>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}
style={{ width: '32px', height: '32px', border: '1px solid #e2e8f0',
borderRadius: '6px', cursor: 'pointer', backgroundColor: 'white' }}>
-
</button>
<span style={{ width: '30px', textAlign: 'center', fontWeight: 600 }}>
{item.quantity}
</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}
style={{ width: '32px', height: '32px', border: '1px solid #e2e8f0',
borderRadius: '6px', cursor: 'pointer', backgroundColor: 'white' }}>
+
</button>
</div>
<p style={{ fontWeight: 700, width: '80px', textAlign: 'right' }}>
{(item.price * item.quantity).toFixed(2)} €
</p>
<button onClick={() => removeFromCart(item.id)}
style={{ color: '#ef4444', background: 'none', border: 'none', cursor: 'pointer' }}>
Entfernen
</button>
</div>
))}
</div>
{/* Zusammenfassung */}
<div style={{
padding: '1.5rem',
backgroundColor: '#f8fafc',
borderRadius: '12px',
border: '1px solid #e2e8f0',
alignSelf: 'start',
position: 'sticky',
top: '80px'
}}>
<h2 style={{ marginBottom: '1rem' }}>Zusammenfassung</h2>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
<span>Zwischensumme</span>
<span>{totalPrice.toFixed(2)} €</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
<span>Versand</span>
<span>{totalPrice >= 50 ? 'Kostenlos' : '4,99 €'}</span>
</div>
<hr style={{ border: 'none', borderTop: '1px solid #e2e8f0', margin: '1rem 0' }} />
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 700, fontSize: '1.2rem' }}>
<span>Gesamt</span>
<span>{(totalPrice + (totalPrice >= 50 ? 0 : 4.99)).toFixed(2)} €</span>
</div>
{totalPrice < 50 && (
<p style={{ fontSize: '0.8rem', color: '#64748b', marginTop: '0.5rem' }}>
Noch {(50 - totalPrice).toFixed(2)} € bis zum kostenlosen Versand!
</p>
)}
<Link to="/checkout" style={{
display: 'block',
width: '100%',
padding: '12px',
marginTop: '1.5rem',
backgroundColor: '#2563eb',
color: 'white',
textAlign: 'center',
textDecoration: 'none',
borderRadius: '8px',
fontWeight: 600
}}>
Zur Kasse
</Link>
</div>
</div>
</div>
);
}
export default Cart;
Schritt 7: App zusammensetzen
// src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { CartProvider } from './context/CartContext';
import Layout from './components/Layout';
import Home from './pages/Home';
import Cart from './pages/Cart';
function App() {
return (
<BrowserRouter>
<CartProvider>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="cart" element={<Cart />} />
</Route>
</Routes>
</CartProvider>
</BrowserRouter>
);
}
export default App;
Uebungen zur Erweiterung
- Erstelle eine Produktdetail-Seite mit useParams, die das vollstaendige Produkt anzeigt
- Fuege eine Checkout-Seite mit Liefer- und Zahlungsformular hinzu
- Implementiere eine Wunschliste als zusaetzlichen Context
- Speichere den Warenkorb im localStorage, damit er nach dem Reload erhalten bleibt
- Lade Produkte von einer API statt aus der statischen Datei
Was kommt als Naechstes?
Du hast einen funktionierenden Online-Shop gebaut! Im naechsten und letzten Projekt erstellst du eine Dashboard-App – das komplexeste Projekt in diesem Kurs.
Zusammenfassung
- Cart-Context mit useReducer verwaltet den Warenkorb-State zentral
- Produktfilter kombinieren Suche, Kategorie und Sortierung mit
useMemo - React Router ermoeglicht Navigation zwischen Produktliste, Warenkorb und Checkout
- Komponentenaufteilung haelt den Code uebersichtlich und wartbar
- Berechnete Werte (Gesamtpreis, Versandkosten) werden aus dem State abgeleitet
Pro-Tipp: In einem echten Online-Shop wuerdest du Produktdaten von einer API laden, Zahlungen ueber einen Service wie Stripe abwickeln und den Warenkorb serverseitig validieren. Dieses Projekt gibt dir das Frontend-Fundament – die Backend-Integration kannst du als naechsten Lernschritt angehen!