I was working on Product Page with its nested children. On the page we can see a specific product data, add item to the cart and set quantity which we want to add. The codebase is nasty as I only finished, but at least seems everything is working properly, so I'm looking for potential improvements including SOLID and other common patterns which I can use for ProductPage and QuantitySelector.
ProductPage:
export const ProductPage = () => {
const user = auth?.currentUser;
const theme = useTheme();
const { category, name } = useParams();
const { data, isLoading } = useGetProductsQuery(category);
const [selectedQ, setSelectedQ] = useState<number>();
const [totalPrice, setTotalPrice] = useState<number>();
const dispatch = useDispatch();
const selectedProduct = useMemo(
() =>
Array.isArray(data?.data) &&
data.data.find((product: Product) => product.name === name),
[data, name]
);
const handleQuantityChange = (q: number, totalPrice: number) => {
setSelectedQ(q);
setTotalPrice(totalPrice);
};
const handleAddItemToCart = async () => {
if (user) {
const cartsRef = doc(db, "carts", user.uid);
const docSnap = await getDoc(cartsRef);
let cartItems = [];
if (docSnap.exists()) {
cartItems = docSnap.data().items || [];
}
const itemIndex = cartItems.findIndex(
(item: any) => item.name === selectedProduct.name
);
if (itemIndex !== -1) {
cartItems[itemIndex].selectedQuantity =
cartItems[itemIndex].selectedQuantity + selectedQ;
cartItems[itemIndex].totalPrice =
cartItems[itemIndex].totalPrice + totalPrice;
} else {
cartItems.push({
name: selectedProduct.name,
imgURL: selectedProduct.imgURL,
quantity: selectedProduct.quantity,
selectedQuantity: selectedQ,
totalPrice: totalPrice,
});
}
await updateDoc(cartsRef, { items: cartItems });
dispatch(
setNotification({
open: true,
title: `${selectedProduct.name} added to cart`,
color: theme.palette.primary.main,
})
);
setTimeout(
() =>
dispatch(
setNotification({
open: false,
})
),
2000
);
}
};
return (
<>
{!isLoading ? (
<>
<Notification />
<Box
display="flex"
flexDirection={{ xs: "column", sm: "column", md: "row" }}
justifyContent="space-between"
marginTop={10}
padding={{
xs: "0 16px 48px 16px",
sm: "0 24px 48px 24px",
md: "0 48px 48px 48px",
}}
>
<Box
width={{ xs: "100%", sm: "100%", md: "50%" }}
display="flex"
alignItems="center"
justifyContent="center"
>
<Box
component="img"
src={selectedProduct.imgURL}
alt={selectedProduct.name}
width={{ xs: "15rem", sm: "18rem", md: "20rem" }}
height={{ xs: "15rem", sm: "18rem", md: "20rem" }}
/>
</Box>
<Box
height="100%"
width={{ xs: "100%", sm: "100%", md: "50%" }}
display="flex"
flexDirection="column"
alignItems={{ xs: "flex-start", sm: "flex-start", md: "center" }}
justifyContent="center"
gap="2rem"
>
<Box
width="100%"
display="flex"
alignItems="center"
justifyContent="space-between"
>
<Typography variant="h1">{selectedProduct.name}</Typography>
<QuantitySelector
quantity={selectedProduct.quantity}
units={selectedProduct.units}
price={selectedProduct.price}
onQuantityChange={handleQuantityChange}
/>
</Box>
<Typography variant="subtitle1">
{selectedProduct.about}
</Typography>
<Box
width="100%"
display="flex"
alignItems="center"
justifyContent="space-between"
fontWeight={600}
>
<Box display="flex" alignItems="center" gap="1rem">
<span>⭐️</span>
<span>4.5</span>
</Box>
<Box display="flex" alignItems="center" gap="1rem">
<span>🔥</span>
<span>{selectedProduct.calories} Kcal</span>
</Box>
<Box display="flex" alignItems="center" gap="1rem">
<span>⏰</span>
<span>10-15 Min</span>
</Box>
</Box>
<Categories title="Related Items" />
<Button sx={{ width: "100%" }} onClick={handleAddItemToCart}>
Add To Cart
</Button>
</Box>
</Box>
</>
) : (
<PageLoader />
)}
</>
);
};
QuantitySelector:
import { useState, useEffect } from "react";
import { Box, Button, styled, useTheme } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "@mui/icons-material/Remove";
const SelectButton = styled(Button)(({ theme }) => ({
minWidth: "1.8rem",
maxWidth: "1.8rem",
height: "1.8rem",
borderRadius: "0.3rem",
backgroundColor: "transparent",
color: theme.palette.primary.main,
border: `1px solid ${theme.palette.primary.main}`,
transitionDuration: "0.2s",
"&:hover": {
backgroundColor: "rgba(46, 171, 92, 0.1)",
},
}));
type QuantitySelectorProps = {
units: string;
quantity: number;
price: number;
onQuantityChange: (q: number) => void;
};
export const QuantitySelector = ({
quantity,
units,
price,
onQuantityChange,
}: QuantitySelectorProps) => {
const theme = useTheme();
const [q, setQ] = useState(quantity);
const [totalPrice, setTotalPrice] = useState(price * (quantity / 1000));
useEffect(() => {
setTotalPrice((q / 1000) * price);
onQuantityChange(q, totalPrice);
}, [q, price, onQuantityChange]);
const handleFormatQ = (q: number) => {
if (units === "g-kg") {
return q && q >= 1000 ? `${(q / 1000).toFixed(1)}kg` : `${q}g`;
}
return `${q}pcs`;
};
const handleIncrementQ = () =>
units === "g-kg"
? setQ((prevQ) => prevQ + 200)
: setQ((prefQ) => prefQ + 1);
const handleDecrementQ = () => {
if (units === "g-kg" && q > 200) {
setQ((prevQ) => prevQ - 200);
} else if (units !== "g-kg" && q > 1) {
setQ((prevQ) => prevQ - 1);
}
};
return (
<Box display="flex" alignItems="center" justifyContent="center" gap="1rem">
<SelectButton onClick={handleIncrementQ}>
<AddIcon sx={{ fontSize: "1.2rem" }} />
</SelectButton>
<Box
width="3.5rem"
height="3rem"
color={theme.palette.primary.main}
bgcolor="rgba(46, 171, 92, 0.1)"
borderRadius="0.3rem"
display="flex"
alignItems="center"
justifyContent="center"
fontWeight={600}
>
{handleFormatQ(q)}
</Box>
<SelectButton onClick={handleDecrementQ}>
<RemoveIcon sx={{ fontSize: "1.2rem" }} />
</SelectButton>
</Box>
);
};