diff --git a/package-lock.json b/package-lock.json
index bb7b3d5..a548bb9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"@types/react-dom": "^19.1.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
+ "react-router-dom": "^7.6.2",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
@@ -13970,6 +13971,53 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.6.2",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz",
+ "integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.6.2",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz",
+ "integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-router/node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -14876,6 +14924,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+ "license": "MIT"
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
diff --git a/package.json b/package.json
index 6137df1..5a67b22 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"@types/react-dom": "^19.1.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
+ "react-router-dom": "^7.6.2",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
diff --git a/src/App.css b/src/App.css
index 74b5e05..1ac25e6 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,38 +1,28 @@
.App {
- text-align: center;
-}
-
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
+ text-align: left;
+ background-color: #f9f9f9;
min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
+ max-width: 600px;
+ margin: 0 auto;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
-.App-link {
- color: #61dafb;
+body {
+ margin: 0;
+ padding: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: #f0f0f0;
}
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
+* {
+ box-sizing: border-box;
+}
+
+button {
+ cursor: pointer;
+ touch-action: manipulation;
}
diff --git a/src/App.test.tsx b/src/App.test.tsx
deleted file mode 100644
index 2a68616..0000000
--- a/src/App.test.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/src/App.tsx b/src/App.tsx
index a53698a..484390e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,25 +1,22 @@
import React from 'react';
-import logo from './logo.svg';
+import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
+import HomePage from './pages/HomePage';
+import ProductDetailsPage from './pages/ProductDetailsPage';
+import { CartProvider } from './contexts/CartContext';
import './App.css';
function App() {
return (
-
+
+
+
+
+ } />
+ } />
+
+
+
+
);
}
diff --git a/src/components/CartOverview.css b/src/components/CartOverview.css
new file mode 100644
index 0000000..166da2d
--- /dev/null
+++ b/src/components/CartOverview.css
@@ -0,0 +1,112 @@
+.cart-overview {
+ padding: 15px;
+}
+
+.cart-summary {
+ background-color: #f5f5f5;
+ border-radius: 8px;
+ padding: 15px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.count-info {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+.food-count, .drink-count {
+ font-weight: 500;
+}
+
+.total-price {
+ font-weight: bold;
+ font-size: 1.2rem;
+ text-align: center;
+ margin-top: 10px;
+ color: #2c7be5;
+}
+
+.clear-cart-button {
+ width: 100%;
+ padding: 12px;
+ margin-bottom: 15px;
+ background-color: #ff5252;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-weight: bold;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.clear-cart-button:active {
+ background-color: #e53935;
+}
+
+.cart-items {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.cart-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: white;
+ padding: 15px;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.item-info {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ flex: 1;
+}
+
+.item-name {
+ font-weight: 500;
+}
+
+.item-option {
+ font-size: 0.9rem;
+ color: #666;
+}
+
+.item-price {
+ font-weight: bold;
+ color: #2c7be5;
+}
+
+.item-quantity {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.item-quantity button {
+ width: 30px;
+ height: 30px;
+ border: none;
+ border-radius: 50%;
+ background-color: #2c7be5;
+ color: white;
+ font-weight: bold;
+ cursor: pointer;
+}
+
+.item-quantity button:active {
+ background-color: #1b5eb5;
+ transform: scale(0.95);
+}
+
+.empty-cart {
+ text-align: center;
+ padding: 30px;
+ color: #666;
+ font-style: italic;
+}
diff --git a/src/components/CartOverview.tsx b/src/components/CartOverview.tsx
new file mode 100644
index 0000000..a589bb6
--- /dev/null
+++ b/src/components/CartOverview.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { useCart } from '../contexts/CartContext';
+import './CartOverview.css';
+
+const CartOverview: React.FC = () => {
+ const {
+ cartItems,
+ incrementQuantity,
+ decrementQuantity,
+ getTotalPrice,
+ getFoodCount,
+ getDrinksCount,
+ clearCart
+ } = useCart();
+
+ if (cartItems.length === 0) {
+ return Warenkorb ist leer
;
+ }
+
+ return (
+
+
+
+
Essen: {getFoodCount()}
+
Getränke: {getDrinksCount()}
+
+
Gesamtpreis: {getTotalPrice().toFixed(2)} €
+
+
+
+
+
+ {cartItems.map((item) => (
+
+
+
{item.productName}
+
{item.selectedOption.name}
+
{(item.selectedOption.price * item.quantity).toFixed(2)} €
+
+
+
+ {item.quantity}
+
+
+
+ ))}
+
+
+ );
+};
+
+export default CartOverview;
diff --git a/src/components/ProductDetails.css b/src/components/ProductDetails.css
new file mode 100644
index 0000000..a3021f7
--- /dev/null
+++ b/src/components/ProductDetails.css
@@ -0,0 +1,110 @@
+.product-details {
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.product-details h2 {
+ margin: 0;
+ text-align: center;
+ font-size: 1.5rem;
+}
+
+.options-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.options-list h3 {
+ margin: 0;
+ font-size: 1.1rem;
+}
+
+.option-item {
+ display: flex;
+ justify-content: space-between;
+ padding: 15px;
+ background-color: #f5f5f5;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.option-item.selected {
+ background-color: #e3f2fd;
+ border: 1px solid #2c7be5;
+}
+
+.uniform-price {
+ padding: 10px;
+ text-align: center;
+ font-weight: bold;
+ color: #2c7be5;
+}
+
+.option-price {
+ font-weight: bold;
+ color: #2c7be5;
+}
+
+.quantity-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.quantity-controls h3 {
+ margin: 0;
+ font-size: 1.1rem;
+}
+
+.quantity-buttons {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 20px;
+}
+
+.quantity-buttons button {
+ width: 40px;
+ height: 40px;
+ font-size: 1.2rem;
+ border: none;
+ border-radius: 50%;
+ background-color: #2c7be5;
+ color: white;
+ cursor: pointer;
+}
+
+.quantity-buttons button:active {
+ background-color: #1b5eb5;
+ transform: scale(0.95);
+}
+
+.quantity-buttons span {
+ font-size: 1.2rem;
+ font-weight: bold;
+}
+
+.price-summary {
+ text-align: center;
+ font-size: 1.2rem;
+}
+
+.add-to-cart-button {
+ background-color: #4caf50;
+ color: white;
+ border: none;
+ padding: 15px 0;
+ border-radius: 8px;
+ font-size: 1.1rem;
+ font-weight: bold;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.add-to-cart-button:active {
+ background-color: #3d8b40;
+}
diff --git a/src/components/ProductDetails.tsx b/src/components/ProductDetails.tsx
new file mode 100644
index 0000000..27e5235
--- /dev/null
+++ b/src/components/ProductDetails.tsx
@@ -0,0 +1,78 @@
+import React, { useState } from 'react';
+import { Product, ProductOption } from '../types/types';
+import { useCart } from '../contexts/CartContext';
+import { useNavigate } from 'react-router-dom';
+import './ProductDetails.css';
+
+type ProductDetailsProps = {
+ product: Product;
+ category: 'food' | 'drinks';
+};
+
+const ProductDetails: React.FC = ({ product, category }) => {
+ const [selectedOption, setSelectedOption] = useState(product.options[0]);
+ const [quantity, setQuantity] = useState(1);
+ const { addToCart } = useCart();
+ const navigate = useNavigate();
+
+ const handleAddToCart = () => {
+ addToCart({
+ productId: product.id,
+ productName: product.name,
+ selectedOption,
+ quantity,
+ category
+ });
+ navigate('/');
+ };
+
+ const incrementQuantity = () => setQuantity(prev => prev + 1);
+ const decrementQuantity = () => setQuantity(prev => Math.max(1, prev - 1));
+
+ // Check if all options have the same price
+ const hasSamePrice = product.options.every(option => option.price === product.options[0].price);
+
+ return (
+
+
{product.name}
+
+
+
Optionen:
+ {product.options.map(option => (
+
setSelectedOption(option)}
+ >
+ {option.name}
+ {!hasSamePrice && {option.price.toFixed(2)} €}
+
+ ))}
+ {hasSamePrice && (
+
+ Preis: {product.options[0].price.toFixed(2)} €
+
+ )}
+
+
+
+
Menge:
+
+
+ {quantity}
+
+
+
+
+
+
Gesamt: {(selectedOption.price * quantity).toFixed(2)} €
+
+
+
+
+ );
+};
+
+export default ProductDetails;
diff --git a/src/components/ProductList.css b/src/components/ProductList.css
new file mode 100644
index 0000000..f728ba8
--- /dev/null
+++ b/src/components/ProductList.css
@@ -0,0 +1,34 @@
+.product-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 10px;
+}
+
+.product-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px;
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ text-decoration: none;
+ color: #333;
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.product-item:active {
+ transform: scale(0.98);
+}
+
+.product-name {
+ font-weight: 500;
+ font-size: 1.1rem;
+}
+
+.product-price {
+ font-weight: bold;
+ color: #2c7be5;
+ font-size: 1.1rem;
+}
diff --git a/src/components/ProductList.tsx b/src/components/ProductList.tsx
new file mode 100644
index 0000000..3d442cd
--- /dev/null
+++ b/src/components/ProductList.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Product } from '../types/types';
+import { Link } from 'react-router-dom';
+import './ProductList.css';
+
+type ProductListProps = {
+ products: Product[];
+ category: 'food' | 'drinks';
+};
+
+const ProductList: React.FC = ({ products, category }) => {
+ // Sort products by name alphabetically
+ const sortedProducts = [...products].sort((a, b) =>
+ a.name.localeCompare(b.name)
+ );
+
+ return (
+
+ {sortedProducts.map(product => (
+
+
{product.name}
+
{product.basePrice.toFixed(2)} €
+
+ ))}
+
+ );
+};
+
+export default ProductList;
diff --git a/src/contexts/CartContext.tsx b/src/contexts/CartContext.tsx
new file mode 100644
index 0000000..0baf6dc
--- /dev/null
+++ b/src/contexts/CartContext.tsx
@@ -0,0 +1,109 @@
+import React, { createContext, useContext, useState, ReactNode } from 'react';
+import { CartItem } from '../types/types';
+
+type CartContextType = {
+ cartItems: CartItem[];
+ addToCart: (item: CartItem) => void;
+ removeFromCart: (productId: string, optionName: string) => void;
+ incrementQuantity: (productId: string, optionName: string) => void;
+ decrementQuantity: (productId: string, optionName: string) => void;
+ clearCart: () => void;
+ getTotalPrice: () => number;
+ getFoodCount: () => number;
+ getDrinksCount: () => number;
+};
+
+const CartContext = createContext(undefined);
+
+export function CartProvider({ children }: { children: ReactNode }) {
+ const [cartItems, setCartItems] = useState([]);
+
+ const addToCart = (item: CartItem) => {
+ const existingItemIndex = cartItems.findIndex(
+ i => i.productId === item.productId && i.selectedOption.name === item.selectedOption.name
+ );
+
+ if (existingItemIndex !== -1) {
+ const updatedItems = [...cartItems];
+ updatedItems[existingItemIndex].quantity += item.quantity;
+ setCartItems(updatedItems);
+ } else {
+ setCartItems([...cartItems, item]);
+ }
+ };
+
+ const removeFromCart = (productId: string, optionName: string) => {
+ setCartItems(cartItems.filter(
+ item => !(item.productId === productId && item.selectedOption.name === optionName)
+ ));
+ };
+
+ const incrementQuantity = (productId: string, optionName: string) => {
+ setCartItems(
+ cartItems.map(item =>
+ item.productId === productId && item.selectedOption.name === optionName
+ ? { ...item, quantity: item.quantity + 1 }
+ : item
+ )
+ );
+ };
+
+ const decrementQuantity = (productId: string, optionName: string) => {
+ setCartItems(
+ cartItems.map(item =>
+ item.productId === productId && item.selectedOption.name === optionName
+ ? { ...item, quantity: Math.max(0, item.quantity - 1) }
+ : item
+ ).filter(item => item.quantity > 0)
+ );
+ };
+
+ const clearCart = () => {
+ setCartItems([]);
+ };
+
+ const getTotalPrice = () => {
+ return cartItems.reduce(
+ (total, item) => total + item.selectedOption.price * item.quantity,
+ 0
+ );
+ };
+
+ const getFoodCount = () => {
+ return cartItems
+ .filter(item => item.category === 'food')
+ .reduce((count, item) => count + item.quantity, 0);
+ };
+
+ const getDrinksCount = () => {
+ return cartItems
+ .filter(item => item.category === 'drinks')
+ .reduce((count, item) => count + item.quantity, 0);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useCart = () => {
+ const context = useContext(CartContext);
+ if (context === undefined) {
+ throw new Error('useCart must be used within a CartProvider');
+ }
+ return context;
+};
diff --git a/src/data/products.json b/src/data/products.json
new file mode 100644
index 0000000..81ce6f1
--- /dev/null
+++ b/src/data/products.json
@@ -0,0 +1,142 @@
+{
+ "food": [
+ {
+ "id": "f1",
+ "name": "Hendl",
+ "basePrice": 9.50,
+ "options": [
+ {"name": "Halbes Hendl", "price": 9.50},
+ {"name": "Ganzes Hendl", "price": 16.00},
+ {"name": "Hendl mit Semmel", "price": 11.00}
+ ]
+ },
+ {
+ "id": "f2",
+ "name": "Schnitzelsemmel",
+ "basePrice": 5.80,
+ "options": [
+ {"name": "Klassisch", "price": 5.80},
+ {"name": "Mit Salat", "price": 6.50},
+ {"name": "Mit Käse", "price": 6.80}
+ ]
+ },
+ {
+ "id": "f3",
+ "name": "Pommes",
+ "basePrice": 3.50,
+ "options": [
+ {"name": "Klein", "price": 3.50},
+ {"name": "Groß", "price": 4.50},
+ {"name": "Mit Ketchup/Mayo", "price": 4.00}
+ ]
+ },
+ {
+ "id": "f4",
+ "name": "Grillwurst",
+ "basePrice": 4.20,
+ "options": [
+ {"name": "Bratwurst", "price": 4.20},
+ {"name": "Käsekrainer", "price": 4.50},
+ {"name": "Debreziner", "price": 4.20}
+ ]
+ },
+ {
+ "id": "f5",
+ "name": "Leberkässemmel",
+ "basePrice": 4.00,
+ "options": [
+ {"name": "Klassisch", "price": 4.00},
+ {"name": "Mit Gurkerl", "price": 4.30},
+ {"name": "Mit Kren", "price": 4.30}
+ ]
+ },
+ {
+ "id": "f6",
+ "name": "Langos",
+ "basePrice": 4.80,
+ "options": [
+ {"name": "Mit Knoblauch", "price": 4.80},
+ {"name": "Mit Käse", "price": 5.50},
+ {"name": "Mit Sauerrahm", "price": 5.50}
+ ]
+ }
+ ],
+ "drinks": [
+ {
+ "id": "d1",
+ "name": "Bier",
+ "basePrice": 4.20,
+ "options": [
+ {"name": "Krügerl (0,5L)", "price": 4.20},
+ {"name": "Seidl (0,3L)", "price": 3.50},
+ {"name": "Radler (0,5L)", "price": 4.20}
+ ]
+ },
+ {
+ "id": "d2",
+ "name": "Cola",
+ "basePrice": 3.50,
+ "options": [
+ {"name": "Cola Rot (0,5L)", "price": 3.50},
+ {"name": "Cola Weiß (0,5L)", "price": 3.50},
+ {"name": "Cola Rot (0,3L)", "price": 2.80},
+ {"name": "Cola Weiß (0,3L)", "price": 2.80}
+ ]
+ },
+ {
+ "id": "d3",
+ "name": "Softdrinks",
+ "basePrice": 3.50,
+ "options": [
+ {"name": "Fanta (0,5L)", "price": 3.50},
+ {"name": "Sprite (0,5L)", "price": 3.50},
+ {"name": "Almdudler (0,5L)", "price": 3.50},
+ {"name": "Eistee (0,5L)", "price": 3.50}
+ ]
+ },
+ {
+ "id": "d4",
+ "name": "Mineral",
+ "basePrice": 2.80,
+ "options": [
+ {"name": "Prickelnd (0,5L)", "price": 2.80},
+ {"name": "Still (0,5L)", "price": 2.80},
+ {"name": "Prickelnd (0,3L)", "price": 2.20},
+ {"name": "Still (0,3L)", "price": 2.20}
+ ]
+ },
+ {
+ "id": "d5",
+ "name": "Gespritzt",
+ "basePrice": 3.00,
+ "options": [
+ {"name": "Apfelsaft g'spritzt", "price": 3.00},
+ {"name": "Johannisbeer g'spritzt", "price": 3.20},
+ {"name": "Himbeer g'spritzt", "price": 3.20},
+ {"name": "Holler g'spritzt", "price": 3.20}
+ ]
+ },
+ {
+ "id": "d6",
+ "name": "Spritzer",
+ "basePrice": 3.80,
+ "options": [
+ {"name": "Weißer Spritzer", "price": 3.80},
+ {"name": "Aperol Spritzer", "price": 4.50},
+ {"name": "Hugo", "price": 4.50},
+ {"name": "G'spritzter süß", "price": 3.80}
+ ]
+ },
+ {
+ "id": "d7",
+ "name": "Schnaps",
+ "basePrice": 3.00,
+ "options": [
+ {"name": "Marille (2cl)", "price": 3.00},
+ {"name": "Williams (2cl)", "price": 3.00},
+ {"name": "Haselnuss (2cl)", "price": 3.00},
+ {"name": "Obstler (2cl)", "price": 2.80}
+ ]
+ }
+ ]
+}
diff --git a/src/pages/HomePage.css b/src/pages/HomePage.css
new file mode 100644
index 0000000..4205d4d
--- /dev/null
+++ b/src/pages/HomePage.css
@@ -0,0 +1,52 @@
+.home-page {
+ padding-bottom: 20px;
+}
+
+.app-header {
+ background-color: #2c7be5;
+ color: white;
+ text-align: center;
+ padding: 15px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.app-header h1 {
+ margin: 0;
+ font-size: 1.5rem;
+}
+
+.category-tabs {
+ display: flex;
+ margin: 10px;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.category-tabs button {
+ flex: 1;
+ padding: 15px;
+ background-color: #f5f5f5;
+ border: none;
+ font-size: 1.1rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.category-tabs button.active {
+ background-color: #2c7be5;
+ color: white;
+}
+
+.products-section, .cart-section {
+ margin: 20px 0;
+}
+
+.products-section h2, .cart-section h2 {
+ margin: 0 10px 10px 10px;
+ font-size: 1.3rem;
+ color: #333;
+ padding-left: 10px;
+ border-left: 4px solid #2c7be5;
+}
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
new file mode 100644
index 0000000..3b4a23a
--- /dev/null
+++ b/src/pages/HomePage.tsx
@@ -0,0 +1,66 @@
+import React, { useState, useEffect } from 'react';
+import ProductList from '../components/ProductList';
+import CartOverview from '../components/CartOverview';
+import { ProductsData } from '../types/types';
+import './HomePage.css';
+
+const HomePage: React.FC = () => {
+ const [productsData, setProductsData] = useState({ food: [], drinks: [] });
+ const [activeTab, setActiveTab] = useState<'food' | 'drinks'>('food');
+
+ useEffect(() => {
+ const loadProducts = async () => {
+ try {
+ const data = await import('../data/products.json');
+
+ // Sort both food and drinks by name
+ const sortedData = {
+ food: [...data.food].sort((a, b) => a.name.localeCompare(b.name)),
+ drinks: [...data.drinks].sort((a, b) => a.name.localeCompare(b.name))
+ };
+
+ setProductsData(sortedData);
+ } catch (error) {
+ console.error('Failed to load products:', error);
+ }
+ };
+
+ loadProducts();
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
{activeTab === 'food' ? 'Essen' : 'Getränke'}
+ {activeTab === 'food' &&
}
+ {activeTab === 'drinks' &&
}
+
+
+
+
Warenkorb
+
+
+
+ );
+};
+
+export default HomePage;
diff --git a/src/pages/ProductDetailsPage.css b/src/pages/ProductDetailsPage.css
new file mode 100644
index 0000000..3ad1a5a
--- /dev/null
+++ b/src/pages/ProductDetailsPage.css
@@ -0,0 +1,40 @@
+.product-details-page {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.details-header {
+ padding: 15px;
+ display: flex;
+ align-items: center;
+ background-color: #2c7be5;
+ color: white;
+ position: relative;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.back-button {
+ background: none;
+ border: none;
+ color: white;
+ font-size: 1.2rem;
+ cursor: pointer;
+ padding: 0;
+ position: absolute;
+ left: 15px;
+}
+
+.details-header h1 {
+ flex: 1;
+ text-align: center;
+ margin: 0;
+ font-size: 1.3rem;
+}
+
+.loading {
+ text-align: center;
+ padding: 30px;
+ font-style: italic;
+ color: #666;
+}
diff --git a/src/pages/ProductDetailsPage.tsx b/src/pages/ProductDetailsPage.tsx
new file mode 100644
index 0000000..ac9a1eb
--- /dev/null
+++ b/src/pages/ProductDetailsPage.tsx
@@ -0,0 +1,56 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import ProductDetails from '../components/ProductDetails';
+import { Product } from '../types/types';
+import './ProductDetailsPage.css';
+
+const ProductDetailsPage: React.FC = () => {
+ const { category, productId } = useParams<{ category: string, productId: string }>();
+ const [product, setProduct] = useState(null);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const loadProduct = async () => {
+ try {
+ const data = await import('../data/products.json');
+ if (!category || (category !== 'food' && category !== 'drinks')) {
+ throw new Error('Invalid category');
+ }
+
+ const foundProduct = data[category].find((p: Product) => p.id === productId);
+
+ if (foundProduct) {
+ setProduct(foundProduct);
+ } else {
+ throw new Error('Product not found');
+ }
+ } catch (error) {
+ console.error('Failed to load product:', error);
+ navigate('/');
+ }
+ };
+
+ loadProduct();
+ }, [category, productId, navigate]);
+
+ if (!product || !category) {
+ return Laden...
;
+ }
+
+ return (
+
+
+
+ {product.name}
+
+
+
+ );
+};
+
+export default ProductDetailsPage;
diff --git a/src/setupTests.ts b/src/setupTests.ts
deleted file mode 100644
index 8f2609b..0000000
--- a/src/setupTests.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';
diff --git a/src/types/types.ts b/src/types/types.ts
new file mode 100644
index 0000000..144fa68
--- /dev/null
+++ b/src/types/types.ts
@@ -0,0 +1,24 @@
+export type ProductOption = {
+ name: string;
+ price: number;
+};
+
+export type Product = {
+ id: string;
+ name: string;
+ basePrice: number;
+ options: ProductOption[];
+};
+
+export type ProductsData = {
+ food: Product[];
+ drinks: Product[];
+};
+
+export type CartItem = {
+ productId: string;
+ productName: string;
+ selectedOption: ProductOption;
+ quantity: number;
+ category: 'food' | 'drinks';
+};