init
This commit is contained in:
parent
420a7c203c
commit
b795bca84a
54
package-lock.json
generated
54
package-lock.json
generated
@ -18,6 +18,7 @@
|
|||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^7.6.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
@ -13970,6 +13971,53 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-scripts": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
||||||
@ -14876,6 +14924,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^7.6.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
|
|||||||
52
src/App.css
52
src/App.css
@ -1,38 +1,28 @@
|
|||||||
.App {
|
.App {
|
||||||
text-align: center;
|
text-align: left;
|
||||||
}
|
background-color: #f9f9f9;
|
||||||
|
|
||||||
.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;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
max-width: 600px;
|
||||||
flex-direction: column;
|
margin: 0 auto;
|
||||||
align-items: center;
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
justify-content: center;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-link {
|
body {
|
||||||
color: #61dafb;
|
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 {
|
box-sizing: border-box;
|
||||||
transform: rotate(0deg);
|
}
|
||||||
}
|
|
||||||
to {
|
button {
|
||||||
transform: rotate(360deg);
|
cursor: pointer;
|
||||||
}
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(<App />);
|
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
27
src/App.tsx
27
src/App.tsx
@ -1,25 +1,22 @@
|
|||||||
import React from 'react';
|
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';
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
<CartProvider>
|
||||||
|
<Router>
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<header className="App-header">
|
<Routes>
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
<Route path="/" element={<HomePage />} />
|
||||||
<p>
|
<Route path="/product/:category/:productId" element={<ProductDetailsPage />} />
|
||||||
Edit <code>src/App.tsx</code> and save to reload.
|
</Routes>
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
className="App-link"
|
|
||||||
href="https://reactjs.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Learn React
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Router>
|
||||||
|
</CartProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
112
src/components/CartOverview.css
Normal file
112
src/components/CartOverview.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
54
src/components/CartOverview.tsx
Normal file
54
src/components/CartOverview.tsx
Normal file
@ -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 <div className="empty-cart">Warenkorb ist leer</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="cart-overview">
|
||||||
|
<div className="cart-summary">
|
||||||
|
<div className="count-info">
|
||||||
|
<div className="food-count">Essen: {getFoodCount()}</div>
|
||||||
|
<div className="drink-count">Getränke: {getDrinksCount()}</div>
|
||||||
|
</div>
|
||||||
|
<div className="total-price">Gesamtpreis: {getTotalPrice().toFixed(2)} €</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="clear-cart-button" onClick={clearCart}>
|
||||||
|
Warenkorb löschen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="cart-items">
|
||||||
|
{cartItems.map((item) => (
|
||||||
|
<div key={`${item.productId}-${item.selectedOption.name}`} className="cart-item">
|
||||||
|
<div className="item-info">
|
||||||
|
<div className="item-name">{item.productName}</div>
|
||||||
|
<div className="item-option">{item.selectedOption.name}</div>
|
||||||
|
<div className="item-price">{(item.selectedOption.price * item.quantity).toFixed(2)} €</div>
|
||||||
|
</div>
|
||||||
|
<div className="item-quantity">
|
||||||
|
<button onClick={() => decrementQuantity(item.productId, item.selectedOption.name)}>-</button>
|
||||||
|
<span>{item.quantity}</span>
|
||||||
|
<button onClick={() => incrementQuantity(item.productId, item.selectedOption.name)}>+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CartOverview;
|
||||||
110
src/components/ProductDetails.css
Normal file
110
src/components/ProductDetails.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
78
src/components/ProductDetails.tsx
Normal file
78
src/components/ProductDetails.tsx
Normal file
@ -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<ProductDetailsProps> = ({ product, category }) => {
|
||||||
|
const [selectedOption, setSelectedOption] = useState<ProductOption>(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 (
|
||||||
|
<div className="product-details">
|
||||||
|
<h2>{product.name}</h2>
|
||||||
|
|
||||||
|
<div className="options-list">
|
||||||
|
<h3>Optionen:</h3>
|
||||||
|
{product.options.map(option => (
|
||||||
|
<div
|
||||||
|
key={option.name}
|
||||||
|
className={`option-item ${selectedOption.name === option.name ? 'selected' : ''}`}
|
||||||
|
onClick={() => setSelectedOption(option)}
|
||||||
|
>
|
||||||
|
<span>{option.name}</span>
|
||||||
|
{!hasSamePrice && <span className="option-price">{option.price.toFixed(2)} €</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{hasSamePrice && (
|
||||||
|
<div className="uniform-price">
|
||||||
|
Preis: {product.options[0].price.toFixed(2)} €
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="quantity-controls">
|
||||||
|
<h3>Menge:</h3>
|
||||||
|
<div className="quantity-buttons">
|
||||||
|
<button onClick={decrementQuantity}>-</button>
|
||||||
|
<span>{quantity}</span>
|
||||||
|
<button onClick={incrementQuantity}>+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="price-summary">
|
||||||
|
<div>Gesamt: <strong>{(selectedOption.price * quantity).toFixed(2)} €</strong></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="add-to-cart-button" onClick={handleAddToCart}>
|
||||||
|
Zum Warenkorb hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductDetails;
|
||||||
34
src/components/ProductList.css
Normal file
34
src/components/ProductList.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
33
src/components/ProductList.tsx
Normal file
33
src/components/ProductList.tsx
Normal file
@ -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<ProductListProps> = ({ products, category }) => {
|
||||||
|
// Sort products by name alphabetically
|
||||||
|
const sortedProducts = [...products].sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="product-list">
|
||||||
|
{sortedProducts.map(product => (
|
||||||
|
<Link
|
||||||
|
to={`/product/${category}/${product.id}`}
|
||||||
|
key={product.id}
|
||||||
|
className="product-item"
|
||||||
|
>
|
||||||
|
<div className="product-name">{product.name}</div>
|
||||||
|
<div className="product-price">{product.basePrice.toFixed(2)} €</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductList;
|
||||||
109
src/contexts/CartContext.tsx
Normal file
109
src/contexts/CartContext.tsx
Normal file
@ -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<CartContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function CartProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [cartItems, setCartItems] = useState<CartItem[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<CartContext.Provider
|
||||||
|
value={{
|
||||||
|
cartItems,
|
||||||
|
addToCart,
|
||||||
|
removeFromCart,
|
||||||
|
incrementQuantity,
|
||||||
|
decrementQuantity,
|
||||||
|
clearCart,
|
||||||
|
getTotalPrice,
|
||||||
|
getFoodCount,
|
||||||
|
getDrinksCount
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CartContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCart = () => {
|
||||||
|
const context = useContext(CartContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useCart must be used within a CartProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
142
src/data/products.json
Normal file
142
src/data/products.json
Normal file
@ -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}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
52
src/pages/HomePage.css
Normal file
52
src/pages/HomePage.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
66
src/pages/HomePage.tsx
Normal file
66
src/pages/HomePage.tsx
Normal file
@ -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<ProductsData>({ 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 (
|
||||||
|
<div className="home-page">
|
||||||
|
<header className="app-header">
|
||||||
|
<h1>Preis Rechner</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="category-tabs">
|
||||||
|
<button
|
||||||
|
className={activeTab === 'food' ? 'active' : ''}
|
||||||
|
onClick={() => setActiveTab('food')}
|
||||||
|
>
|
||||||
|
Essen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={activeTab === 'drinks' ? 'active' : ''}
|
||||||
|
onClick={() => setActiveTab('drinks')}
|
||||||
|
>
|
||||||
|
Trinken
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="products-section">
|
||||||
|
<h2>{activeTab === 'food' ? 'Essen' : 'Getränke'}</h2>
|
||||||
|
{activeTab === 'food' && <ProductList products={productsData.food} category="food" />}
|
||||||
|
{activeTab === 'drinks' && <ProductList products={productsData.drinks} category="drinks" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="cart-section">
|
||||||
|
<h2>Warenkorb</h2>
|
||||||
|
<CartOverview />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
40
src/pages/ProductDetailsPage.css
Normal file
40
src/pages/ProductDetailsPage.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
56
src/pages/ProductDetailsPage.tsx
Normal file
56
src/pages/ProductDetailsPage.tsx
Normal file
@ -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<Product | null>(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 <div className="loading">Laden...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="product-details-page">
|
||||||
|
<header className="details-header">
|
||||||
|
<button className="back-button" onClick={() => navigate('/')}>
|
||||||
|
← Zurück
|
||||||
|
</button>
|
||||||
|
<h1>{product.name}</h1>
|
||||||
|
</header>
|
||||||
|
<ProductDetails
|
||||||
|
product={product}
|
||||||
|
category={category as 'food' | 'drinks'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductDetailsPage;
|
||||||
@ -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';
|
|
||||||
24
src/types/types.ts
Normal file
24
src/types/types.ts
Normal file
@ -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';
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user