Added Product Page and Basket.

This commit is contained in:
FlorianSpeicher
2025-05-03 19:35:50 +02:00
parent 3f7e73487c
commit abed3422f5
10 changed files with 495 additions and 14 deletions

View File

@@ -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>
)
}

View 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;
};

View File

@@ -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>

View 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;
}

View File

@@ -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 }}>

View File

View 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>
);
}
}

View 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>
);
}

View 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>
);
};

View File

@@ -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;
}