Added snackbars on product page. Refactored folder structure

This commit is contained in:
FlorianSpeicher
2025-05-22 18:15:01 +02:00
parent 6d571094ff
commit fd70f26809
12 changed files with 280 additions and 196 deletions

View File

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

View File

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

View File

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

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

View File

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

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

View File

@@ -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() {

View File

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

View File

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