Skip to content

Commit

Permalink
Merge pull request #715 from IkkiOcean/agro-backend-route-1
Browse files Browse the repository at this point in the history
[FEAT] Integrated Cart Backend with Cart Page, Category Page and Product Page
  • Loading branch information
manikumarreddyu authored Oct 28, 2024
2 parents 50afb6f + 11aa886 commit bd14152
Show file tree
Hide file tree
Showing 13 changed files with 457 additions and 204 deletions.
2 changes: 1 addition & 1 deletion backend/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ SMTP_EMAIL=
# 4. Click Generate, and you’ll receive a 16-character password. This is your SMTP passkey.
# )
SMTP_PASSWORD=
JWT_SECRET=mugundh
JWT_SECRET=
116 changes: 116 additions & 0 deletions backend/controllers/cartController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const ExtendedUser = require('../model/shop/extendedUser');

// Add product to cart
exports.addProductToCart = async (req, res) => {
const { userId } = req.params;
const { productId,variantId, quantity} = req.body;

try {
let user = await ExtendedUser.findOne({ _id: userId });
if (!user) {
user = new ExtendedUser({ _id: userId });
}

const existingItemIndex = user.cart.findIndex(item => item.variantId === variantId);
if (existingItemIndex !== -1) {
user.cart[existingItemIndex].quantity += quantity;
} else {
user.cart.push({ productId,variantId, quantity });
}

await user.save();
res.status(200).json({ message: 'Product added to cart', cart: user.cart });
} catch (error) {
res.status(500).json({ error: 'Failed to add product to cart' });
}
};

// Get user's cart
exports.getUserCart = async (req, res) => {
const { userId } = req.params;

try {
// Fetch the user and populate the cart with variant and product data
const user = await ExtendedUser.findById(userId)
.populate({
path: 'cart', // Populate the cart
populate: [
{
path: 'variantId', // Populate the variant inside each cart item
model: 'Variant', // Replace 'Variant' with the actual model name for variants
},
{
path: 'productId', // Populate the productId inside each cart item
model: 'Product', // Replace 'Product' with the actual model name for products
},
],
});

// Check if user exists
if (!user) return res.status(404).json({ error: 'User not found' });

// Return the cart directly
res.status(200).json({ cart: user.cart });
} catch (error) {
console.error(error); // Log the error for debugging
res.status(500).json({ error: 'Failed to fetch cart' });
}
};



// Update cart item quantity
exports.updateCartItemQuantity = async (req, res) => {
const { userId } = req.params;
const { productId, variantId,quantity } = req.body;

try {
const user = await ExtendedUser.findById(userId);
if (!user) return res.status(404).json({ error: 'User not found' });

const item = user.cart.find(item => item.variantId === variantId);
if (!item) return res.status(404).json({ error: 'Product not found in cart' });

item.quantity = quantity;
await user.save();

res.status(200).json({ message: 'Cart updated successfully', cart: user.cart });
} catch (error) {
res.status(500).json({ error: 'Failed to update cart' });
}
};

// Remove product from cart
exports.removeProductFromCart = async (req, res) => {
const { userId } = req.params;
const { productId, variantId } = req.body;

try {
const user = await ExtendedUser.findById(userId);
if (!user) return res.status(404).json({ error: 'User not found' });

user.cart = user.cart.filter(item => item.variantId !== variantId);
await user.save();

res.status(200).json({ message: 'Product removed from cart', cart: user.cart });
} catch (error) {
res.status(500).json({ error: 'Failed to remove product from cart' });
}
};

// Clear user's cart
exports.clearUserCart = async (req, res) => {
const { userId } = req.params;

try {
const user = await ExtendedUser.findById(userId);
if (!user) return res.status(404).json({ error: 'User not found' });

user.cart = [];
await user.save();

res.status(200).json({ message: 'Cart cleared successfully' });
} catch (error) {
res.status(500).json({ error: 'Failed to clear cart' });
}
};
2 changes: 1 addition & 1 deletion backend/model/shop/extendedUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ const addressSchema = new mongoose.Schema({
// Define the cart item schema
const cartItemSchema = new mongoose.Schema({
productId: { type: String, required: true },
variantId : { type: String, required: true },
quantity: { type: Number, required: true, min: 1 },
price: { type: Number, required: true },
addedAt: { type: Date, default: Date.now },
});

Expand Down
24 changes: 22 additions & 2 deletions backend/routes/shop.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const brandController = require('../controllers/brandController');
const sellerController = require('../controllers/sellerController');
const reviewController = require('../controllers/reviewController');
const variantController = require('../controllers/variantController');

const cartController = require('../controllers/cartController')
/**
* Product Routes
*/
Expand Down Expand Up @@ -66,4 +66,24 @@ router.post('/variants', variantController.createVariant);
router.put('/variants/:id', variantController.updateVariant);
router.delete('/variants/:id', variantController.deleteVariant);

module.exports = router;

/**
* Cart Routes
*/

// Route to add product to cart
router.post('/cart/:userId/add', cartController.addProductToCart);

// Route to get user's cart
router.get('/cart/:userId', cartController.getUserCart);

// Route to update cart item quantity
router.put('/cart/:userId/update', cartController.updateCartItemQuantity);

// Route to remove product from cart
router.delete('/cart/:userId/remove', cartController.removeProductFromCart);

// Route to clear user's cart
router.delete('/cart/:userId/clear', cartController.clearUserCart);

module.exports = router;
2 changes: 2 additions & 0 deletions frontend/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
REACT_APP_MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiYW5hbmRoYSIsImEiOiJjbTIwN29haWEwYzVrMmpzZ25yeTF4MmN4In0.3fHnwKMxxXNy9pM-Vcn9gw'
VITE_BACKEND_BASE_URL = https://agrotech-ai-11j3.onrender.com/
5 changes: 3 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fortawesome/fontawesome-svg-core": "^6.x.x",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@headlessui/react": "^2.1.10",
Expand Down Expand Up @@ -46,8 +48,7 @@
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
"three": "^0.169.0",
"tsparticles": "^3.5.0",
"@fortawesome/fontawesome-svg-core": "^6.x.x"
"tsparticles": "^3.5.0"
},
"devDependencies": {
"@types/react": "^18.3.3",
Expand Down
82 changes: 82 additions & 0 deletions frontend/src/AgroShopAI/components/CartComponents.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons';

const CartItem = ({ index, image, title, description,size, type,color, qty, price, originalPrice, updateQuantity, deleteItem }) => (
<div className="flex justify-between items-start mb-4 border-b border-gray-200 pb-4">
<img alt={title} className="w-24 h-24" src={image} />
<div className="flex-1 ml-4">
<h2 className="font-bold">{title}</h2>
<p>{description}</p>

<p>SIZE: {size} {type ? "Kg" : "L"}</p>
<p>COLOR: {color}</p>
<p>QTY:
<button onClick={() => updateQuantity(index, Math.max(1, qty - 1))} className="text-red-500 mx-2">-</button>
<span className="mx-2">{qty}</span>
<button onClick={() => updateQuantity(index, qty + 1)} className="text-green-500 mx-2">+</button>

</p>
<div className="flex space-x-4 text-gray-500">
<button onClick={() => deleteItem(index)} className="hover:text-red-500">
<FontAwesomeIcon icon={faTrashAlt} /> {/* Trash Can Icon */}
</button>
</div>
</div>
<div className="flex flex-col items-end text-right w-24"> {/* Width set for alignment */}
{originalPrice && (
<p className="line-through text-gray-400">{originalPrice.toFixed(2)}</p>
)}
<p className="font-bold">{price.toFixed(2)}</p>
</div>
<div className="flex flex-col items-end text-right w-24"> {/* Width set for alignment */}
<p className="font-bold">{(price * qty).toFixed(2)}</p> {/* Calculate total */}
</div>
</div>
);

const PromoCodeInput = () => (
<div className="mb-4">
<label className="block text-gray-500 mb-2" htmlFor="promo-code">
Do you have a promo code?
</label>
<div className="flex flex-wrap">
<input
className="border border-gray-300 p-2 flex-1 min-w-0" // Added 'min-w-0' to prevent overflow
id="promo-code"
placeholder="Enter code"
type="text"
/>
<button className="bg-black text-white p-2 ml-2">
APPLY
</button>
</div>
</div>
);

const OrderSummary = ({ subtotal }) => (
<div className="border-t border-gray-200 pt-4">
<div className="flex justify-between mb-2">
<span>SUBTOTAL</span>
<span className="font-bold">{subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between mb-2">
<span>Shipping</span>
<span className="text-gray-500">TBD</span>
</div>
<div className="flex justify-between mb-4">
<span>
Sales Tax
<i className="fas fa-info-circle"></i>
</span>
<span className="text-gray-500">TBD</span>
</div>
<div className="flex justify-between mb-4">
<span className="font-bold">ESTIMATED TOTAL</span>
<span className="font-bold">{subtotal.toFixed(2)}</span> {/* You can calculate the total here as well */}
</div>
<button className="bg-black text-white w-full py-2">CHECKOUT</button>
<p className="text-gray-500 text-sm mt-4">Need help? Call us at 1-877-707-6272</p>
</div>
);

export {CartItem, OrderSummary, PromoCodeInput};
26 changes: 26 additions & 0 deletions frontend/src/AgroShopAI/components/LoginPrompt.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';

const LoginPrompt = () => {
const navigate = useNavigate();

const handleRedirect = () => {
navigate('/login'); // Adjust the path as per your routing setup
};

return (
<div className="bg-gray-800 font-sans min-h-screen flex flex-col items-center justify-center">
<p className="text-green-500 text-2xl font-semibold mb-4">
Please log in to view your cart.
</p>
<button
onClick={handleRedirect}
className="bg-green-500 hover:bg-green-600 text-white py-2 px-6 rounded-lg transition duration-300"
>
Go to Login
</button>
</div>
);
};

export default LoginPrompt;
41 changes: 37 additions & 4 deletions frontend/src/AgroShopAI/components/ProductCard.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
import React from 'react';
import { Link } from 'react-router-dom'
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import LoginPrompt from './LoginPrompt';
const ProductCard = ({ item }) => {
console.log(item)
const {isLoggedIn, userData} = useAuth();
const salePrice = item.variant.price * (1 - (item.offer / 100));
const savings = item.variant.price - salePrice;
const navigate = useNavigate();
const handleCart = ()=>{
console.log("cart")
navigate('/agroshop/cart')
const handleCart = async()=>{
if(!isLoggedIn){
return (
<LoginPrompt/>
)
}
try {
const response = await fetch(`${import.meta.env.VITE_BACKEND_BASE_URL}api/cart/${userData}/add`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
productId: item._id,
variantId: item.variant._id,
quantity: 1,
}),
});

const result = await response.json();

if (response.ok) {
console.log(result.message);
navigate('/agroshop/cart')
} else {
alert(result.message || "Failed to add item to cart.");
}
} catch (error) {
console.error("Error adding to cart:", error);
}
}
return (
<div
Expand Down Expand Up @@ -40,6 +69,10 @@ const ProductCard = ({ item }) => {
{item.name.length > 25 ? `${item.name.substring(0, 25)}...` : item.name}
</Link>
<Link className="block text-gray-400 text-xs hover:underline cursor-pointer hover:text-blue-500">{item.brand.name.length > 25 ? `${item.brand.name.substring(0, 35)}...` : item.brand.name}</Link>
<div className="flex items-center">
<p className="text-black text-sm display-inline mr-2">Size :</p>
<p className="text-gray-400 text-sm display-inline">{item.variant.size} {item.variant.type? 'Kg' : 'L'} </p>
</div>
<div className="flex items-center">
<p className="text-black text-m display-inline mr-2">{salePrice.toFixed(2)}</p>
<p className="text-gray-400 text-m display-inline line-through">{item.variant.price}</p>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/AgroShopAI/components/ReviewSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const ReviewSection = ({ product_id,reviews, setReviews }) => {
// Check if review text, rating, and user ID are valid
if (reviewText.trim() && rating > 0 && userId.trim()) {
try {
const response = await fetch('http://127.0.0.1:8080/api/reviews', {
const response = await fetch(`${import.meta.env.VITE_BACKEND_BASE_URL}api/reviews`, {
method: 'POST',
headers: {
'Content-Type': 'application/json', // Set the content type to JSON
Expand Down
Loading

0 comments on commit bd14152

Please sign in to comment.