Added Product Page and Basket.
This commit is contained in:
@@ -4,17 +4,24 @@ import './App.css';
|
||||
import NavBar from './helper/NavBar';
|
||||
import Home from './pages/Home';
|
||||
import NoPage from './pages/NoPage';
|
||||
import Product from './pages/Product';
|
||||
import Payment from './pages/Payment';
|
||||
import { BasketProvider } from './helper/BasketProvider';
|
||||
|
||||
export default function App() {
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<BasketProvider>
|
||||
<BrowserRouter>
|
||||
<NavBar />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="*" element={<NoPage />} />
|
||||
<Route path="/product/:id" element={<Product />} />
|
||||
<Route path="/payment" element={<Payment />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</BrowserRouter>
|
||||
</BasketProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
52
01-frontend/src/helper/BasketProvider.tsx
Normal file
52
01-frontend/src/helper/BasketProvider.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
interface BasketItem {
|
||||
itemId: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
interface BasketContextType {
|
||||
basket: BasketItem[];
|
||||
addToBasket: (itemId: string, quantity: number) => void;
|
||||
clearBasket: () => void;
|
||||
}
|
||||
|
||||
const BasketContext = createContext<BasketContextType | undefined>(undefined);
|
||||
|
||||
export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [basket, setBasket] = useState<BasketItem[]>([]);
|
||||
|
||||
const addToBasket = (itemId: string, quantity: number) => {
|
||||
setBasket((prevBasket) => {
|
||||
const existingItem = prevBasket.find((item) => item.itemId === itemId);
|
||||
if (existingItem) {
|
||||
// Update quantity if item already exists
|
||||
return prevBasket.map((item) =>
|
||||
item.itemId === itemId
|
||||
? { ...item, quantity: item.quantity + quantity }
|
||||
: item
|
||||
);
|
||||
}
|
||||
// Add new item to basket
|
||||
return [...prevBasket, { itemId, quantity }];
|
||||
});
|
||||
};
|
||||
|
||||
const clearBasket = () => {
|
||||
setBasket([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<BasketContext.Provider value={{ basket, addToBasket, clearBasket }}>
|
||||
{children}
|
||||
</BasketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useBasket = () => {
|
||||
const context = useContext(BasketContext);
|
||||
if (!context) {
|
||||
throw new Error('useBasket must be used within a BasketProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,12 +1,26 @@
|
||||
import { AddShoppingCart } from "@mui/icons-material";
|
||||
import { Card, CardActionArea, CardContent, CardMedia, IconButton, Paper, Rating, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Item from "../components/Item";
|
||||
import { useBasket } from "./BasketProvider";
|
||||
|
||||
export default function ItemCard({ item }: { item: Item }) {
|
||||
const navigate = useNavigate()
|
||||
const { addToBasket } = useBasket();
|
||||
|
||||
const handleAddToCart = () => {
|
||||
addToBasket(item.id, 1);
|
||||
console.log(`Added ${1} of ${item.name} to basket`);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
navigate(`/product/${item.id}`, { state: { item } });
|
||||
}
|
||||
|
||||
export default function ItemCard({item}: {item: Item}) {
|
||||
return (
|
||||
<Paper elevation={3}>
|
||||
<Card>
|
||||
<CardActionArea>
|
||||
<CardActionArea onClick={handleClick}>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="140"
|
||||
@@ -19,9 +33,9 @@ export default function ItemCard({item}: {item: Item}) {
|
||||
</Typography>
|
||||
<Rating name="half-rating" readOnly defaultValue={item.rating} precision={0.5} />
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{item.description}
|
||||
{item.description}
|
||||
</Typography>
|
||||
<IconButton color="primary" aria-label="add to shopping cart">
|
||||
<IconButton color="primary" aria-label="add to shopping cart" onClick={handleAddToCart}>
|
||||
<AddShoppingCart />
|
||||
</IconButton>
|
||||
</CardContent>
|
||||
|
||||
67
01-frontend/src/helper/NavBar.css
Normal file
67
01-frontend/src/helper/NavBar.css
Normal file
@@ -0,0 +1,67 @@
|
||||
/* Navbar styles */
|
||||
.navbar {
|
||||
background-color: #1976d2; /* Material-UI Primary Color */
|
||||
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Logo styles */
|
||||
.navbar-logo {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
/* Menu styles */
|
||||
.navbar-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Search styles */
|
||||
.search {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.search:hover {
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
.search-icon-wrapper {
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.search-input {
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
padding: 8px 8px 8px 40px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* User avatar styles */
|
||||
.navbar-user {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
/* Typography styles */
|
||||
.navbar-typography {
|
||||
font-family: 'monospace';
|
||||
font-weight: 700;
|
||||
letter-spacing: .3rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.navbar-button {
|
||||
my: 2;
|
||||
color: white;
|
||||
display: block;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import MenuItem from '@mui/material/MenuItem';
|
||||
import AdbIcon from '@mui/icons-material/Adb';
|
||||
import { alpha, InputBase, styled } from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const pages = ['Products', 'Pricing', 'Contact'];
|
||||
const settings = ['Profile', 'Account', 'Dashboard', 'Logout'];
|
||||
@@ -61,6 +62,7 @@ const Search = styled('div')(({ theme }) => ({
|
||||
}));
|
||||
|
||||
export default function NavBar() {
|
||||
const navigate = useNavigate();
|
||||
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(null);
|
||||
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(null);
|
||||
|
||||
@@ -79,6 +81,11 @@ export default function NavBar() {
|
||||
setAnchorElUser(null);
|
||||
};
|
||||
|
||||
const handleCheckout = () => {
|
||||
navigate('/payment');
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<AppBar>
|
||||
<Container maxWidth="xl">
|
||||
@@ -176,6 +183,16 @@ export default function NavBar() {
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
|
||||
|
||||
<Button
|
||||
onClick={handleCheckout}
|
||||
sx={{ my: 2, color: 'white', display: 'block' }}
|
||||
>
|
||||
Checkout
|
||||
</Button>
|
||||
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 0 }}>
|
||||
<Tooltip title="Open settings">
|
||||
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
|
||||
|
||||
0
01-frontend/src/helper/helper.css
Normal file
0
01-frontend/src/helper/helper.css
Normal file
@@ -1,9 +1,10 @@
|
||||
import { Box, Pagination } from "@mui/material";
|
||||
import { useState, useEffect } from "react";
|
||||
import ItemCard from "../helper/ItemCard";
|
||||
import Item from "../components/Item";
|
||||
import "./pages.css"; // Import der CSS-Datei
|
||||
|
||||
export default function Home() {
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
id: "1",
|
||||
@@ -25,16 +26,160 @@ export default function Home() {
|
||||
rating: 4.0,
|
||||
discount: 20,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Item 3",
|
||||
description: "Description 3",
|
||||
price: 30,
|
||||
stock: 300,
|
||||
category: "Category 3",
|
||||
rating: 4.8,
|
||||
discount: 15,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Item 3",
|
||||
description: "Description 3",
|
||||
price: 30,
|
||||
stock: 300,
|
||||
category: "Category 3",
|
||||
rating: 4.8,
|
||||
discount: 15,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "Item 3",
|
||||
description: "Description 3",
|
||||
price: 30,
|
||||
stock: 300,
|
||||
category: "Category 3",
|
||||
rating: 4.8,
|
||||
discount: 15,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "Item 3",
|
||||
description: "Description 3",
|
||||
price: 30,
|
||||
stock: 300,
|
||||
category: "Category 3",
|
||||
rating: 4.8,
|
||||
discount: 15,
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
name: "Item 3",
|
||||
description: "Description 3",
|
||||
price: 30,
|
||||
stock: 300,
|
||||
category: "Category 3",
|
||||
rating: 4.8,
|
||||
discount: 15,
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
name: "Item 3",
|
||||
description: "Description 3",
|
||||
price: 30,
|
||||
stock: 300,
|
||||
category: "Category 3",
|
||||
rating: 4.8,
|
||||
discount: 15,
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
name: "Item 3",
|
||||
description: "Description 3",
|
||||
price: 30,
|
||||
stock: 300,
|
||||
category: "Category 3",
|
||||
rating: 4.8,
|
||||
discount: 15,
|
||||
},
|
||||
{
|
||||
id: "10",
|
||||
name: "Item 3",
|
||||
description: "Description 3",
|
||||
price: 30,
|
||||
stock: 300,
|
||||
category: "Category 3",
|
||||
rating: 4.8,
|
||||
discount: 15,
|
||||
},
|
||||
{
|
||||
id: "11",
|
||||
name: "Item 3",
|
||||
description: "Description 3",
|
||||
price: 30,
|
||||
stock: 300,
|
||||
category: "Category 3",
|
||||
rating: 4.8,
|
||||
discount: 15,
|
||||
},
|
||||
{
|
||||
id: "12",
|
||||
name: "Item 3",
|
||||
description: "Description 3",
|
||||
price: 30,
|
||||
stock: 300,
|
||||
category: "Category 3",
|
||||
rating: 4.8,
|
||||
discount: 15,
|
||||
},
|
||||
// Weitere Items hinzufügen
|
||||
];
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1); // Zustand für die aktuelle Seite
|
||||
const [itemsPerPage, setItemsPerPage] = useState(9); // Dynamische Anzahl der Items pro Seite
|
||||
|
||||
// Berechnung der angezeigten Items basierend auf der aktuellen Seite
|
||||
const indexOfLastItem = currentPage * itemsPerPage;
|
||||
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||
const currentItems = items.slice(indexOfFirstItem, indexOfLastItem);
|
||||
|
||||
// Handler für die Pagination
|
||||
const handlePageChange = (event: React.ChangeEvent<unknown>, page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
// Dynamische Berechnung der Anzahl der Items pro Seite basierend auf der Fensterbreite
|
||||
const calculateItemsPerPage = () => {
|
||||
const width = window.innerWidth;
|
||||
const columns = Math.floor(width / 300); // Jede Spalte ist ca. 300px breit
|
||||
const rows = Math.floor(window.innerHeight / 400); // Jede Zeile ist ca. 400px hoch
|
||||
return columns * rows;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const updateItemsPerPage = () => {
|
||||
setItemsPerPage(calculateItemsPerPage());
|
||||
};
|
||||
|
||||
// Initiale Berechnung und Event-Listener hinzufügen
|
||||
updateItemsPerPage();
|
||||
window.addEventListener("resize", updateItemsPerPage);
|
||||
|
||||
// Event-Listener entfernen, wenn die Komponente unmountet wird
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateItemsPerPage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="page-background">
|
||||
<Box className="cardgrid">
|
||||
{items.map((item) => (
|
||||
<ItemCard item={item} />
|
||||
{currentItems.map((item) => (
|
||||
<ItemCard key={item.id} item={item} />
|
||||
))}
|
||||
<Pagination count={10} variant="outlined" shape="rounded" />
|
||||
</Box>
|
||||
<Pagination
|
||||
count={Math.ceil(items.length / itemsPerPage)} // Gesamtanzahl der Seiten
|
||||
page={currentPage} // Aktuelle Seite
|
||||
onChange={handlePageChange} // Seitenwechsel-Handler
|
||||
variant="outlined"
|
||||
shape="rounded"
|
||||
sx={{ marginTop: 2 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
23
01-frontend/src/pages/Payment.tsx
Normal file
23
01-frontend/src/pages/Payment.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useBasket } from "../helper/BasketProvider";
|
||||
|
||||
export default function Payment() {
|
||||
const { basket, clearBasket } = useBasket();
|
||||
|
||||
if (basket.length === 0) {
|
||||
return <div>Your basket is empty</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Payment Page</h1>
|
||||
<ul>
|
||||
{basket.map((item) => (
|
||||
<li key={item.itemId}>
|
||||
Item ID: {item.itemId}, Quantity: {item.quantity}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button onClick={clearBasket}>Clear Basket</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
141
01-frontend/src/pages/Product.tsx
Normal file
141
01-frontend/src/pages/Product.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||||
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
Grid,
|
||||
Rating,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import Item from '../components/Item';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useBasket } from '../helper/BasketProvider';
|
||||
|
||||
|
||||
export default function Product() {
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const location = useLocation();
|
||||
const item = location.state?.item as Item;
|
||||
const { addToBasket } = useBasket();
|
||||
|
||||
if (!item) {
|
||||
return <div>Item not found</div>;
|
||||
}
|
||||
|
||||
|
||||
const handleAddToCart = () => {
|
||||
addToBasket(item.id, quantity);
|
||||
console.log(`Added ${quantity} of ${item.name} to basket`);
|
||||
};
|
||||
|
||||
const discountedPrice = item.price * (1 - item.discount / 100);
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||
<Grid container spacing={4}>
|
||||
{/* Left Column - Image */}
|
||||
|
||||
<Card elevation={2} sx={{ width: '100%', maxWidth: 400 }}>
|
||||
<Box
|
||||
component="img"
|
||||
src="/src/assets/HTW.jpg"
|
||||
alt={item.name}
|
||||
sx={{ width: '100%', height: 'auto' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Right Column - Product Details */}
|
||||
|
||||
<Stack spacing={3}>
|
||||
<Typography variant="h4" component="h1">
|
||||
{item.name}
|
||||
</Typography>
|
||||
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Rating value={item.rating} precision={0.5} readOnly />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
({item.rating} / 5)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Stack direction="row" alignItems="center" spacing={2}>
|
||||
{item.discount > 0 ? (
|
||||
<>
|
||||
<Typography variant="h4" color="primary">
|
||||
${discountedPrice.toFixed(2)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
color="text.secondary"
|
||||
sx={{ textDecoration: 'line-through' }}
|
||||
>
|
||||
${item.price.toFixed(2)}
|
||||
</Typography>
|
||||
<Typography variant="h6" color="error">
|
||||
-{item.discount}%
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="h4" color="primary">
|
||||
${item.price.toFixed(2)}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Typography variant="body1">
|
||||
{item.description}
|
||||
</Typography>
|
||||
|
||||
<Box>
|
||||
{item.stock > 0 ? (
|
||||
<Alert severity="success">
|
||||
In Stock ({item.stock} available)
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert severity="error">Out of Stock</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<TextField
|
||||
type="number"
|
||||
label="Quantity"
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(Math.max(1, parseInt(e.target.value)))}
|
||||
InputProps={{ inputProps: { min: 1, max: item.stock } }}
|
||||
sx={{ width: 100 }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<ShoppingCartIcon />}
|
||||
onClick={handleAddToCart}
|
||||
disabled={item.stock === 0}
|
||||
fullWidth
|
||||
>
|
||||
Add to Cart
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<LocalShippingIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Free shipping on orders over $50
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -46,5 +46,20 @@ cardgrid {
|
||||
display: grid;
|
||||
gap: 2%;
|
||||
max-width: 3%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.page-background {
|
||||
background: linear-gradient(135deg, #ece9e6, #ffffff);
|
||||
min-height: 100vh; /* Damit der Hintergrund die gesamte Seite abdeckt */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cardgrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr); /* 3 Spalten */
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
Reference in New Issue
Block a user