Compare commits

...

13 Commits

23 changed files with 2362 additions and 2047 deletions

View File

@@ -1,8 +1,8 @@
import Razorpay from "razorpay";
const razorpay = new Razorpay({
key_id: process.env.NEXT_RAZORPAY_CLIENT_ID,
key_secret: process.env.NEXT_RAZORPAY_SECRET_KEY,
key_id: "rzp_live_REdeGZclfm8KFo",
key_secret: "BXgwGPsYhxgUYsozCr2uoEZC",
});
export async function POST(req) {

View File

@@ -27,7 +27,7 @@ export const ProductContextProvider = ({ children }) => {
fetchCategory();
}, []);
const cartFn = async (variantId, designId, quantity) => {
const cartFn = async (variantId, designId, quantity, showToast = true) => {
try{
const response = await authAxios.post('/orders/cart/manage_item/',{
@@ -35,7 +35,9 @@ export const ProductContextProvider = ({ children }) => {
design: designId,
quantity: quantity
})
toast.success('Modified Cart Successfully!')
if (showToast) {
toast.success('Modified Cart Successfully!')
}
return response
}
catch(error){

View File

@@ -1,19 +1,21 @@
import AddToCardLeftSection from "@/components/products/AddToCardLeftSection";
import AddToCardRightSection from "@/components/products/AddToCardRightSection";
import RelatedProductCards from "@/components/products/RelatedProductCards";
import RelatedVideos from "@/components/products/RelatedVideos";
// import RelatedVideos from "@/components/products/RelatedVideos";
import React from "react";
const page = ({ params }) => {
const slug = params.name;
const productId = slug.split("-").pop();
return (
<div className="min-h-screen bg-white">
<div className="flex gap-3 min-h-screen sm:flex-row flex-col">
<AddToCardLeftSection productId={params.name} />
<AddToCardRightSection productId={params.name}/>
<AddToCardLeftSection productId={productId} />
<AddToCardRightSection productId={productId} />
</div>
<RelatedProductCards productId={params.name}/>
<RelatedProductCards productId={productId} />
{/* <RelatedVideos /> */}
</div>
);

View File

@@ -5,10 +5,12 @@ import React, { useState, useEffect } from "react";
import { backendUrl } from "@/utils/axios";
import axios from "axios";
import Image from "next/image";
import slugify from "slugify";
export default function SubcategoryList({ params }) {
console.log(params);
console.log(params.id);
//console.log("category params is", params);
//console.log("Category name:", params.name);
const [subcategories, setSubcategories] = useState([]);
const [category, setCategory] = useState(null);
const [loading, setLoading] = useState(true);
@@ -17,23 +19,29 @@ export default function SubcategoryList({ params }) {
useEffect(() => {
const fetchData = async () => {
try {
// Fetch category details
const categoryResponse = await axios.get(
`${backendUrl}/products/category/`
);
// Fetch all categories
const categoryResponse = await axios.get(`${backendUrl}/products/category/`);
// Find the category by name
const selectedCategory = categoryResponse.data.find(
(cat) => cat.id == params.id
(cat) => cat.category_name.toLowerCase() === decodeURIComponent(params.name).toLowerCase()
);
if (!selectedCategory) {
setError("Category not found.");
setLoading(false);
return;
}
setCategory(selectedCategory);
// Fetch subcategories for this category
// Fetch subcategories using category id
const subcategoriesResponse = await axios.get(
`${backendUrl}/admins/product/subcategories/?category_id=${params.id}`
`${backendUrl}/admins/product/subcategories/?category_id=${selectedCategory.id}`
);
setSubcategories(subcategoriesResponse.data);
console.log("Selected sub category")
console.log(subcategoriesResponse.data);
// console.log(sub)
console.log("Selected subcategories", subcategoriesResponse.data);
} catch (err) {
console.error("Error fetching data:", err);
setError("Failed to load subcategories. Please try again later.");
@@ -43,7 +51,7 @@ export default function SubcategoryList({ params }) {
};
fetchData();
}, [params.id]);
}, [params.name]);
if (loading) {
return <div className="py-16 text-center">Loading subcategories...</div>;
@@ -71,7 +79,7 @@ export default function SubcategoryList({ params }) {
key={subcategory.id}
className="bg-white shadow-lg rounded-lg overflow-hidden transition-transform transform hover:scale-105"
>
<Link href={`/category/${params.id}/subcategory/${subcategory.id}`}>
<Link href={`/category/${params.name}/subcategory/${slugify(subcategory.subcategory_name, { lower: true })}`}>
<div className="relative w-full h-64 bg-gray-200">
<Image
src={`${

View File

@@ -1,6 +1,6 @@
"use client";
import Image from "next/image";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useContext } from "react";
import {
Accordion,
AccordionContent,
@@ -10,10 +10,13 @@ import {
import Link from "next/link";
import axios from "axios";
import { backendUrl } from "@/utils/axios";
import slugify from "slugify";
import ProductContext from "@/app/contexts/productContext";
const DEFAULT_IMAGE = "/sidhi-mala/Designer_30.png"; // Default image
const DEFAULT_IMAGE = "/sidhi-mala/Designer_30.png";
const FaqCard = ({ params }) => {
const { products } = useContext(ProductContext); // ✅ get products
const [faq, setFaq] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -21,11 +24,46 @@ const FaqCard = ({ params }) => {
useEffect(() => {
const fetchFaq = async () => {
try {
const response = await axios.get(
`${backendUrl}/products/faq/${params.id}/`
if (!products || products.length === 0) {
console.log("❌ Products not loaded yet.");
return;
}
// 1⃣ Collect all unique subcategories from products
const subcategories = products.map((p) => p.product_subcategory);
// 2⃣ Find subcategory by slug
const matchedSub = subcategories.find(
(sub) =>
slugify(sub?.subcategory_name || "", { lower: true }) ===
params.subcategory
);
setFaq(response.data);
console.log("✅ Matched Subcategory for FAQ:", matchedSub);
if (!matchedSub) {
console.warn("⚠️ No subcategory found for slug:", params.subcategory);
setError("Subcategory not found");
setLoading(false);
return;
}
// 3⃣ Fetch FAQs using subcategory ID
const url = `${backendUrl}/products/faq/${matchedSub.category}/`;
console.log("📡 Fetching FAQ from:", url);
const faqRes = await axios.get(url);
console.log("📥 Full FAQ Response:", faqRes);
console.log("✅ FAQ Data:", faqRes.data);
setFaq(faqRes.data);
} catch (err) {
console.error("❌ FAQ Fetch Error:", err);
if (err.response) {
console.error("⚠️ Response Status:", err.response.status);
console.error("⚠️ Response Data:", err.response.data);
}
setError("Failed to fetch FAQs. Please try again later.");
} finally {
setLoading(false);
@@ -33,24 +71,19 @@ const FaqCard = ({ params }) => {
};
fetchFaq();
}, [params.id]);
}, [products, params.subcategory]);
if (loading) {
if (loading)
return <p className="text-center text-xl mt-10">Loading FAQs...</p>;
}
if (error) {
return <p className="text-center text-red-600 mt-10">{error}</p>;
}
if (!faq || faq.length === 0) {
if (error) return <p className="text-center text-red-600 mt-10">{error}</p>;
if (!faq || faq.length === 0)
return <p className="text-center text-xl mt-10">No FAQs available.</p>;
}
const hasImage = faq.some((item) => item.image !== null);
const imageToShow = hasImage
? `${backendUrl}${faq.find((item) => item.image !== null).image}`
: DEFAULT_IMAGE;
return (
<div className="min-h-screen sm:block hidden bg-[#FFFFFF]">
<h1 className="sm:pt-14 pb-8 font-serif mt-4 sm:text-6xl text-2xl text-zinc-800 text-center">

View File

@@ -24,9 +24,14 @@ const Navbar = () => {
SUPPORTED_CURRENCIES,
error,
setUserCountry,
userCountry
userCountry,
} = useCurrency();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
useEffect(() => {
@@ -101,7 +106,7 @@ const Navbar = () => {
const categoryItems = category
? category.map((category) => ({
label: category.category_name,
url: `/category/${category.id}`,
url: `/category/${category.category_name}`,
}))
: []; // Fallback to an empty array if category is undefined
@@ -118,12 +123,13 @@ const Navbar = () => {
<div className="container mx-auto flex justify-between items-center px-4 py-2">
<div className="flex items-center md:space-x-4">
<button onClick={toggleSidebar} className="z-50">
{isSidebarOpen ? (
{!mounted ? null : isSidebarOpen ? (
<IoMdClose size={28} />
) : (
<Menu size={28} strokeWidth={1} />
)}
</button>
<Link href="/">
<Image
src="/logo1.jpg"

View File

@@ -33,7 +33,7 @@ const HeroSix = ({ guranteeData, data }) => {
description2: description6,
}}
/>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-8 mt-4">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-8 mt-4">
{guranteeData.map((item, index) => (
<motion.div
key={item.id}

View File

@@ -1,11 +0,0 @@
import React from 'react'
const Instasection = () => {
return (
<div className='h-[70vh]'>
</div>
)
}
export default Instasection

View File

@@ -2,15 +2,12 @@ import React, { useState, useEffect } from "react";
import { PayPalButtons, PayPalScriptProvider } from "@paypal/react-paypal-js";
import axios from "axios";
import { useRazorpay } from "react-razorpay";
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
const EXCHANGE_RATE_KEY = "exchange_rate_cache";
import { useCurrency } from "@/app/contexts/currencyContext";
const PaymentComponent = ({ amount, onSuccess }) => {
const { error: razorpayError, isLoading, Razorpay } = useRazorpay();
const [error, setError] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
const [usdAmount, setUsdAmount] = useState(null);
const [showPopup, setShowPopup] = useState(false);
const [shippingData, setShippingData] = useState({
address_line_1: "",
@@ -19,50 +16,47 @@ const PaymentComponent = ({ amount, onSuccess }) => {
city: "",
country_code: "",
postal_code: "",
phone_number: "",
});
const [shippingInfoCollected, setShippingInfoCollected] = useState(false);
const { selectedCurrency, convertPrice, exchangeRates } = useCurrency();
const amountInSelectedCurrency = convertPrice(amount);
const [usdAmount, setUsdAmount] = useState(null);
useEffect(() => {
const fetchExchangeRate = async () => {
const calculateUsdAmount = async () => {
try {
const cachedData = localStorage.getItem(EXCHANGE_RATE_KEY);
if (cachedData) {
const { rate, timestamp } = JSON.parse(cachedData);
if (Date.now() - timestamp < CACHE_DURATION) {
setUsdAmount((amount * rate).toFixed(2));
return;
}
if (selectedCurrency === 'USD') {
setUsdAmount(amountInSelectedCurrency);
return;
}
const response = await axios.get("https://apilayer.net/api/live", {
params: {
access_key: "9bcb30907dee1cda9866f7b49f0f8def",
currencies: "USD",
source: "INR",
format: 1,
},
});
if (exchangeRates && exchangeRates['USD']) {
const usdRate = exchangeRates['USD'];
setUsdAmount((amountInSelectedCurrency / usdRate).toFixed(2));
return;
}
if (response.data.success) {
const rate = response.data.quotes.INRUSD;
localStorage.setItem(
EXCHANGE_RATE_KEY,
JSON.stringify({
rate,
timestamp: Date.now(),
})
);
setUsdAmount((amount * rate).toFixed(2));
const response = await axios.get('https://api.exchangerate-api.com/v4/latest/' + selectedCurrency);
if (response.data && response.data.rates && response.data.rates.USD) {
const usdRate = response.data.rates.USD;
setUsdAmount((amountInSelectedCurrency * usdRate).toFixed(2));
} else {
throw new Error("Failed to fetch exchange rate");
throw new Error("Failed to fetch USD exchange rate");
}
} catch (err) {
console.error("Exchange rate error:", err);
console.error("Error calculating USD amount:", err);
if (selectedCurrency === 'INR') {
setUsdAmount((amountInSelectedCurrency / 75).toFixed(2));
} else {
setUsdAmount(amountInSelectedCurrency);
}
}
};
fetchExchangeRate();
}, [amount]);
calculateUsdAmount();
}, [amountInSelectedCurrency, selectedCurrency, exchangeRates]);
const createOrder = async () => {
try {
@@ -89,7 +83,7 @@ const PaymentComponent = ({ amount, onSuccess }) => {
const order = await createOrder();
const options = {
key: "rzp_test_Xf4wnkvQQeUnCq",
key: "rzp_live_REdeGZclfm8KFo",
amount: order.amount,
currency: order.currency,
name: "Rudraksha",
@@ -98,7 +92,7 @@ const PaymentComponent = ({ amount, onSuccess }) => {
prefill: {
name: shippingData.address_line_1,
email: shippingData.address_line_2,
contact: shippingData.city,
contact: shippingData.phone_number || shippingData.city,
},
handler: (response) => {
response.address = shippingData;
@@ -115,24 +109,23 @@ const PaymentComponent = ({ amount, onSuccess }) => {
};
const handlePopupSubmit = () => {
if (!shippingData.address_line_1 || !shippingData.city || !shippingData.postal_code) {
if (!shippingData.address_line_1 || !shippingData.city || !shippingData.postal_code || !shippingData.phone_number) {
setError("Please fill in all required fields.");
return;
}
// Validate phone number
const phoneRegex = /^\+?[0-9]{10,15}$/;
if (!phoneRegex.test(shippingData.phone_number)) {
setError("Please enter a valid phone number (10-15 digits).");
return;
}
setShowPopup(false);
setShippingInfoCollected(true);
setError(null);
};
const validateShippingInfo = () => {
if (!shippingInfoCollected) {
setShowPopup(true);
return false;
}
return true;
};
return (
<div className="max-w-md mx-auto">
{error && (
@@ -172,49 +165,53 @@ const PaymentComponent = ({ amount, onSuccess }) => {
<PayPalScriptProvider
options={{
"client-id": process.env.NEXT_PUBLIC_CLIENT_ID,
currency: "USD", // PayPal requires USD or other supported currencies
}}
>
<PayPalButtons
style={{
layout: "horizontal",
label: "checkout",
tagline: false,
fundingicons: true,
}}
disabled={isProcessing || !shippingInfoCollected}
className="w-full"
createOrder={(data, actions) => {
return actions.order.create({
purchase_units: [
{
amount: {
value: usdAmount,
currency_code: "USD",
{usdAmount && (
<PayPalButtons
style={{
layout: "horizontal",
label: "checkout",
tagline: false,
fundingicons: true,
}}
disabled={isProcessing || !shippingInfoCollected}
className="w-full"
createOrder={(data, actions) => {
return actions.order.create({
purchase_units: [
{
amount: {
value: usdAmount,
currency_code: "USD",
},
},
],
application_context: {
shipping_preference: "GET_FROM_FILE",
},
],
application_context: {
shipping_preference: "GET_FROM_FILE",
},
});
}}
onApprove={async (data, actions) => {
try {
setIsProcessing(true);
const order = await actions.order.capture();
order.address = shippingData; // Add shipping data to PayPal order
handlePaymentSuccess(order);
} catch (err) {
});
}}
onApprove={async (data, actions) => {
try {
setIsProcessing(true);
const order = await actions.order.capture();
order.address = shippingData; // Add shipping data to PayPal order
handlePaymentSuccess(order);
} catch (err) {
setError("Payment failed. Please try again.");
console.error("Payment error:", err);
} finally {
setIsProcessing(false);
}
}}
onError={(err) => {
setError("Payment failed. Please try again.");
console.error("Payment error:", err);
} finally {
setIsProcessing(false);
}
}}
onError={(err) => {
setError("Payment failed. Please try again.");
}}
/>
console.error("PayPal error:", err);
}}
/>
)}
</PayPalScriptProvider>
<button
@@ -272,6 +269,13 @@ const PaymentComponent = ({ amount, onSuccess }) => {
onChange={(e) => setShippingData({ ...shippingData, postal_code: e.target.value })}
className="w-full p-2 mb-4 border border-gray-300 rounded"
/>
<input
type="tel"
placeholder="Phone Number *"
value={shippingData.phone_number}
onChange={(e) => setShippingData({ ...shippingData, phone_number: e.target.value })}
className="w-full p-2 mb-4 border border-gray-300 rounded"
/>
<div className="flex gap-2">
<button
onClick={() => setShowPopup(false)}

View File

@@ -1,5 +1,5 @@
"use client";
import React, { useContext, useState } from "react";
import React, { useContext, useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ChevronLeft, ChevronRight, Award } from "lucide-react"; // Added Award icon
@@ -9,8 +9,10 @@ import Image from "next/image";
const ProductGallery = ({ productId }) => {
const { products } = useContext(ProductContext);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [showZoom, setShowZoom] = useState(false);
const [zoomPosition, setZoomPosition] = useState({ x: 0, y: 0 });
const imageContainerRef = useRef(null);
const nextImage = () => {
setCurrentImageIndex((prevIndex) => (prevIndex + 1) % productImages.length);
@@ -22,6 +24,7 @@ const ProductGallery = ({ productId }) => {
(prevIndex - 1 + productImages.length) % productImages.length
);
};
const product = products?.find((pr) => pr.id == productId);
if (!product) {
return null;
@@ -30,6 +33,23 @@ const ProductGallery = ({ productId }) => {
const productImages = product?.images?.map((img) => img.image) || [];
const hasCertificate = product.certificate && product.certificate !== "NA";
const handleMouseMove = (e) => {
if (!imageContainerRef.current) return;
const { left, top, width, height } = imageContainerRef.current.getBoundingClientRect();
// Calculate position as percentage
const x = ((e.clientX - left) / width) * 100;
const y = ((e.clientY - top) / height) * 100;
setZoomPosition({ x, y });
setShowZoom(true);
};
const handleMouseLeave = () => {
setShowZoom(false);
};
return (
<div className="flex flex-col md:flex-row gap-4 p-4 sm:w-[70%] ">
<div className="hidden md:flex flex-col gap-2">
@@ -38,8 +58,8 @@ const ProductGallery = ({ productId }) => {
key={index}
src={`${backendUrl}${src}`}
alt={`Thumbnail ${index + 1}`}
width={160} // Width of w-40 (160px)
height={128} // Height of h-32 (128px)
width={160}
height={128}
className="object-cover cursor-pointer border border-gray-300 hover:border-blue-500"
onClick={() => setCurrentImageIndex(index)}
/>
@@ -55,14 +75,18 @@ const ProductGallery = ({ productId }) => {
</div>
)}
{/* Main image with carousel controls */}
<div className="relative mb-4 h-full">
<div
className="relative mb-4 h-full"
ref={imageContainerRef}
// onMouseMove={handleMouseMove}
// onMouseLeave={handleMouseLeave}
>
<Image
src={`${backendUrl}${productImages[currentImageIndex]}`}
alt="Main product"
width={500}
height={500}
className="object-cover w-full h-full"
className="object-cover w-full h-full cursor-zoom-in"
/>
<Button
className="absolute top-1/2 left-2 transform -translate-y-1/2 md:hidden"
@@ -78,6 +102,24 @@ const ProductGallery = ({ productId }) => {
>
<ChevronRight />
</Button>
{/* Zoom overlay */}
{showZoom && (
<div className="absolute top-0 right-0 w-40 h-40 md:w-60 md:h-60 lg:w-[300px] lg:h-[300px] border-2 border-gray-300 overflow-hidden z-20 bg-white shadow-lg">
<div
className="absolute w-[1000px] h-[1000px]"
style={{
backgroundImage: `url(${backendUrl}${productImages[currentImageIndex]})`,
backgroundPosition: `${zoomPosition.x}% ${zoomPosition.y}%`,
backgroundSize: '250%',
backgroundRepeat: 'no-repeat',
transform: 'translate(-50%, -50%)',
left: '50%',
top: '50%',
}}
/>
</div>
)}
</div>
</Card>
<div className="my-6">
@@ -101,6 +143,13 @@ const ProductGallery = ({ productId }) => {
<h2 className="my-1">100% Secured Payment</h2>
<h2>Authenticity Guaranteed</h2>
</div>
<div className="mt-8 border-t pt-6 hidden md:block">
<h2 className="text-xl font-semibold mb-4">Beej Mantra</h2>
<div
dangerouslySetInnerHTML={{ __html: product.beej_mantra }}
className="text-sm prose prose-sm max-w-none"
/>
</div>
</div>
</div>
);

View File

@@ -221,6 +221,13 @@ function ProductDetails({ productId }) {
className="text-sm prose prose-sm max-w-none"
/>
</div>
<div className="mt-8 border-t pt-6 block md:hidden">
<h2 className="text-xl font-semibold mb-4">Beej Mantra</h2>
<div
dangerouslySetInnerHTML={{ __html: product.beej_mantra }}
className="text-sm prose prose-sm max-w-none"
/>
</div>
</div>
{showCertificate && !product.certificate.toLowerCase().endsWith('.pdf') && (

View File

@@ -30,12 +30,12 @@ const RelatedProductCards = ({ productId }) => {
You may also like
</h1>
</div>
<div className="overflow-x-auto hide-navbar gap-5 flex items-center justify-center">
<div className="overflow-x-auto hide-navbar flex items-center justify-start px-4 pb-4 -mx-4 snap-x">
{relatedProducts.map((product) => (
<Link href={`/products/${product.id}`} key={product.id}>
<div
key={product.id}
className="relative bg-[#EDE8E0] overflow-hidden sm:h-[350px] sm:w-[300px] "
className="relative bg-[#EDE8E0] overflow-hidden sm:h-[350px] sm:w-[300px] flex-shrink-0 mx-3 snap-center"
>
<div className="absolute top-0 left-0 bg-[#C19A5B] text-white py-1 px-3 rounded-br-lg z-10">
Exclusive
@@ -49,8 +49,8 @@ const RelatedProductCards = ({ productId }) => {
className="w-full h-full object-cover hover:scale-125 transition-transform ease-in-out duration-300"
/>
</div>
<div className="p-4 ">
<h3 className="text-center text-[1rem] uppercase text-gray-800">
<div className="p-4">
<h3 className="text-center text-[1rem] uppercase text-gray-800">
{product.product_name}
</h3>
</div>

View File

@@ -75,13 +75,12 @@ const ShippingPolicy = () => {
experience with us.
</p>
<h2 className="text-2xl font-semibold mb-4 underline">
Delivery Services Used:
Delivery Services Used : Any Courier and Post Office used
</h2>
<p className="text-lg mt-6">
Note: Gupta Rudraksha only uses the expedited shipping options and
ensures the fastest delivery using any of the above courier services
based on their estimated delivery date. BlueDart is used for
deliveries all over India.
ensures the fastest delivery using any courier services
based on their estimated delivery date.
</p>
</div>
</div>

View File

@@ -1,3 +1,4 @@
"use client";
import React, { useContext, useEffect, useState } from "react";
import { Trash2, Plus } from "lucide-react";
@@ -22,26 +23,33 @@ const ShoppingCart = () => {
const { formatPrice } = useCurrency();
const getCart = async () => {
token = localStorage.getItem("token")
if (token == null) {
setCartItems([])
} else {
const token = localStorage.getItem("token");
if (!token) {
console.log("User not logged in, setting empty cart");
setCartItems([]);
return;
}
try {
const response = await authAxios.get("/orders/cart/");
setCartItems(response.data);
} catch (error) {
console.error("Error fetching cart:", error);
setCartItems([]);
}
};
useEffect(() => {
getCart();
}, []);
const handleQuantityChange = async (variantId, designId, quantityChange) => {
const response = await cartFn(variantId, designId, quantityChange);
const handleQuantityChange = async (variantId, designId, quantityChange, showToast = true) => {
const response = await cartFn(variantId, designId, quantityChange, showToast);
console.log(response)
if (response?.status == 200 || response?.status == 204) {
setCartItems((prev) => {
if (!prev) return prev;
const updatedItems = prev[0].items.map((item) => {
if (item.variant.id === variantId && item?.design?.id === designId) {
@@ -79,6 +87,15 @@ const ShoppingCart = () => {
}
};
const clearCart = async () => {
if (!cartItems || !cartItems[0]?.items?.length) return;
for (const item of cartItems[0].items) {
await handleQuantityChange(item.variant.id, item.design?.id, -item.quantity, false);
}
toast.success('Cart cleared successfully!');
};
if (!cartItems) {
return null;
}
@@ -116,6 +133,7 @@ const ShoppingCart = () => {
const paymentData = {
paymentId: order.id,
payerEmail: order.payer.email_address,
orderId: order.id,
cartId: cartItems[0].id,
address: order.address,
};
@@ -130,14 +148,6 @@ const ShoppingCart = () => {
}
};
if (!cartItems) {
return null;
}
if (!cartItems[0]?.items?.length) {
return <EmptyCart />;
}
return (
<div className="w-full bg-white">
<div className="w-full px-4 md:px-8 max-w-[1600px] mx-auto font-['Inter']">
@@ -224,8 +234,8 @@ const ShoppingCart = () => {
size={18}
onClick={() =>
handleQuantityChange(
item.variant.id,
item.design.id,
item?.variant?.id,
item?.design?.id,
-item.quantity
)
}
@@ -242,7 +252,10 @@ const ShoppingCart = () => {
<button className="text-[#c19a5b] hover:text-[#ab885b] font-medium order-2 sm:order-1">
<Link href="/"> CONTINUE SHOPPING</Link>
</button>
<button className="text-[#c19a5b] hover:text-[#ab885b] sm:ml-auto font-medium order-1 sm:order-2">
<button
onClick={clearCart}
className="text-[#c19a5b] hover:text-[#ab885b] sm:ml-auto font-medium order-1 sm:order-2"
>
CLEAR SHOPPING CART
</button>
</div>

View File

@@ -6,6 +6,7 @@ import { backendUrl } from "@/utils/axios";
import Image from "next/image";
import { useCurrency } from "@/app/contexts/currencyContext";
import ProductContext from "@/app/contexts/productContext";
import slugify from "slugify";
export default function ExploreSiddhaMala({ params }) {
const { products, category } = useContext(ProductContext);
@@ -24,44 +25,54 @@ export default function ExploreSiddhaMala({ params }) {
{selectedCategory ? selectedCategory.category_name : "Products"}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8">
{filteredProducts?.map((product) => (
<div
key={product.id}
className="bg-white shadow-lg rounded-lg overflow-hidden transition-transform transform hover:scale-105"
>
<Link href={`/products/${product.id}`}>
<div className="relative w-full h-64 bg-gray-200">
<Image
src={`${backendUrl}${product?.images[0]?.image}`}
alt={product.product_name}
layout="fill"
objectFit="cover"
className="rounded-t-lg"
/>
{filteredProducts?.map((product) => {
// ✅ generate slug for debugging
const slug = `${slugify(product.product_name, { lower: true })}-${
product.id
}`;
console.log("Generated slug for product:", slug);
return (
<div
key={product.id}
className="bg-white shadow-lg rounded-lg overflow-hidden transition-transform transform hover:scale-105"
>
<Link href={`/products/${slug}`}>
<div className="relative w-full h-64 bg-gray-200">
<Image
src={`${backendUrl}${product?.images[0]?.image}`}
alt={product.product_name}
layout="fill"
objectFit="cover"
className="rounded-t-lg"
/>
</div>
</Link>
<div className="p-5">
<h3 className="text-lg font-semibold text-gray-900 mb-2 truncate">
{product.product_name}
</h3>
{product.variants?.length > 0 ? (
<p className="text-lg font-bold text-green-600">
{isLoading
? "Loading..."
: formatPrice(
Math.min(
...product.variants.map(
(variant) => variant.price
)
)
)}
</p>
) : (
<p className="text-md font-medium text-gray-500">
Price on request
</p>
)}
</div>
</Link>
<div className="p-5">
<h3 className="text-lg font-semibold text-gray-900 mb-2 truncate">
{product.product_name}
</h3>
{product.variants?.length > 0 ? (
<p className="text-lg font-bold text-green-600">
{isLoading
? "Loading..."
: formatPrice(
Math.min(
...product.variants.map((variant) => variant.price)
)
)}
</p>
) : (
<p className="text-md font-medium text-gray-500">
Price on request
</p>
)}
</div>
</div>
))}
);
})}
</div>
</div>
</section>

View File

@@ -14,7 +14,8 @@ const cleanHTML = (html) => {
const CategoryHero = ({ params }) => {
const { category } = useContext(ProductContext);
const selectedCategory = category?.find((cat) => cat.id == params.id);
const selectedCategory = category?.find((cat) => cat.category_name.toLowerCase() ===
decodeURIComponent(params.name).toLowerCase());
console.log(selectedCategory);
@@ -25,43 +26,46 @@ const CategoryHero = ({ params }) => {
const fallbackCategoryName = "Spiritual Essentials";
return (
<section className="relative h-[50vh] md:h-[80vh] flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src={`${
selectedCategory?.image
? backendUrl + selectedCategory?.image
: "/sidhi-mala/Artboard_1_bf5ccd46-7152-4355-82a8-9e9f27c1bfc2.jpg"
}`}
alt="Category Background"
layout="fill"
objectFit="cover"
quality={100}
priority
/>
</div>
<section className="relative min-h-[50vh] md:h-[80vh] py-8 md:py-0 flex items-center justify-center">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
<div className="relative h-[250px] md:h-[500px] overflow-hidden rounded-lg shadow-xl">
<Image
src={`${
selectedCategory?.image
? backendUrl + selectedCategory?.image
: "/sidhi-mala/Artboard_1_bf5ccd46-7152-4355-82a8-9e9f27c1bfc2.jpg"
}`}
alt="Category Background"
layout="fill"
objectFit="cover"
quality={100}
priority
/>
</div>
<div className="text-start flex justify-start flex-col h-full">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-2xl md:text-3xl font-bold text-black mb-4"
>
{selectedCategory?.category_name || fallbackCategoryName}
</motion.h1>
<div className="relative z-10 text-center px-6 md:px-12 max-w-3xl">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-4xl md:text-6xl font-bold text-white mb-4 drop-shadow-lg"
>
{selectedCategory?.category_name || fallbackCategoryName}
</motion.h1>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="text-lg md:text-xl text-gray-200 drop-shadow-md"
dangerouslySetInnerHTML={{
__html: cleanHTML(
selectedCategory?.descriptions || fallbackDescription
),
}}
/>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="text-black text-sm md:text-base"
dangerouslySetInnerHTML={{
__html: cleanHTML(
selectedCategory?.descriptions || fallbackDescription
),
}}
/>
</div>
</div>
</div>
</section>
);

View File

@@ -23,9 +23,13 @@ const EnhancedSlider = () => {
category: cat,
products: products
?.filter((product) => product.product_category?.id === cat.id)
.slice(0, 3),
.slice(0, 6),
}));
const trimText = (text = "", max) =>
text.length > max ? text.slice(0, max) + "…" : text;
return (
<section className="bg-gradient-to-b from-background to-secondary/10 py-16 md:py-24">
<div className="container mx-auto px-4">
@@ -69,9 +73,9 @@ const EnhancedSlider = () => {
<Link href={`/products/${product.id}`}>
<motion.div
whileHover={{ y: -5 }}
className="group rounded-lg overflow-hidden bg-card shadow-lg transition-shadow hover:shadow-xl"
className="group rounded-lg overflow-hidden bg-card shadow-lg transition-shadow hover:shadow-xl h-80"
>
<div className="aspect-square relative overflow-hidden max-h-[50%]">
<div className="aspect-square relative overflow-hidden max-h-[50%] w-full">
<Image
src={`${backendUrl}${
product.images[0]?.image || "/placeholder.jpg"
@@ -84,11 +88,14 @@ const EnhancedSlider = () => {
</div>
<div className="p-4">
<h3 className="text-lg font-semibold text-foreground group-hover:text-primary transition-colors">
{product.product_name}
{trimText(product.product_name, 30)}
</h3>
<p className="text-sm text-muted-foreground mt-2">
{product.short_description ||
"Discover more about this product"}
{trimText(
product.short_description ||
"Discover more about this product",
60
)}
</p>
</div>
</motion.div>

View File

@@ -6,17 +6,27 @@ import { backendUrl } from "@/utils/axios";
import Image from "next/image";
import { useCurrency } from "@/app/contexts/currencyContext";
import ProductContext from "@/app/contexts/productContext";
import slugify from "slugify";
export default function ExploreSiddhaMala({ params }) {
const { products, category } = useContext(ProductContext);
console.log("Products ", products)
console.log("URL param:", params.subcategory);
//console.log("Products ", products);
const { formatPrice, isLoading } = useCurrency();
const filteredProducts = products?.filter(
(product) => product.product_subcategory?.id == params.subCategoryId
(product) =>
slugify(product.product_subcategory?.subcategory_name, {
lower: true,
}) === params.subcategory
);
console.log(filteredProducts)
const selectedCategory = filteredProducts?.length > 0 ? filteredProducts[0].product_subcategory : {"category_name": ""}
console.log("subcategory product are :", filteredProducts);
const selectedCategory =
filteredProducts?.length > 0
? filteredProducts[0].product_subcategory
: { category_name: "" };
return (
<section className="py-16 bg-gray-50">
@@ -26,44 +36,54 @@ export default function ExploreSiddhaMala({ params }) {
{selectedCategory ? selectedCategory.category_name : "Products"}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8">
{filteredProducts?.map((product) => (
<div
key={product.id}
className="bg-white shadow-lg rounded-lg overflow-hidden transition-transform transform hover:scale-105"
>
<Link href={`/products/${product.id}`}>
<div className="relative w-full h-64 bg-gray-200">
<Image
src={`${backendUrl}${product?.images[0]?.image}`}
alt={product.product_name}
layout="fill"
objectFit="cover"
className="rounded-t-lg"
/>
{filteredProducts?.map((product) => {
// ✅ generate slug for debugging
const slug = `${slugify(product.product_name, { lower: true })}-${
product.id
}`;
console.log("Generated slug for product:", slug);
return (
<div
key={product.id}
className="bg-white shadow-lg rounded-lg overflow-hidden transition-transform transform hover:scale-105"
>
<Link href={`/products/${slug}`}>
<div className="relative w-full h-64 bg-gray-200">
<Image
src={`${backendUrl}${product?.images[0]?.image}`}
alt={product.product_name}
layout="fill"
objectFit="cover"
className="rounded-t-lg"
/>
</div>
</Link>
<div className="p-5">
<h3 className="text-lg font-semibold text-gray-900 mb-2 truncate">
{product.product_name}
</h3>
{product.variants?.length > 0 ? (
<p className="text-lg font-bold text-green-600">
{isLoading
? "Loading..."
: formatPrice(
Math.min(
...product.variants.map(
(variant) => variant.price
)
)
)}
</p>
) : (
<p className="text-md font-medium text-gray-500">
Price on request
</p>
)}
</div>
</Link>
<div className="p-5">
<h3 className="text-lg font-semibold text-gray-900 mb-2 truncate">
{product.product_name}
</h3>
{product.variants?.length > 0 ? (
<p className="text-lg font-bold text-green-600">
{isLoading
? "Loading..."
: formatPrice(
Math.min(
...product.variants.map((variant) => variant.price)
)
)}
</p>
) : (
<p className="text-md font-medium text-gray-500">
Price on request
</p>
)}
</div>
</div>
))}
);
})}
</div>
</div>
</section>

View File

@@ -1,10 +1,12 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useContext } from "react";
import { backendUrl } from "@/utils/axios";
import axios from "axios";
import Image from "next/image";
import { motion } from "framer-motion";
import slugify from "slugify";
import ProductContext from "@/app/contexts/productContext";
const cleanHTML = (html) => {
if (!html) return "";
@@ -12,68 +14,77 @@ const cleanHTML = (html) => {
};
const SubcategoryHero = ({ params }) => {
const [selectedCategory, setSubcategory] = useState(null);
const { products, category } = useContext(ProductContext);
console.log("SubcategoryHero mounted. Params:", params);
const [selectedSubcategory, setSelectedSubcategory] = useState(null);
useEffect(() => {
const fetchSubcategory = async () => {
try {
const response = await axios.get(
`${backendUrl}/admins/product/subcategory/${params.subCategoryId}`
);
console.log(response.data)
const selectedSubcategory = response.data
setSubcategory(selectedSubcategory);
} catch (error) {
console.error("Error fetching subcategory:", error);
}
};
fetchSubcategory();
}, [params.subCategoryId]);
useEffect(() => {
if (products?.length > 0) {
const match = products
.map((p) => p.product_subcategory)
.find(
(sub) =>
slugify(sub?.subcategory_name || "", { lower: true }) ===
params.subcategory
);
// Fallback values
console.log("Matched from products:", match);
setSelectedSubcategory(match || null);
}
}, [products, params.subcategory]);
// Fallbacks
const fallbackImage = "/placeholder-image.jpg";
const fallbackDescription =
"Explore our curated collection of spiritual items designed to enhance your journey of self-discovery and inner peace.";
const fallbackCategoryName = "Spiritual Essentials";
return (
<section className="relative h-[50vh] md:h-[80vh] flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src={`${
selectedCategory?.image_url
? backendUrl + selectedCategory?.image_url
: "/sidhi-mala/Artboard_1_bf5ccd46-7152-4355-82a8-9e9f27c1bfc2.jpg"
}`}
alt="Category Background"
layout="fill"
objectFit="cover"
quality={100}
priority
/>
</div>
<section className="relative min-h-[50vh] md:h-[80vh] py-8 md:py-0 flex mt-3 justify-center">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
{/* Hero Image */}
<div className="relative h-[250px] md:h-[500px] overflow-hidden rounded-lg shadow-xl">
<Image
src={
selectedSubcategory?.image
? `${backendUrl}${selectedSubcategory.image}`
: fallbackImage
}
alt={
selectedSubcategory?.subcategory_name || "Category Background"
}
fill
style={{ objectFit: "cover" }}
quality={100}
priority
/>
</div>
<div className="relative z-10 text-center px-6 md:px-12 max-w-3xl">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-4xl md:text-6xl font-bold text-white mb-4 drop-shadow-lg"
>
{selectedCategory?.subcategory_name || fallbackCategoryName}
</motion.h1>
{/* Hero Text */}
<div className="text-start flex justify-start flex-col h-full">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-2xl md:text-3xl font-bold text-black mb-4"
>
{selectedSubcategory?.subcategory_name || fallbackCategoryName}
</motion.h1>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="text-lg md:text-xl text-gray-200 drop-shadow-md"
dangerouslySetInnerHTML={{
__html: cleanHTML(
selectedCategory?.description || fallbackDescription
),
}}
/>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="text-black text-sm md:text-base"
dangerouslySetInnerHTML={{
__html: cleanHTML(
selectedSubcategory?.description || fallbackDescription
),
}}
/>
</div>
</div>
</div>
</section>
);

3537
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -33,6 +33,7 @@
"react-hot-toast": "^2.4.1",
"react-icons": "^5.3.0",
"react-razorpay": "^3.0.1",
"slugify": "^1.6.6",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7"
},