Added snackbars on product page. Refactored folder structure
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import './App.css';
|
||||
import NavBar from './helper/NavBar';
|
||||
import NavBar from './helper/navbar/NavBar';
|
||||
import Home from './pages/Home';
|
||||
import NoPage from './pages/NoPage';
|
||||
import Product from './pages/Product';
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { Box, Button, Divider, Rating, TextField, Typography } from "@mui/material";
|
||||
import RatingCard from "./RatingCard";
|
||||
import RatingType from "../components/Rating";
|
||||
|
||||
export default function Ratings() {
|
||||
|
||||
const ratings: RatingType[] = [
|
||||
{
|
||||
id: "1",
|
||||
rating: 4.5,
|
||||
text: "Great product!",
|
||||
date: "2023-10-01",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
rating: 3.0,
|
||||
text: "Average quality.",
|
||||
date: "2023-10-02",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
rating: 5.0,
|
||||
text: "Excellent value for money!",
|
||||
date: "2023-10-03",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='contact-divider-box'>
|
||||
<Divider className='contact-divider' />
|
||||
</div>
|
||||
<div className="rating-card-body">
|
||||
<Typography variant="h5">
|
||||
Rate this product:</Typography>
|
||||
<Rating name="half-rating" defaultValue={2.5} precision={0.5}/>
|
||||
<TextField
|
||||
label="Rating text (optional)"
|
||||
multiline
|
||||
minRows={4}
|
||||
maxRows={16}
|
||||
className="rating-text-field"
|
||||
/>
|
||||
<Button variant="contained" color="primary" className="rating-button">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
<div className='contact-divider-box'>
|
||||
<Divider className='contact-divider' />
|
||||
</div>
|
||||
<Box className="rating-card-box">
|
||||
{ratings.map((ratingType: RatingType) => (
|
||||
<RatingCard key={ratingType.id} {...ratingType} />
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
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";
|
||||
import "./helper.css";
|
||||
import Item from "../../components/Item";
|
||||
import { useBasket } from "../BasketProvider";
|
||||
import "../helper.css";
|
||||
|
||||
export default function ItemCard({ item }: { item: Item }) {
|
||||
const navigate = useNavigate()
|
||||
165
01-frontend/src/helper/productpage/ProductInfo.tsx
Normal file
165
01-frontend/src/helper/productpage/ProductInfo.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Alert, Box, Button, Card, Divider, Grid, IconButton, Rating, Snackbar, SnackbarCloseReason, Stack, TextField, Typography } from "@mui/material";
|
||||
import Item from "../../components/Item";
|
||||
import React, { useState } from "react";
|
||||
import { Close, LocalShipping, ShoppingCart } from "@mui/icons-material";
|
||||
import { useBasket } from "../BasketProvider";
|
||||
|
||||
export default function ProductInfo({ item }: { item: Item }) {
|
||||
const [quantity, setQuantity] = useState<number>(1);
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 });
|
||||
|
||||
|
||||
const { addToBasket } = useBasket();
|
||||
|
||||
const handleClose = (
|
||||
event: React.SyntheticEvent | Event,
|
||||
reason?: SnackbarCloseReason,
|
||||
) => {
|
||||
if (reason === 'clickaway') {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const action = (
|
||||
<React.Fragment>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="close"
|
||||
color="inherit"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
|
||||
const handleAddToCart = () => {
|
||||
addToBasket(item.id, quantity);
|
||||
setOpen(true);
|
||||
console.log(`Added {quantity} of €{item.name} to basket`);
|
||||
};
|
||||
|
||||
const discountedPrice = item.price * (1 - item.discount / 100);
|
||||
|
||||
const handleImageLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
const { naturalWidth, naturalHeight } = event.currentTarget;
|
||||
setImageDimensions({ width: naturalWidth, height: naturalHeight });
|
||||
};
|
||||
|
||||
return (
|
||||
<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}
|
||||
onLoad={handleImageLoad} // Event-Handler zum Ermitteln der Bildgröße
|
||||
sx={{
|
||||
maxWidth: imageDimensions.width > imageDimensions.height ? "100%" : "auto",
|
||||
maxHeight: imageDimensions.height >= imageDimensions.width ? 400 : "auto",
|
||||
width: "auto",
|
||||
height: "auto",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</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="green">
|
||||
€{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="green">
|
||||
€{item.price.toFixed(2)}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box>
|
||||
{item.stock > 10 ? (
|
||||
<Alert severity="success" variant='outlined'>
|
||||
In Stock ({item.stock} available)
|
||||
</Alert>
|
||||
) : item.stock > 0 ? (
|
||||
<Alert severity="warning" variant='outlined'>Almost sold out ({item.stock} available)</Alert>
|
||||
) : (
|
||||
<Alert severity="error" variant='filled'>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={<ShoppingCart />}
|
||||
onClick={handleAddToCart}
|
||||
disabled={item.stock <= 0}
|
||||
fullWidth
|
||||
>
|
||||
Add to Cart
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<LocalShipping sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Free shipping on orders over €50
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
</Stack>
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={3000}
|
||||
onClose={handleClose}
|
||||
message="Successfully added to basket"
|
||||
action={action}
|
||||
/>
|
||||
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card, CardActionArea, CardContent, Paper, Rating, Typography } from "@mui/material";
|
||||
import RatingType from "../components/Rating";
|
||||
import RatingType from "../../components/Rating";
|
||||
|
||||
export default function RatingCard(ratingType: RatingType) {
|
||||
|
||||
99
01-frontend/src/helper/productpage/Ratings.tsx
Normal file
99
01-frontend/src/helper/productpage/Ratings.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Box, Button, Divider, IconButton, Rating, Snackbar, SnackbarCloseReason, TextField, Typography } from "@mui/material";
|
||||
import RatingCard from "./RatingCard";
|
||||
import RatingType from "../../components/Rating";
|
||||
import React, { useState } from "react";
|
||||
import { Close } from "@mui/icons-material";
|
||||
|
||||
export default function Ratings() {
|
||||
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const handleClose = (
|
||||
event: React.SyntheticEvent | Event,
|
||||
reason?: SnackbarCloseReason,
|
||||
) => {
|
||||
if (reason === 'clickaway') {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleRatingSubmit = () => {
|
||||
// Handle the rating submission logic here
|
||||
setOpen(true);
|
||||
console.log("Rating submitted");
|
||||
}
|
||||
|
||||
const action = (
|
||||
<React.Fragment>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="close"
|
||||
color="inherit"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
const ratings: RatingType[] = [
|
||||
{
|
||||
id: "1",
|
||||
rating: 4.5,
|
||||
text: "Great product!",
|
||||
date: "2023-10-01",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
rating: 3.0,
|
||||
text: "Average quality.",
|
||||
date: "2023-10-02",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
rating: 5.0,
|
||||
text: "Excellent value for money!",
|
||||
date: "2023-10-03",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='contact-divider-box'>
|
||||
<Divider className='contact-divider' />
|
||||
</div>
|
||||
<div className="rating-card-body">
|
||||
<Typography variant="h5">
|
||||
Rate this product:</Typography>
|
||||
<Rating name="half-rating" defaultValue={2.5} precision={0.5} />
|
||||
<TextField
|
||||
label="Rating text (optional)"
|
||||
multiline
|
||||
minRows={4}
|
||||
maxRows={16}
|
||||
className="rating-text-field"
|
||||
/>
|
||||
<Button variant="contained" color="primary" className="rating-button" onClick={handleRatingSubmit}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
<div className='contact-divider-box'>
|
||||
<Divider className='contact-divider' />
|
||||
</div>
|
||||
<Box className="rating-card-box">
|
||||
{ratings.map((ratingType: RatingType) => (
|
||||
<RatingCard key={ratingType.id} {...ratingType} />
|
||||
))}
|
||||
</Box>
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={3000}
|
||||
onClose={handleClose}
|
||||
message="Thanks for your rating!"
|
||||
action={action}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Box, Pagination } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import Item from "../components/Item";
|
||||
import FilterItem from "../helper/FilterItem";
|
||||
import ItemCard from "../helper/ItemCard";
|
||||
import FilterItem from "../helper/homepage/FilterItem";
|
||||
import ItemCard from "../helper/homepage/ItemCard";
|
||||
import "./pages.css"; // Import der CSS-Datei
|
||||
|
||||
export default function Home() {
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
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,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import Item from '../components/Item';
|
||||
import { useBasket } from '../helper/BasketProvider';
|
||||
import Ratings from '../helper/Ratings';
|
||||
import ProductInfo from '../helper/productpage/ProductInfo';
|
||||
import Ratings from '../helper/productpage/Ratings';
|
||||
|
||||
|
||||
export default function Product() {
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 });
|
||||
const location = useLocation();
|
||||
const item = location.state?.item as Item;
|
||||
const { addToBasket } = useBasket();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleGoHome = () => {
|
||||
@@ -55,125 +43,10 @@ export default function Product() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const handleAddToCart = () => {
|
||||
addToBasket(item.id, quantity);
|
||||
console.log(`Added {quantity} of €{item.name} to basket`);
|
||||
};
|
||||
|
||||
const discountedPrice = item.price * (1 - item.discount / 100);
|
||||
|
||||
const handleImageLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
const { naturalWidth, naturalHeight } = event.currentTarget;
|
||||
setImageDimensions({ width: naturalWidth, height: naturalHeight });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='product-page-background'>
|
||||
<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}
|
||||
onLoad={handleImageLoad} // Event-Handler zum Ermitteln der Bildgröße
|
||||
sx={{
|
||||
maxWidth: imageDimensions.width > imageDimensions.height ? "100%" : "auto",
|
||||
maxHeight: imageDimensions.height >= imageDimensions.width ? 400 : "auto",
|
||||
width: "auto",
|
||||
height: "auto",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</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="green">
|
||||
€{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="green">
|
||||
€{item.price.toFixed(2)}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Box>
|
||||
{item.stock > 10 ? (
|
||||
<Alert severity="success" variant='outlined'>
|
||||
In Stock ({item.stock} available)
|
||||
</Alert>
|
||||
) : item.stock > 0 ? (
|
||||
<Alert severity="warning" variant='outlined'>Almost sold out ({item.stock} available)</Alert>
|
||||
) : (
|
||||
<Alert severity="error" variant='filled'>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>
|
||||
<ProductInfo item={item}/>
|
||||
<div className='contact-divider-box'>
|
||||
<Divider className='contact-divider' />
|
||||
</div>
|
||||
|
||||
@@ -128,6 +128,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: calc(100vh - 3rem); /* Damit der Hintergrund die gesamte Seite abdeckt */
|
||||
min-height: 600px;
|
||||
padding: 20px 0; /* Abstand oben und unten */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
|
||||
Reference in New Issue
Block a user