first commit

This commit is contained in:
2026-02-14 16:15:31 +05:30
commit 8f4cf07ec0
197 changed files with 17228 additions and 0 deletions

95
.gitignore vendored Normal file
View File

@@ -0,0 +1,95 @@
# ===============================
# Dependencies
# ===============================
node_modules/
vendor/
# ===============================
# Build / Output
# ===============================
dist/
build/
target/
out/
# ===============================
# Environment files
# ===============================
.env
.env.local
.env.development
.env.production
.env.test
# ===============================
# Logs
# ===============================
*.log
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
# ===============================
# Runtime / Temp files
# ===============================
tmp/
temp/
.cache/
# ===============================
# Python
# ===============================
__pycache__/
*.py[cod]
*.pyo
.venv/
venv/
# ===============================
# Java / JVM
# ===============================
*.class
*.jar
*.war
# ===============================
# OS generated files
# ===============================
.DS_Store
Thumbs.db
# ===============================
# IDE / Editor
# ===============================
.idea/
.vscode/
*.suo
*.ntvs*
*.njsproj
*.sln
# ===============================
# Uploads / Media (IMPORTANT)
# ===============================
uploads/
public/uploads/
storage/
media/
# ===============================
# Large media files
# ===============================
*.mp4
*.mov
*.avi
*.flv
*.wmv
*.tiff
*.psd
*.zip
*.rar
# ===============================
# Git
# ===============================
.git/

16
README.md Normal file
View File

@@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

29
eslint.config.js Normal file
View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

15
index.html Normal file
View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<title>Vaishnavi Collection</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

4028
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "ecommerce-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@reduxjs/toolkit": "^2.11.1",
"clsx": "^2.1.1",
"framer-motion": "^12.23.25",
"lucide-react": "^0.556.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-icons": "^5.5.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.10.1",
"swiper": "^12.1.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.22",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"vite": "^7.2.4"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/Frame (1).png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

BIN
public/Frame.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/add-to-cart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
public/default-product.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
public/empty-wishlist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

BIN
public/hero1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

BIN
public/hero2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

BIN
public/hero3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

BIN
public/hero4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

BIN
public/hero5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 MiB

BIN
public/logo-small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/logo-small2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
public/not-found.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

BIN
public/order-success.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

BIN
public/rating.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

23
src/App.jsx Normal file
View File

@@ -0,0 +1,23 @@
import React from "react";
import Navbar from "./components/navbar/Navbar";
import Footer from "./components/layout/Footer";
import AppRoutes from "./routes/AppRoutes";
import TopPromoBanner from "./components/navbar/TopPromoBanner";
import ScrollToTop from "./components/ui/ScrollToTop";
const App = () => {
return (
<div className="flex flex-col min-h-screen">
<Navbar />
{/* <TopPromoBanner/> */}
<main className="flex-1">
<AppRoutes />
<ScrollToTop/>
</main>
{/* <Footer /> */}
</div>
);
};
export default App;

51
src/app/store.js Normal file
View File

@@ -0,0 +1,51 @@
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "../features/user/userSlice";
import authReducer from "../features/auth/authSlice";
import { authApi } from "../features/auth/authApi";
import { wishlistApi } from "../features/wishlist/wishlistApi";
import cartReducer from "../features/cart/cartSlice";
import { cartApi } from "../features/cart/cartAPI";
import { addressApi } from "../features/address/addressApi";
// import authReducer from "../features/address/addressApi";
import { categoriesApi } from "../features/categories/categoryApi";
import { productApi } from "../features/products/productsAPI";
import recentlyViewedReducer from "../features/recentlyViewed/recentlyViewedApi";
import { ordersApi } from "../features/orders/ordersApi";
import { wardrobeApi } from "../features/wardrobe/wardrobeApi";
import { couponsApi } from "../features/coupons/couponsApi";
export const store = configureStore({
reducer: {
user: userReducer,
auth: authReducer,
cart: cartReducer,
[authApi.reducerPath]: authApi.reducer,
[ordersApi.reducerPath]: ordersApi.reducer,
[wishlistApi.reducerPath]: wishlistApi.reducer,
[cartApi.reducerPath]: cartApi.reducer,
[addressApi.reducerPath]: addressApi.reducer,
// [categoryApi.reducerPath]: categoryApi.reducer,
[categoriesApi.reducerPath]: categoriesApi.reducer,
[productApi.reducerPath]: productApi.reducer,
[wardrobeApi.reducerPath]: wardrobeApi.reducer,
// [categoriesApi.reducerPath]: categoriesApi.reducer,
recentlyViewed: recentlyViewedReducer,
[couponsApi.reducerPath]: couponsApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(authApi.middleware)
.concat(ordersApi.middleware)
.concat(wishlistApi.middleware)
.concat(cartApi.middleware)
.concat(addressApi.middleware)
// .concat(categoryApi.middleware)
.concat(categoriesApi.middleware)
.concat(productApi.middleware)
.concat(wardrobeApi.middleware)
.concat(couponsApi.middleware),
});
export default store;

1
src/assets/react.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,210 @@
import React, { useState } from "react";
const sizes = ["S", "M", "L", "XL", "Free Size", "3XL", "4XL"];
const colors = [
"Red",
"Black",
"Green",
"Brown",
"Maroon",
"Gold",
"Fawn",
"Peach",
"Wine",
"Yellow",
];
const designs = ["Print", "Ambiodry", "Stone Work", "Weave", "Zari"];
const fabrics = ["Silk", "Tissue", "Velvet"];
const tabs = ["Size", "Color", "Price", "Design", "Fabric", "Availability"];
const FilterPanel = () => {
const [activeTab, setActiveTab] = useState("Size");
const [selectedSizes, setSelectedSizes] = useState([]);
const [selectedColors, setSelectedColors] = useState([]);
const [priceRange, setPriceRange] = useState([0, 5000]);
const [selectedDesigns, setSelectedDesigns] = useState([]);
const [selectedFabrics, setSelectedFabrics] = useState([]);
const [inStockOnly, setInStockOnly] = useState(false);
const toggleSelection = (item, list, setList) => {
list.includes(item)
? setList(list.filter((i) => i !== item))
: setList([...list, item]);
};
return (
<div className="w-full flex gap-4 bg-white p-4 rounded-2xl ">
{/* LEFT TABS */}
<div className="w-32 border-r pr-4 flex flex-col gap-2">
{tabs.map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-3 py-2 text-left rounded-lg text-base transition-all
${
activeTab === tab
? "bg-primary-default text-white shadow"
: "bg-gray-50 hover:bg-gray-100 text-gray-700"
}`}
>
{tab}
</button>
))}
</div>
{/* RIGHT CONTENT */}
<div className="flex-1">
<div className="bg-gray-50 p-4 rounded-xl h-[400px] overflow-y-auto shadow-inner">
{/* SIZE */}
{activeTab === "Size" && (
<div className="flex flex-col gap-2">
{sizes.map((size) => (
<button
key={size}
onClick={() =>
toggleSelection(size, selectedSizes, setSelectedSizes)
}
className={`px-3 py-2 rounded-lg border text-sm font-medium transition
${
selectedSizes.includes(size)
? "bg-primary-light text-white border-primary shadow"
: "bg-white border-gray-300 hover:bg-primary/10"
}`}
>
{size}
</button>
))}
</div>
)}
{/* COLOR */}
{activeTab === "Color" && (
<div className="flex flex-col gap-3">
{colors.map((color) => (
<div
key={color}
onClick={() =>
toggleSelection(color, selectedColors, setSelectedColors)
}
className={`w-14 h-14 rounded-full cursor-pointer border-4 shadow
${
selectedColors.includes(color)
? "border-primary scale-105"
: "border-gray-300"
}`}
style={{ backgroundColor: color.toLowerCase() }}
/>
))}
</div>
)}
{/* PRICE */}
{activeTab === "Price" && (
<div className="flex flex-col gap-3">
{/* Min Label */}
<span className="font-semibold text-gray-700 text-sm">
Min: 0
</span>
{/* Max Label */}
<span className="font-semibold text-gray-700 text-sm">
Max: {priceRange[1]}
</span>
{/* Slider */}
<input
type="range"
min="0"
max="5000"
value={priceRange[1]}
onChange={(e) => setPriceRange([0, Number(e.target.value)])}
className="w-full accent-primary"
/>
{/* Apply Button */}
<button className="w-full bg-primary text-white py-2 rounded-lg text-sm hover:bg-primary/90">
Apply
</button>
</div>
)}
{/* DESIGN */}
{activeTab === "Design" && (
<div className="flex flex-col gap-3">
{designs.map((design) => (
<label
key={design}
className="flex items-center gap-3 bg-white p-3 rounded-lg border text-xs shadow cursor-pointer"
>
<input
type="checkbox"
checked={selectedDesigns.includes(design)}
onChange={() =>
toggleSelection(
design,
selectedDesigns,
setSelectedDesigns
)
}
className="accent-primary"
/>
{design}
</label>
))}
</div>
)}
{/* FABRIC */}
{activeTab === "Fabric" && (
<div className="flex flex-col gap-3">
{fabrics.map((fabric) => (
<label
key={fabric}
className="flex items-center gap-3 bg-white p-3 rounded-lg border text-sm shadow cursor-pointer"
>
<input
type="checkbox"
checked={selectedFabrics.includes(fabric)}
onChange={() =>
toggleSelection(
fabric,
selectedFabrics,
setSelectedFabrics
)
}
className="accent-primary"
/>
{fabric}
</label>
))}
</div>
)}
{/* AVAILABILITY */}
{activeTab === "Availability" && (
<label className="flex items-center gap-3 bg-white p-3 rounded-lg border text-xs shadow">
<input
type="checkbox"
checked={inStockOnly}
onChange={() => setInStockOnly(!inStockOnly)}
className="accent-primary"
/>
In Stock Only
</label>
)}
</div>
{/* BOTTOM APPLY BUTTON */}
<div className="mt-4">
<button className="w-full bg-primary-default text-white py-3 rounded-lg text-base hover:bg-primary/90">
Apply Filters
</button>
</div>
</div>
</div>
);
};
export default FilterPanel;

View File

@@ -0,0 +1,48 @@
import { motion } from "framer-motion";
const OfferingCard = ({ icon, title }) => {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="
group p-[2px] rounded-card
bg-gradient-to-r
from-primary-light/30
via-secondary-light/30
to-accent-light/30
hover:from-primary-light/50
hover:via-secondary-light/50
hover:to-accent-light/50
transition-all duration-500
"
>
<div
className="
bg-white rounded-card shadow-card
group-hover:shadow-xl
flex items-center gap-5
py-5 px-6
transition-all duration-500
"
>
{/* Icon */}
<div className="
w-14 h-14 flex items-center justify-center rounded-full
bg-gradient-to-br from-primary.default to-primary.dark
text-white text-3xl shadow-md
">
{icon}
</div>
{/* Title */}
<h3 className="text-xl font-semibold text-neutral-800 tracking-wide">
{title}
</h3>
</div>
</motion.div>
);
};
export default OfferingCard;

View File

@@ -0,0 +1,31 @@
import OfferingCard from "./OfferingCard";
import { offeringsData } from "../../data/offeringsData";
const OfferingsSection = () => {
return (
<section className="py-20 px-6 bg-neutral-50">
<div className="max-w-7xl mx-auto text-center">
<h2 className="text-4xl font-bold mb-3 text-neutral-900">
Our Unique Offerings
</h2>
<p className="text-neutral-600 mb-14 text-lg">
Experience premium services crafted to elevate your shopping journey.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10">
{offeringsData.map((item) => (
<OfferingCard
key={item.id}
icon={item.icon}
title={item.title}
/>
))}
</div>
</div>
</section>
);
};
export default OfferingsSection;

View File

@@ -0,0 +1,9 @@
const Badge = ({ children, className = "" }) => {
return (
<span className={`px-3 py-1 text-xs rounded-full bg-black text-white ${className}`}>
{children}
</span>
);
};
export default Badge;

View File

@@ -0,0 +1,18 @@
const Button = ({ children, className = "", ...props }) => {
return (
<button
{...props}
className={`
px-6 py-3 rounded-xl text-sm font-medium
bg-primary-default text-white
hover:bg-primary-light
transition active:scale-95
${className}
`}
>
{children}
</button>
);
};
export default Button;

View File

View File

@@ -0,0 +1,37 @@
// const Checkbox = ({ label, ...props }) => {
// return (
// <label className="flex items-center gap-2 cursor-pointer group">
// <input
// type="checkbox"
// className="w-4 h-4 border-gray-400 rounded-sm
// checked:bg-primary checked:border-primary"
// {...props}
// />
// <span className="text-gray-700 group-hover:text-primary transition-colors">
// {label}
// </span>
// </label>
// );
// };
// export default Checkbox;
const Checkbox = ({ label, ...props }) => {
return (
<label className="flex items-center gap-3 cursor-pointer group pl-1">
<input
type="checkbox"
className="w-4 h-4 border-gray-400 rounded-sm
checked:bg-primary checked:border-primary"
{...props}
/>
<span className="text-gray-700 group-hover:text-primary transition-colors ml-1">
{label}
</span>
</label>
);
};
export default Checkbox;

View File

@@ -0,0 +1,66 @@
// import React from "react";
// const EmptyState = ({
// title = "No products found",
// description = "Try adjusting filters or check back later",
// icon = "🛍️",
// }) => {
// return (
// <div className="flex flex-col items-center justify-center py-20 text-center">
// <div className="text-5xl mb-4">{icon}</div>
// <h3 className="text-xl font-semibold text-gray-800 mb-2">
// {title}
// </h3>
// <p className="text-gray-500 max-w-sm">
// {description}
// </p>
// </div>
// );
// };
// export default EmptyState;
import React from "react";
import clsx from "clsx";
const VARIANTS = {
default: {
title: "text-gray-800",
desc: "text-[#f2f2f2]",
},
error: {
title: "text-red-600",
desc: "text-[#f2f2f2]",
},
warning: {
title: "text-yellow-600",
desc: "text-[#f2f2f2]",
},
};
const EmptyState = ({
title,
description,
icon = "📦",
variant = "default",
}) => {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="text-5xl mb-4">{icon}</div>
<h3
className={clsx("text-xl font-semibold mb-2", VARIANTS[variant].title)}
>
{title}
</h3>
<p className={clsx("max-w-sm text-sm", VARIANTS[variant].desc)}>
{description}
</p>
</div>
);
};
export default EmptyState;

View File

@@ -0,0 +1,15 @@
const Input = ({ label, className = "", ...props }) => {
return (
<div className="flex flex-col gap-1">
{label && <label className="text-sm font-medium text-gray-600">{label}</label>}
<input
{...props}
className={`border border-gray-300 rounded-lg px-3 py-2
focus:outline-none focus:ring-2 focus:ring-black
${className}`}
/>
</div>
);
};
export default Input;

View File

@@ -0,0 +1,7 @@
const Loader = () => (
<div className="flex items-center justify-center py-10">
<div className="w-8 h-8 border-4 border-gray-300 border-t-black rounded-full animate-spin"></div>
</div>
);
export default Loader;

View File

View File

@@ -0,0 +1,14 @@
import React from "react";
import ProductCardSkeleton from "./ProductCardSkeleton";
const ProductGridSkeleton = ({ count = 8 }) => {
return (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-8">
{Array.from({ length: count }).map((_, i) => (
<ProductCardSkeleton key={i} />
))}
</div>
);
};
export default ProductGridSkeleton;

View File

View File

@@ -0,0 +1,27 @@
const Table = ({ columns = [], data = [] }) => {
return (
<table className="w-full border border-gray-200 rounded-lg overflow-hidden">
<thead className="bg-gray-100">
<tr>
{columns.map((col) => (
<th key={col} className="p-3 text-left text-sm font-semibold text-gray-800">
{col}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i} className="border-t hover:bg-gray-50">
{Object.values(row).map((item, index) => (
<td key={index} className="p-3 text-sm">{item}</td>
))}
</tr>
))}
</tbody>
</table>
);
};
export default Table;

View File

@@ -0,0 +1,18 @@
import React from "react";
// import CategoryCard from "./CategoryCard";
import CategoryCarousel from "./CategoryCarousel";
const Category = () => {
return (
<section className="relative py-16 bg-neutral-50">
{/* Curved top */}
<div className="absolute top-0 left-0 w-full h-32 bg-primary rounded-b-[80px] z-0"></div>
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<CategoryCarousel />
</div>
</section>
);
};
export default Category;

View File

@@ -0,0 +1,51 @@
import React from "react";
import { Link } from "react-router-dom";
const CategoryCard = ({ category }) => {
const parent = category.grandParent || "all";
const categorySlug = category.parent || category.slug;
const subCategory = category.slug;
return (
<Link
to={`/product/${parent}/${categorySlug}/${subCategory}`}
className="group block h-full w-full"
>
<div className="relative h-full overflow-hidden rounded-2xl bg-white shadow-lg transition-all duration-500 hover:-translate-y-2 hover:shadow-2xl">
{/* Image */}
<img
src={category.image}
alt={category.name}
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
{/* Gradient Overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
{/* Content */}
<div className="absolute inset-0 flex items-end p-6">
<h3 className="
rounded-lg
bg-black/70
px-5
py-2
text-xl
font-bold
text-white
shadow-lg
backdrop-blur-md
transition-all
duration-300
group-hover:translate-y-[-4px]
group-hover:bg-black/80
">
{category.name}
</h3>
</div>
</div>
</Link>
);
};
export default CategoryCard;

View File

@@ -0,0 +1,78 @@
import React from "react";
import { Swiper, SwiperSlide } from "swiper/react";
import { EffectCoverflow, Navigation } from "swiper/modules";
import "swiper/css";
import "swiper/css/effect-coverflow";
import "swiper/css/navigation";
import { ChevronLeft, ChevronRight } from "lucide-react";
import CategoryCard from "./CategoryCard";
import { useGetCategoriesQuery } from "../../../features/categories/categoryApi";
const CategoryCarousel = () => {
const { data, isLoading, isError } = useGetCategoriesQuery();
console.log("CATEGORY API DATA 👉", data);
if (isLoading) {
return (
<div className="text-center py-20 text-gray-500">
Loading categories...
</div>
);
}
if (isError) {
return (
<div className="text-center py-20 text-red-500">
Failed to load categories
</div>
);
}
return (
<div className="relative">
<h2 className="text-3xl md:text-4xl font-bold text-primary-default text-center mb-10">
Shop by Category
</h2>
{/* Navigation */}
<div className="swiper-button-prev-custom">
<ChevronLeft size={26} />
</div>
<div className="swiper-button-next-custom">
<ChevronRight size={26} />
</div>
<Swiper
effect="coverflow"
grabCursor
centeredSlides
slidesPerView="auto"
loop
navigation={{
nextEl: ".swiper-button-next-custom",
prevEl: ".swiper-button-prev-custom",
}}
coverflowEffect={{
rotate: 0,
stretch: 150,
depth: 350,
modifier: 1,
slideShadows: false,
}}
modules={[EffectCoverflow, Navigation]}
className="w-full max-w-7xl mx-auto"
>
{data?.data?.map((cat) => (
<SwiperSlide
key={cat.id}
className="w-[260px] md:w-[330px] h-[420px] md:h-[480px] rounded-xl overflow-hidden bg-white"
>
<CategoryCard category={cat} />
</SwiperSlide>
))}
</Swiper>
</div>
);
};
export default CategoryCarousel;

View File

@@ -0,0 +1,64 @@
import React from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useGetCategoryColorsQuery } from "../../../features/categories/categoryApi";
const CategoryGrid = () => {
const { category, subCategory } = useParams();
const navigate = useNavigate();
// use current category slug
const categorySlug = subCategory || category;
const { data, isLoading } = useGetCategoryColorsQuery(categorySlug, {
skip: !categorySlug,
});
const colors = data?.data || [];
const message = data?.message;
if (isLoading) return null;
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-20 mt-4">
{colors.length === 0 ? (
<p className="text-center text-gray-500 font-medium py-10">
{message || "No colors available for this category"}
</p>
) : (
<div className="flex space-x-4 overflow-x-auto scrollbar-thin scrollbar-thumb-gray-300 sm:grid sm:grid-cols-3 md:grid-cols-6 sm:gap-6 sm:overflow-visible">
{colors.map((color) => (
<div
key={color.slug}
onClick={() =>
navigate(`/products/${categorySlug}?color=${color.slug}`)
}
className="flex-shrink-0 flex flex-col items-center cursor-pointer group transition-transform hover:scale-105 w-28 sm:w-auto"
>
{/* Circle */}
<div
className="w-24 h-24 sm:w-28 sm:h-28 md:w-32 md:h-32 rounded-full flex items-center justify-center shadow-lg hover:shadow-xl transition"
style={{ backgroundColor: color.bg }}
>
<div className="w-20 h-20 sm:w-24 sm:h-24 md:w-28 md:h-28 rounded-full overflow-hidden border-2 border-white">
<img
src={color.image}
alt={color.name}
className="w-full h-full object-cover"
/>
</div>
</div>
{/* Name */}
<p className="mt-3 text-center text-xs sm:text-sm font-semibold text-gray-700 group-hover:text-primary">
{color.name}
</p>
</div>
))}
</div>
)}
</div>
);
};
export default CategoryGrid;

View File

@@ -0,0 +1 @@
export { default } from "./Category";

View File

@@ -0,0 +1,130 @@
import React, { useState, useEffect } from "react";
const slides = [
{
image: "/hero1.png",
title: "LIMITED EDITION",
subtitle: "Premium Menswear Collection",
cta1: "Shop Collection",
cta2: "Explore Luxury",
},
{
image: "/hero2.png",
title: "THE ROYAL LOOK",
subtitle: "Crafted for Modern Kings",
cta1: "Shop Sherwanis",
cta2: "View Collection",
},
{
image: "/hero3.png",
title: "BLACK • GOLD EDITION",
subtitle: "Timeless Elegance & Precision",
cta1: "Discover Now",
cta2: "View Collection",
},
{
image: "/hero4.png",
title: "BLACK • GOLD EDITION",
subtitle: "Timeless Elegance & Precision",
cta1: "Discover Now",
cta2: "View Collection",
},
];
const HeroSection = () => {
const [current, setCurrent] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCurrent((prev) => (prev + 1) % slides.length);
}, 6000);
return () => clearInterval(interval);
}, []);
return (
<section className="relative w-full h-[650px] md:h-[750px] overflow-hidden">
{slides.map((slide, index) => (
<div
key={index}
className={`absolute inset-0 w-full h-full transition-opacity duration-[1500ms] ease-[cubic-bezier(0.25,0.1,0.25,1)]
${index === current ? "opacity-100" : "opacity-0"}
`}
>
<img
src={slide.image}
className="w-full h-full object-cover scale-105 hover:scale-110 transition-transform duration-[3000ms]"
alt="hero"
/>
{/* Dark Cinematic Overlay */}
<div className="absolute inset-0 bg-gradient-to-r from-black/90 via-black/40 to-black/10" />
{/* Brand-Colored Spotlight */}
<div
className="absolute inset-0 pointer-events-none
bg-[radial-gradient(circle_at_20%_30%,rgba(176,127,93,0.25),transparent_60%)]"
/>
{/* Content */}
<div className="absolute inset-0 flex items-center">
<div className="px-8 md:px-20 max-w-3xl text-left">
<h1 className="text-white text-4xl md:text-6xl font-extrabold tracking-wide leading-tight drop-shadow-xl uppercase animate-fadeInUp">
{slide.title}
</h1>
<p className="text-neutral-200 text-lg md:text-2xl mt-3 mb-8 tracking-wide animate-fadeInUp delay-200">
{slide.subtitle}
</p>
<div className="flex gap-5 animate-fadeInUp delay-300">
{/* Primary Button (Luxury Gold-Brown) */}
<button
className="px-7 py-3
bg-gradient-to-r from-primary-default to-primary-light
text-black font-semibold rounded-full
shadow-[0_0_20px_rgba(176,127,93,0.4)]
hover:shadow-[0_0_30px_rgba(176,127,93,0.7)]
transition-all uppercase tracking-wide"
>
{slide.cta1}
</button>
{/* Outline Button */}
<button
className="px-7 py-3 border border-primary-default
text-primary-default font-semibold rounded-full
hover:bg-primary-default hover:text-white
transition-all uppercase tracking-wide"
>
{slide.cta2}
</button>
</div>
{/* Accent Bar */}
<div className="w-24 h-1 bg-primary-default mt-8 rounded-full animate-pulse" />
</div>
</div>
</div>
))}
{/* Navigation Dots */}
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 flex gap-2">
{slides.map((_, idx) => (
<div
key={idx}
onClick={() => setCurrent(idx)}
className={`w-3 h-3 rounded-full cursor-pointer border border-primary-default transition-all
${
idx === current
? "bg-primary-default shadow-[0_0_10px_rgba(176,127,93,0.8)]"
: "bg-transparent"
}
`}
></div>
))}
</div>
</section>
);
};
export default HeroSection;

View File

@@ -0,0 +1,23 @@
import React from "react";
import HeroSection from "./HeroSection/HeroSection";
import Category from "./Category/Category";
import MostLoved from "./MostLoved/MostLoved";
import NewGems from "./NewGems/NewGems";
import OfferingsSection from "../Offerings/OfferingsSection";
import RecentlyViewed from "../recentlyViewed/RecentlyViewed";
import RecommendedForYou from "./RecommendedForYou/RecommendedForYou";
// import TopPromoBanner from "../navbar/TopPromoBanner";
const Home = () => (
<div className="flex flex-col">
<HeroSection />
<Category />
<RecentlyViewed limit={6} />
<RecommendedForYou limit={10} />
<MostLoved />
<NewGems />
<OfferingsSection/>
</div>
);
export default Home;

View File

@@ -0,0 +1,62 @@
import React from "react";
import { useGetMostLovedProductsQuery } from "../../../features/products/productsAPI";
import MostLovedCard from "./MostLovedCard";
import ProductGridSkeleton from "../../skeletons/ProductGridSkeleton";
import EmptyState from "../../common/EmptyState";
import { getRTKErrorMessage } from "../../../utils/rtkError";
const MostLoved = () => {
const {
data: products = [],
isLoading,
isError,
error,
} = useGetMostLovedProductsQuery(8);
return (
<section className="pb-16 bg-[#B07F5D]">
<div className="max-w-7xl mx-auto px-6">
<div className="mb-8">
<img
src="/most-loved-banner.png"
alt="Most Loved"
className="mx-auto w-80 max-w-md rounded-lg"
/>
</div>
{/* 🔥 HEADER */}
<div className="text-center mb-14">
<span className="inline-block mb-3 px-4 py-1 text-sm font-semibold rounded-full bg-pink-100 text-pink-600">
Trending Picks
</span>
<h2 className="text-3xl sm:text-4xl font-extrabold text-gray-900 tracking-tight">
Most Loved Labels
</h2>
</div>
{/* ⏳ LOADING */}
{isLoading && <ProductGridSkeleton count={8} />}
{/* ❌ ERROR */}
{isError && (
<EmptyState
title="Unable to load products"
description={getRTKErrorMessage(error)}
icon="⚠️"
variant="error"
/>
)}
{/* ❤️ PRODUCTS */}
{!isLoading && products.length > 0 && (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6">
{products.map((product) => (
<MostLovedCard key={product._id} product={product} />
))}
</div>
)}
</div>
</section>
);
};
export default MostLoved;

View File

@@ -0,0 +1,177 @@
// import React from "react";
// import { Link } from "react-router-dom";
// const MostLovedCard = ({ product }) => {
// const image =
// product?.images?.gallery?.[0] ||
// product?.images?.primary ||
// "/placeholder-product.png";
// const discount = product?.compareAtPrice
// ? Math.round(
// ((product.compareAtPrice - product.basePrice) /
// product.compareAtPrice) *
// 100,
// )
// : 0;
// return (
// <Link
// to={`/product/${product.slug}`}
// className="group block rounded-2xl overflow-hidden bg-white
// border border-gray-100
// transition-all duration-500
// hover:-translate-y-1 hover:shadow-xl"
// >
// {/* Image */}
// <div className="relative aspect-[3/4] overflow-hidden">
// <img
// src={image}
// alt={product.name}
// className="w-full h-full object-cover
// transition-transform duration-700
// group-hover:scale-105"
// />
// {/* Soft Gradient */}
// <div
// className="absolute inset-0 bg-gradient-to-t
// from-black/20 via-transparent to-transparent"
// />
// {/* Discount */}
// {discount > 0 && (
// <div
// className="absolute top-3 left-3 bg-yellow-600 backdrop-blur
// text-gray-900 text-xs font-semibold
// px-3 py-1 rounded-full shadow-sm"
// >
// {discount}%
// </div>
// )}
// </div>
// {/* Content */}
// <div className="p-4">
// <h3 className="text-gray-900 text-base font-medium leading-snug line-clamp-2">
// {product.name}
// </h3>
// {/* Price */}
// <div className="mt-2 flex items-center gap-2">
// <span className="text-lg font-semibold text-gray-900">
// ₹{product.basePrice}
// </span>
// {discount > 0 && (
// <span className="text-sm text-gray-400 line-through">
// ₹{product.compareAtPrice}
// </span>
// )}
// </div>
// {/* Hover Hint */}
// <div
// className="mt-3 h-[2px] w-0 bg-gray-900
// transition-all duration-500
// group-hover:w-10"
// />
// </div>
// </Link>
// );
// };
// export default MostLovedCard;
import React from "react";
import { Link } from "react-router-dom";
const MostLovedCard = ({ product }) => {
const image =
product?.images?.gallery?.[0] ||
product?.images?.primary ||
"/placeholder-product.png";
const hasOffer =
product?.compareAtPrice &&
product?.compareAtPrice > product?.basePrice;
const discount = hasOffer
? Math.round(
((product.compareAtPrice - product.basePrice) /
product.compareAtPrice) *
100
)
: 0;
return (
<Link
to={`/product/${product.slug}`}
className="group block rounded-2xl overflow-hidden bg-white
border border-gray-100
transition-all duration-500
hover:-translate-y-1 hover:shadow-xl"
>
{/* IMAGE */}
<div className="relative aspect-[3/4] overflow-hidden bg-gray-100">
<img
src={image}
alt={product.name}
className="w-full h-full object-cover
transition-transform duration-700
group-hover:scale-105"
/>
{/* DARK GRADIENT */}
<div className="absolute inset-0 bg-gradient-to-t from-black/25 via-transparent to-transparent" />
{/* 🔥 OFFER BADGE (ONLY IF DISCOUNT) */}
{hasOffer && (
<div className="absolute top-3 left-3 bg-black/85 text-white text-xs font-semibold px-3 py-1 rounded-full shadow">
{discount}% OFF
</div>
)}
{/* ❤️ MOST LOVED TAG */}
<div className="absolute top-3 right-3 bg-white/90 backdrop-blur text-xs font-medium px-3 py-1 rounded-full text-gray-900 shadow">
Loved
</div>
</div>
{/* CONTENT */}
<div className="p-4 space-y-1">
<h3 className="text-gray-900 text-sm font-medium leading-snug line-clamp-1">
{product.name}
</h3>
{/* DESCRIPTION */}
<p className="text-xs text-gray-500 line-clamp-2">
{product.shortDescription || product.description}
</p>
{/* PRICE */}
<div className="mt-2 flex items-center gap-2">
<span className="text-base font-semibold text-gray-900">
{product.basePrice}
</span>
{hasOffer && (
<span className="text-sm text-gray-400 line-through">
{product.compareAtPrice}
</span>
)}
</div>
{/* SUBTLE HOVER LINE */}
<div
className="mt-3 h-[2px] w-0 bg-gray-900
transition-all duration-500
group-hover:w-10"
/>
</div>
</Link>
);
};
export default MostLovedCard;

View File

@@ -0,0 +1,187 @@
// import React from "react";
// import { Link } from "react-router-dom";
// const NewArrivalCard = ({ product }) => {
// const image =
// product?.variants?.[0]?.images?.[0] ||
// product?.images?.primary ||
// "/placeholder-product.png";
// const price = product?.variants?.[0]?.price || product?.basePrice;
// const discount =
// product?.compareAtPrice && product.compareAtPrice > price
// ? Math.round(
// ((product.compareAtPrice - price) / product.compareAtPrice) * 100
// )
// : 0;
// return (
// <Link
// to={`/product/${product.slug}`}
// className="group block rounded-2xl overflow-hidden bg-white
// border border-gray-100
// transition-all duration-500
// hover:-translate-y-1 hover:shadow-xl"
// >
// {/* Image */}
// <div className="relative aspect-[3/4] overflow-hidden">
// <img
// src={image}
// alt={product.name}
// className="w-full h-full object-cover
// transition-transform duration-700
// group-hover:scale-105"
// />
// {/* Soft gradient */}
// <div
// className="absolute inset-0 bg-gradient-to-t
// from-black/20 via-transparent to-transparent"
// />
// {/* NEW badge */}
// <div
// className="absolute top-3 left-3 bg-primary-dark backdrop-blur
// text-white text-xs font-semibold
// px-3 py-1 rounded-full shadow-sm"
// >
// NEW
// </div>
// {/* Discount */}
// {discount > 0 && (
// <div
// className="absolute top-3 right-3 bg-yellow-800 backdrop-blur
// text-gray-100 text-xs font-semibold
// px-3 py-1 rounded-full shadow-sm"
// >
// {discount}%
// </div>
// )}
// </div>
// {/* Content */}
// <div className="p-4">
// <h3 className="text-gray-900 text-base font-medium leading-snug line-clamp-2">
// {product.name}
// </h3>
// {/* Price */}
// <div className="mt-2 flex items-center gap-2">
// <span className="text-lg font-semibold text-gray-900">₹{price}</span>
// {discount > 0 && (
// <span className="text-sm text-gray-400 line-through">
// ₹{product.compareAtPrice}
// </span>
// )}
// </div>
// {/* Minimal hover accent */}
// <div
// className="mt-3 h-[2px] w-0 bg-primary-dark
// transition-all duration-500
// group-hover:w-10"
// />
// </div>
// </Link>
// );
// };
// export default NewArrivalCard;
import React from "react";
import { Link } from "react-router-dom";
const NewArrivalCard = ({ product }) => {
const image =
product?.variants?.[0]?.images?.[0] ||
product?.images?.gallery?.[0] ||
product?.images?.primary ||
"/placeholder-product.png";
const price = product?.variants?.[0]?.price || product?.basePrice;
const hasDiscount =
product?.compareAtPrice && product.compareAtPrice > price;
const discount = hasDiscount
? Math.round(
((product.compareAtPrice - price) / product.compareAtPrice) * 100
)
: 0;
return (
<Link
to={`/product/${product.slug}`}
className="group relative block rounded-3xl overflow-hidden bg-white
border border-gray-100
transition-all duration-500
hover:-translate-y-1 hover:shadow-2xl"
>
{/* IMAGE */}
<div className="relative aspect-[3/4] overflow-hidden bg-gray-100">
<img
src={image}
alt={product.name}
className="w-full h-full object-cover
transition-transform duration-700
group-hover:scale-110"
/>
{/* GRADIENT OVERLAY */}
<div className="absolute inset-0 bg-gradient-to-t from-black/30 via-black/5 to-transparent" />
{/* BADGES */}
<div className="absolute top-3 left-3 flex flex-col gap-2">
{/* NEW */}
<span className="bg-black/80 text-white text-[11px] font-semibold px-3 py-1 rounded-full backdrop-blur">
NEW IN
</span>
{/* DISCOUNT */}
{hasDiscount && (
<span className="bg-yellow-700 text-white text-[11px] font-semibold px-3 py-1 rounded-full shadow">
{discount}% OFF
</span>
)}
</div>
</div>
{/* CONTENT */}
<div className="p-4 space-y-1">
<h3 className="text-gray-900 text-sm font-semibold leading-snug line-clamp-1">
{product.name}
</h3>
{/* DESCRIPTION */}
<p className="text-xs text-gray-500 line-clamp-2">
{product.shortDescription || product.description}
</p>
{/* PRICE */}
<div className="flex items-center gap-2 pt-2">
<span className="text-base font-bold text-gray-900">{price}</span>
{hasDiscount && (
<span className="text-sm text-gray-400 line-through">
{product.compareAtPrice}
</span>
)}
</div>
{/* CTA HINT */}
<div className="mt-3 flex items-center gap-2 text-xs font-semibold text-gray-900 opacity-0 group-hover:opacity-100 transition">
<span>View Product</span>
<span className="inline-block h-[2px] w-6 bg-gray-900" />
</div>
</div>
</Link>
);
};
export default NewArrivalCard;

View File

@@ -0,0 +1,61 @@
import React from "react";
import { useGetNewArrivalsQuery } from "../../../features/products/productsAPI";
import NewArrivalCard from "./NewArrivalCard";
import ProductGridSkeleton from "../../skeletons/ProductGridSkeleton";
import EmptyState from "../../common/EmptyState";
const NewGems = () => {
const { data, isLoading, isError } = useGetNewArrivalsQuery(8);
const products = data?.data || [];
return (
<section className="bg-[#9FAB57] pb-16">
<div className="max-w-7xl mx-auto px-6">
{/* Banner Image Centered & Smaller */}
<div className="mb-8">
<img
src="/most-loved-banner.png"
alt="Most Loved"
className="mx-auto w-80 max-w-md rounded-lg"
/>
</div>
{/* Heading */}
<div className="text-center mb-14">
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900">
New Arrivals
</h2>
<p className="text-gray-100 mt-2 text-lg">Fresh picks just for you</p>
</div>
{/* ✅ Loading */}
{isLoading && <ProductGridSkeleton count={8} />}
{/* ❌ Error */}
{isError && (
<EmptyState
title="Failed to load products"
description="Please try again later"
variant="error"
icon="⚠️"
/>
)}
{/* ✅ Products */}
{!isLoading && !isError && (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-8">
{products.map((product) => (
<NewArrivalCard key={product._id} product={product} />
))}
</div>
)}
</div>
{/* </div> */}
</section>
);
};
export default NewGems;

View File

@@ -0,0 +1,157 @@
// import React from "react";
// import { useGetPersonalizedRecommendationsQuery } from "../../../features/products/productsAPI";
// const RecommendedForYou = ({ limit = 10 }) => {
// const token = localStorage.getItem("token");
// const { data: products = [], isLoading } =
// useGetPersonalizedRecommendationsQuery(limit, {
// skip: !token, // skip query if not logged in
// });
// if (!token) return null; // hide if user not logged in
// if (isLoading)
// return <p className="text-center my-4">Loading recommendations...</p>;
// if (!products.length)
// return <p className="text-center my-4">No recommendations found.</p>;
// return (
// <section className="bg-[#8C3B2E] px-5 md:px-20 py-12">
// <div className="max-w-7xl mx-auto px-6">
// {/* <h2 className="text-2xl font-semibold mb-4 text-[#f03861]">
// Recommended for You
// </h2> */}
// <div className="text-center mb-14">
// <h2 className="text-3xl sm:text-4xl font-bold text-gray-900">
// Recommended for You
// </h2>
// <p className="text-gray-100 mt-2 text-lg">
// Continue shopping from where you left off
// </p>
// </div>
// <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
// {products.map((product) => (
// <div
// key={product._id}
// className="border rounded-lg overflow-hidden shadow hover:shadow-lg transition p-2"
// >
// <img
// src={product.images.primary || product.images.gallery[0]}
// alt={product.name}
// className="w-full h-56 object-cover mb-2"
// />
// <h3 className="font-medium text-lg">{product.name}</h3>
// <p className="text-sm text-gray-600 mb-1">
// {product.description.slice(0, 60)}...
// </p>
// <p className="text-[#f03861] font-semibold">
// ₹{product.basePrice}
// </p>
// </div>
// ))}
// </div>
// </div>
// </section>
// );
// };
// export default RecommendedForYou;
import { Link } from "react-router-dom";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination } from "swiper/modules";
import { useGetPersonalizedRecommendationsQuery } from "../../../features/products/productsAPI";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "../../../styles/recommendedSwiper.css";
const RecommendedForYou = ({ limit = 10 }) => {
const token = localStorage.getItem("token");
const { data: products = [], isLoading } =
useGetPersonalizedRecommendationsQuery(limit, {
skip: !token,
});
if (!token) return null;
if (isLoading)
return <p className="text-center mt-10 text-gray-500">Loading...</p>;
if (!products.length) return null;
return (
<section className="mt-24">
{/* Heading */}
<div className="text-center mb-10">
<h2 className="text-3xl font-bold text-[#f03861]">
Recommended for You
</h2>
<p className="text-gray-500 mt-2">
Continue shopping from where you left off
</p>
</div>
<Swiper
modules={[Navigation, Pagination]}
navigation
pagination={{ clickable: true }}
spaceBetween={28}
slidesPerView={1.2}
style={{ padding: "0 56px" }}
breakpoints={{
640: { slidesPerView: 2 },
768: { slidesPerView: 3 },
1024: { slidesPerView: 4 },
}}
className="recommended-swiper"
>
{products.map((product) => (
<SwiperSlide key={product._id}>
<Link
to={`/product/${product.slug}`}
className="block bg-white rounded-2xl overflow-hidden
shadow-sm hover:shadow-2xl transition-all duration-300
hover:-translate-y-1"
>
{/* Image */}
<div className="relative h-80 overflow-hidden">
{product.isFeatured && (
<span
className="absolute top-3 left-3 z-10
bg-[#f03861] text-white text-xs
font-semibold px-3 py-1 rounded-full shadow"
>
Featured
</span>
)}
<img
src={
product.images?.primary ||
product.images?.gallery?.[0] ||
"/placeholder.jpg"
}
alt={product.name}
className="w-full h-full object-cover
hover:scale-105 transition-transform duration-500"
/>
</div>
{/* Content */}
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 truncate">
{product.name}
</h3>
<p className="mt-1 text-lg font-bold text-gray-900">
{product.basePrice}
</p>
</div>
</Link>
</SwiperSlide>
))}
</Swiper>
</section>
);
};
export default RecommendedForYou;

View File

@@ -0,0 +1,166 @@
import { Link } from "react-router-dom";
import { Facebook, Instagram, Youtube, Twitter } from "lucide-react";
const Footer = () => {
return (
<footer className="bg-[#1A1A1A] text-gray-300 pt-12 pb-8 px-4 sm:px-10 md:px-20">
{/* Newsletter */}
<div className="max-w-6xl mx-auto text-center mb-12 px-2">
<h2 className="text-xl sm:text-2xl md:text-3xl font-semibold text-white">
Join Our Style Circle
</h2>
<p className="text-gray-400 mt-2 text-sm sm:text-base">
Be the first to know about new gems & exclusive ethnic collections.
</p>
{/* Responsive Input */}
<div className="mt-6 flex flex-col sm:flex-row items-center justify-center gap-3 w-full px-4">
<input
type="email"
placeholder="Enter your email"
className="w-full sm:w-80 md:w-96 px-4 py-3 rounded-lg sm:rounded-l-lg sm:rounded-r-none outline-none text-gray-900"
/>
<button
className="
w-full sm:w-auto
bg-[#D4C67A] text-black
px-6 py-3 rounded-lg sm:rounded-r-lg sm:rounded-l-none
font-medium text-sm sm:text-base
hover:bg-[#c1b36b] transition
"
>
Subscribe
</button>
</div>
</div>
{/* Main Footer Links */}
<div className="max-w-6xl mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-10 pb-12 border-b border-gray-700 px-4">
<div>
<h3 className="text-white font-semibold text-lg mb-3">Shop</h3>
<ul className="space-y-2 text-gray-400 text-sm sm:text-base">
<li>
<Link to="/sarees" className="hover:text-[#D4C67A]">
Sarees
</Link>
</li>
<li>
<Link to="/badhni" className="hover:text-[#D4C67A]">
Bandhani
</Link>
</li>
<li>
<Link to="/patola" className="hover:text-[#D4C67A]">
Patola
</Link>
</li>
<li>
<Link to="/lehenga" className="hover:text-[#D4C67A]">
Lehengas
</Link>
</li>
</ul>
</div>
<div>
<h3 className="text-white font-semibold text-lg mb-3">
Customer Care
</h3>
<ul className="space-y-2 text-gray-400 text-sm sm:text-base">
<li>
<Link to="/contact" className="hover:text-[#D4C67A]">
Contact Us
</Link>
</li>
<li>
<Link to="/faq" className="hover:text-[#D4C67A]">
FAQs
</Link>
</li>
<li>
<Link to="/returns" className="hover:text-[#D4C67A]">
Return & Exchange
</Link>
</li>
<li>
<Link to="/shipping" className="hover:text-[#D4C67A]">
Shipping Info
</Link>
</li>
</ul>
</div>
<div>
<h3 className="text-white font-semibold text-lg mb-3">Information</h3>
<ul className="space-y-2 text-gray-400 text-sm sm:text-base">
<li>
<Link to="/about" className="hover:text-[#D4C67A]">
About Us
</Link>
</li>
<li>
<Link to="/blog" className="hover:text-[#D4C67A]">
Blog
</Link>
</li>
<li>
<Link to="/privacy-policy" className="hover:text-[#D4C67A]">
Privacy Policy
</Link>
</li>
<li>
<Link to="/terms" className="hover:text-[#D4C67A]">
Terms & Conditions
</Link>
</li>
</ul>
</div>
{/* Social Section */}
<div>
<h3 className="text-white font-semibold text-lg mb-3">Follow Us</h3>
<div className="flex space-x-4 mt-2">
<a
href="#"
className="p-2 bg-gray-700 rounded-full hover:bg-[#D4C67A] hover:text-black transition"
>
<Facebook size={20} />
</a>
<a
href="#"
className="p-2 bg-gray-700 rounded-full hover:bg-[#D4C67A] hover:text-black transition"
>
<Instagram size={20} />
</a>
<a
href="#"
className="p-2 bg-gray-700 rounded-full hover:bg-[#D4C67A] hover:text-black transition"
>
<Youtube size={20} />
</a>
<a
href="#"
className="p-2 bg-gray-700 rounded-full hover:bg-[#D4C67A] hover:text-black transition"
>
<Twitter size={20} />
</a>
</div>
</div>
</div>
{/* Bottom Strip */}
<div className="max-w-6xl mx-auto text-center text-xs sm:text-sm text-gray-500 pt-6">
<p>
© {new Date().getFullYear()}
<span className="text-[#D4C67A]"> Vaishnavi Collections</span>. All
Rights Reserved.
</p>
<p className="mt-2">Crafted with for your ethnic elegance.</p>
</div>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,18 @@
import React from "react";
import { Bell } from "lucide-react";
import { Link } from "react-router-dom";
const BellIcon = ({ notificationCount = 0 }) => {
return (
<Link to="/notifications" className="relative">
<Bell className="w-6 h-6 text-gray-700 hover:text-primary transition" />
{notificationCount > 0 && (
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
{notificationCount}
</span>
)}
</Link>
);
};
export default BellIcon;

View File

@@ -0,0 +1,46 @@
// import React from "react";
// import { ShoppingCart } from "lucide-react";
// import { Link } from "react-router-dom";
// const CartIcon = ({ itemCount = 0 }) => {
// return (
// <Link to="/cart" className="relative">
// <ShoppingCart className="w-6 h-6 text-gray-700 hover:text-primary transition" />
// {itemCount > 0 && (
// <span className="absolute -top-2 -right-2 bg-primary text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
// {itemCount}
// </span>
// )}
// </Link>
// );
// };
// export default CartIcon;
import { ShoppingCart } from "lucide-react";
import { Link } from "react-router-dom";
import { useGetCartItemsQuery } from "../../features/cart/cartAPI";
const CartIcon = ({ className = "" }) => {
const { data } = useGetCartItemsQuery();
const cartCount = data?.data?.items?.length || 0;
return (
<Link to="/cart" className="relative">
<ShoppingCart
className={`w-6 h-6 text-gray-700 hover:text-rose-500 ${className}`}
/>
{cartCount > 0 && (
<span className="absolute -top-2 -right-2 bg-rose-500 text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
{cartCount}
</span>
)}
</Link>
);
};
export default CartIcon;

View File

@@ -0,0 +1,21 @@
import { Link } from "react-router-dom";
const Logo = () => (
<Link to="/">
{/* Desktop Full Logo */}
<img
src="/logo.png"
alt="E-Shop Logo"
className="hidden md:block h-36 w-auto"
/>
{/* Mobile Small Logo */}
<img
src="/logo-small.png"
alt="E-Shop Logo"
className="block md:hidden h-8 w-auto"
/>
</Link>
);
export default Logo;

View File

@@ -0,0 +1,144 @@
import { Link } from "react-router-dom";
import { useState } from "react";
import { useGetCategoryTreeQuery } from "../../features/categories/categoryApi";
const navItems = [
{ id: "home", label: "Home", href: "/" },
{ id: "collections", label: "Collections", href: "#", hasDropdown: true },
{ id: "wardrobe", label: "My Wardrobe", href: "/wardrobe" },
{ id: "calendar", label: "Style Calendar", href: "#" },
{ id: "trending", label: "Trending", href: "/trending" },
];
// const megaMenu = {
// men: [
// { title: "Casual Wear", items: ["T-Shirts", "Polos", "Shirts", "Jackets"] },
// { title: "Formal Wear", items: ["Shirts", "Trousers", "Blazers"] },
// {
// title: "Indian & Festive",
// items: ["Kurtas", "Kurta Sets", "Nehru Jackets"],
// },
// { title: "Winterwear", items: ["Sweaters", "Jackets", "Thermals"] },
// ],
// women: [
// { title: "Westernwear", items: ["Dresses", "Tops", "Shrugs", "Jeans"] },
// // { title: "Indian & Fusion", items: ["Kurtas", "Ethnic Skirts", "Sarees"] },
// {
// title: "Indian & Fusion",
// items: [
// { label: "Sarees", href: "/sarees" },
// { label: "Badhni", href: "/badhni" },
// { label: "Patola", href: "/patola" },
// { label: "Lehengas", href: "/lehenga" },
// ],
// },
// { title: "Winterwear", items: ["Sweaters", "Thermals"] },
// { title: "Athleisure", items: ["Sports Bra", "Tights", "Track Pants"] },
// ],
// kids: [
// { title: "Girls", items: ["Dresses", "T-Shirts", "Jeans", "Ethnic Wear"] },
// { title: "Boys", items: ["Shirts", "Shorts", "Jeans", "Winterwear"] },
// ],
// accessories: [
// { title: "Footwear", items: ["Shoes", "Sandals"] },
// { title: "Accessories", items: ["Belts", "Wallets", "Caps"] },
// ],
// };
const MenuLinks = () => {
// const [megaMenu, setMegaMenu] = useState(null);
const [activeDropdown, setActiveDropdown] = useState(null);
// const [activeTab, setActiveTab] = useState("men");
const { data } = useGetCategoryTreeQuery();
const categories = data?.data || [];
const [activeTab, setActiveTab] = useState("men");
// find active category object
const activeCategory = categories.find((cat) => cat.slug === activeTab);
return (
<div className="hidden md:flex items-center space-x-8 font-medium">
{navItems.map((item) => (
<div
key={item.id}
className="relative"
onMouseEnter={() => item.hasDropdown && setActiveDropdown(item.id)}
onMouseLeave={() => item.hasDropdown && setActiveDropdown(null)}
>
<Link
to={item.href}
className="relative text-gray-700 hover:text-primary-default transition font-medium"
>
{item.label}
<span className="absolute left-0 -bottom-1 w-0 h-0.5 bg-primary-default transition-all duration-300 group-hover:w-full"></span>
</Link>
{/* Dropdown */}
{/* Dropdown */}
{item.hasDropdown && activeDropdown === item.id && (
<>
{/* Invisible Hover Bridge (prevents dropdown from closing) */}
<div className="absolute left-0 top-full w-full h-4"></div>
<div
className="
absolute left-0 top-[100%] mt-4 w-[950px]
bg-white/70 backdrop-blur-xl
rounded-2xl border border-gray-200
shadow-[0_12px_40px_rgba(0,0,0,0.12)]
p-6 z-50 animate-slideDownFade
"
>
{/* Tabs */}
<div className="flex space-x-6 border-b pb-3">
{categories.map((cat) => (
<button
key={cat.id}
onMouseEnter={() => setActiveTab(cat.slug)}
className={`capitalize pb-2 px-2 font-semibold text-sm
${
activeTab === cat.slug
? "text-primary-default border-b-2 border-primary-default"
: "text-gray-500 hover:text-primary-default"
}`}
>
{cat.name}
</button>
))}
</div>
{/* Mega Menu Grid */}
<div className="grid grid-cols-4 gap-8 pt-5">
{activeCategory?.children?.map((section) => (
<div key={section.id}>
<h4 className="text-gray-800 font-semibold mb-3 text-sm">
{section.name}
</h4>
<ul className="space-y-2">
{section.children?.map((sub) => (
<li key={sub.id}>
<Link
to={`/product/${activeCategory.slug}/${section.slug}/${sub.slug}`}
className="text-gray-600 hover:text-primary-default text-sm"
>
{sub.name}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</>
)}
</div>
))}
</div>
);
};
export default MenuLinks;

View File

@@ -0,0 +1,132 @@
import { useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { Link } from "react-router-dom";
const MobileMenu = ({ isOpen, closeMenu, navItems, megaMenu }) => {
const [openSection, setOpenSection] = useState(null);
if (!isOpen) return null;
const toggle = (key) => {
setOpenSection(openSection === key ? null : key);
};
return (
<div className="md:hidden bg-white shadow-lg border-t z-50">
<div className="px-4 py-3 space-y-2">
{/* MAIN NAV ITEMS */}
{navItems.map((item) => (
<div key={item.id}>
{/* If dropdown exists */}
{item.hasDropdown ? (
<>
<button
onClick={() => toggle(item.id)}
className="w-full flex justify-between items-center py-3 text-left font-semibold text-gray-900"
>
{item.label}
{openSection === item.id ? (
<ChevronUp size={20} />
) : (
<ChevronDown size={20} />
)}
</button>
{/* DROPDOWN MEGA MENU */}
<div
className={`transition-all overflow-hidden duration-300 ${
openSection === item.id
? "max-h-[700px] opacity-100"
: "max-h-0 opacity-0"
}`}
>
{/* MEN */}
<AccordionBlock
title="Men"
items={megaMenu.men}
closeMenu={closeMenu}
/>
{/* WOMEN */}
<AccordionBlock
title="Women"
items={megaMenu.women}
closeMenu={closeMenu}
/>
{/* KIDS */}
<AccordionBlock
title="Kids"
items={megaMenu.kids}
closeMenu={closeMenu}
/>
{/* ACCESSORIES */}
<AccordionBlock
title="Accessories"
items={megaMenu.accessories}
closeMenu={closeMenu}
/>
</div>
</>
) : (
<Link
to={item.href}
onClick={closeMenu}
className="block py-3 font-semibold text-gray-900 rounded hover:bg-gray-100"
>
{item.label}
</Link>
)}
</div>
))}
</div>
</div>
);
};
export default MobileMenu;
/* ---------------------------------------------
Sub Menu Accordion (MEN / WOMEN / KIDS...)
---------------------------------------------- */
const AccordionBlock = ({ title, items, closeMenu }) => {
const [open, setOpen] = useState(false);
return (
<div className="border-b">
<button
onClick={() => setOpen(!open)}
className="w-full flex justify-between items-center py-2 font-medium text-gray-800"
>
{title}
{open ? <ChevronUp size={18} /> : <ChevronDown size={18} />}
</button>
<div
className={`transition-all duration-300 overflow-hidden ${
open ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0"
}`}
>
{items.map((block, idx) => (
<div key={idx} className="pl-4 py-2">
<h4 className="font-semibold text-gray-700">{block.title}</h4>
<div className="mt-1 space-y-1">
{block.items.map((item, index) => (
<Link
key={index}
to={item.href}
onClick={closeMenu}
className="block text-sm text-gray-600 hover:text-rose-500"
>
{item.label}
</Link>
))}
</div>
</div>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,145 @@
import React, { useState } from "react";
import Logo from "./Logo";
import MenuLinks from "./MenuLinks";
import CartIcon from "./CartIcon";
import UserIcon from "./UserIcon";
import BellIcon from "./BellIcon";
import MobileMenu from "./MobileMenu";
import SearchBar from "./SearchBar";
import TopPromoBanner from "./TopPromoBanner";
import { Menu, X, Heart } from "lucide-react";
import { Link } from "react-router-dom";
import { useGetCategoryTreeQuery } from "../../features/categories/categoryApi";
import { formatMegaMenu } from "../../utils/formatMegaMenu";
// ----------------------------
// NAV ITEMS
// ----------------------------
const navItems = [
{ id: "home", label: "Home", href: "/" },
{ id: "collections", label: "Collections", href: "#", hasDropdown: true },
{ id: "wardrobe", label: "My Wardrobe", href: "/wardrobe" },
{ id: "calendar", label: "Style Calendar", href: "#" },
{ id: "trending", label: "Trending", href: "#" },
];
// ----------------------------
// MEGA MENU DATA
// ----------------------------
// const megaMenu = {
// men: [
// { title: "Casual Wear", items: ["T-Shirts", "Polos", "Shirts", "Jackets"] },
// { title: "Formal Wear", items: ["Shirts", "Trousers", "Blazers"] },
// {
// title: "Indian & Festive",
// items: ["Kurtas", "Kurta Sets", "Nehru Jackets"],
// },
// { title: "Winterwear", items: ["Sweaters", "Jackets", "Thermals"] },
// ],
// women: [
// { title: "Westernwear", items: ["Dresses", "Tops", "Shrugs", "Jeans"] },
// {
// title: "Indian & Fusion",
// items: [
// { label: "Sarees", href: "/sarees" },
// { label: "Badhni", href: "/badhni" },
// { label: "Patola", href: "/patola" },
// { label: "Lehengas", href: "/lehenga" },
// ],
// },
// { title: "Winterwear", items: ["Sweaters", "Thermals"] },
// { title: "Athleisure", items: ["Sports Bra", "Tights", "Track Pants"] },
// ],
// kids: [
// { title: "Girls", items: ["Dresses", "T-Shirts", "Jeans", "Ethnic Wear"] },
// { title: "Boys", items: ["Shirts", "Shorts", "Jeans", "Winterwear"] },
// ],
// accessories: [
// { title: "Footwear", items: ["Shoes", "Sandals"] },
// { title: "Accessories", items: ["Belts", "Wallets", "Caps"] },
// ],
// };
const Navbar = () => {
const [menuOpen, setMenuOpen] = useState(false);
const toggleMenu = () => setMenuOpen(!menuOpen);
// const [menuOpen, setMenuOpen] = useState(false);
const { data } = useGetCategoryTreeQuery();
const megaMenu = data ? formatMegaMenu(data.data) : {};
// const toggleMenu = () => setMenuOpen(!menuOpen);
return (
<nav className="bg-white shadow-md fixed w-full z-50">
<TopPromoBanner />
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<div className="flex-shrink-0">
<Logo />
</div>
{/* Desktop Menu + Search */}
<div className="hidden md:flex flex-1 items-center justify-between mx-4">
<MenuLinks />
<SearchBar />
</div>
{/* Icons */}
<div className="flex items-center space-x-3 md:space-x-4">
{/* Desktop Icons */}
<div className="hidden md:flex items-center space-x-5 h-10">
<Link to="/wishlist">
<Heart className="w-6 h-6 cursor-pointer text-gray-700 hover:text-rose-500" />
</Link>
<BellIcon notificationCount={3} />
<Link to="/cart">
<CartIcon className="w-6 h-6 cursor-pointer text-gray-700 hover:text-rose-500" />
</Link>
<UserIcon />
</div>
{/* Mobile */}
<div className="flex md:hidden items-center space-x-4 h-10">
<Link to="/wishlist">
<Heart className="w-5 h-5 text-gray-700 hover:text-rose-500 cursor-pointer" />
</Link>
<CartIcon />
<UserIcon />
<BellIcon notificationCount={3} />
<button onClick={toggleMenu}>
{menuOpen ? (
<X className="w-6 h-6 text-gray-700" />
) : (
<Menu className="w-6 h-6 text-gray-700" />
)}
</button>
</div>
{/* Mobile Icons */}
</div>
</div>
</div>
{/* Mobile Menu */}
<MobileMenu
isOpen={menuOpen}
closeMenu={() => setMenuOpen(false)}
navItems={navItems}
megaMenu={megaMenu}
/>
</nav>
);
};
export default Navbar;

View File

@@ -0,0 +1,20 @@
import { Search } from "lucide-react";
const SearchBar = () => {
return (
<div className="flex flex-1 max-w-sm mx-4">
<div className="relative w-full">
<input
type="text"
placeholder="Search products, brands..."
className="w-full h-10 pl-10 pr-4 rounded-full border border-gray-300 text-sm sm:text-base
focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary
transition-shadow duration-300 shadow-sm hover:shadow-md"
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
</div>
</div>
);
};
export default SearchBar;

View File

@@ -0,0 +1,124 @@
// import React, { useState, useEffect } from "react";
// import { Sparkles, Play, HelpCircle, ChevronRight } from "lucide-react";
// const lines = [
// "🎉 Limited Time Offer Extra 15% OFF!",
// "🔥 Shop New Winter Collection Now!",
// "💝 Use Code: FASHION15 & Save More!",
// ];
// const TopPromoBanner = () => {
// const [current, setCurrent] = useState(0);
// useEffect(() => {
// const interval = setInterval(() => {
// setCurrent((prev) => (prev + 1) % lines.length);
// }, 2500);
// return () => clearInterval(interval);
// }, []);
// return (
// <div className="relative w-full bg-gradient-to-r from-pink-600 via-rose-500 to-pink-600 text-white py-2 shadow-md z-50 overflow-hidden">
// {/* Shiny moving light */}
// <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shine" />
// <div className="relative max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between px-4 gap-2">
// {/* LEFT — Rotating Text */}
// <div className="flex items-center gap-2 flex-1 min-w-0">
// <Sparkles className="w-4 h-4 text-yellow-300 animate-pulse flex-shrink-0" />
// <p className="text-xs sm:text-sm md:text-base font-medium truncate">
// {lines[current]}
// </p>
// </div>
// {/* RIGHT — Icons / Menu */}
// <div className="flex flex-wrap sm:flex-nowrap items-center gap-4 text-xs sm:text-sm font-medium justify-center sm:justify-end w-full sm:w-auto">
// <button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
// First Citizen Club <ChevronRight size={12} />
// </button>
// <button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
// <HelpCircle size={14} />
// Help & Support
// </button>
// <button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
// <Play size={14} />
// App Download
// </button>
// </div>
// </div>
// </div>
// );
// };
// export default TopPromoBanner;
import React, { useState, useEffect } from "react";
import { Sparkles, Play, HelpCircle, ChevronRight } from "lucide-react";
const lines = [
"🎉 Limited Time Offer Extra 15% OFF!",
"🔥 Shop New Winter Collection Now!",
"💝 Use Code: FASHION15 & Save More!",
];
const TopPromoBanner = () => {
const [current, setCurrent] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCurrent((prev) => (prev + 1) % lines.length);
}, 2500);
return () => clearInterval(interval);
}, []);
return (
<div className="relative w-full bg-gradient-to-r from-pink-600 via-rose-500 to-pink-600 text-white py-2 shadow-md z-50 overflow-hidden">
{/* Shiny moving light */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shine" />
<div className="relative max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-center sm:justify-between px-4 gap-2">
{/* LEFT — Rotating Text */}
<div className="flex items-center gap-2 flex-1 min-w-0">
<Sparkles className="w-4 h-4 text-yellow-300 animate-pulse flex-shrink-0" />
<p className="text-xs sm:text-sm md:text-base font-medium truncate">
{lines[current]}
</p>
</div>
{/* RIGHT — MOBILE: ONLY ICONS */}
<div className="flex items-center gap-4 text-xs sm:hidden">
<Sparkles size={16} className="text-yellow-200" />
<HelpCircle size={16} />
<Play size={16} />
</div>
{/* RIGHT — DESKTOP: FULL TEXT MENU */}
<div className="hidden sm:flex flex-wrap sm:flex-nowrap items-center gap-4 text-xs sm:text-sm font-medium justify-end">
<button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
First Citizen Club <ChevronRight size={12} />
</button>
<button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
<HelpCircle size={14} />
Help & Support
</button>
<button className="hover:text-yellow-200 transition flex items-center gap-1 whitespace-nowrap">
<Play size={14} />
App Download
</button>
</div>
</div>
</div>
);
};
export default TopPromoBanner;

View File

@@ -0,0 +1,80 @@
import React, { useState } from "react";
import { User } from "lucide-react";
import { useSelector, useDispatch } from "react-redux";
import { Link, useNavigate } from "react-router-dom";
import { logout } from "../../features/auth/authSlice";
const UserIcon = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { user } = useSelector((state) => state.auth);
const [open, setOpen] = useState(false);
const handleLogout = () => {
dispatch(logout());
localStorage.removeItem("token");
localStorage.removeItem("refreshToken");
localStorage.removeItem("user");
setOpen(false);
};
const handleUserClick = () => {
if (!user) {
navigate("/login");
return;
}
setOpen(!open);
};
return (
<div className="relative">
{/* CLICKABLE SECTION */}
<div
onClick={handleUserClick}
className="cursor-pointer flex items-center space-x-1"
>
<User className="w-6 h-6 text-gray-700 hover:text-primary" />
{user && (
<span className="text-sm font-medium text-gray-700 hover:text-primary">
{user.name}
</span>
)}
</div>
{/* DROPDOWN */}
{user && open && (
<div className="absolute right-0 mt-3 w-52 bg-white shadow-xl rounded-lg border border-gray-200 z-50 animate-fade-in">
<div className="px-4 py-3 border-b border-gray-200">
<p className="text-gray-900 font-semibold">
{`${user.firstName || ""} ${user.lastName || ""}`.trim()}
</p>
</div>
<Link
to="/profile"
className="block px-4 py-3 text-gray-700 hover:bg-primary-default hover:text-white transition"
>
Profile
</Link>
<Link
to="/my-orders"
className="block px-4 py-3 text-gray-700 hover:bg-primary-default hover:text-white transition"
>
My Order
</Link>
<button
onClick={handleLogout}
className="w-full text-left px-4 py-3 text-gray-700 hover:bg-red-500 hover:text-white transition"
>
Logout
</button>
</div>
)}
</div>
);
};
export default UserIcon;

View File

@@ -0,0 +1,353 @@
// import React, { useState } from "react";
// import {
// useGetWishlistQuery,
// useRemoveFromWishlistMutation,
// } from "../../features/wishlist/wishlistApi";
// import WishlistEmpty from "./WishlistEmpty";
// import AddToCart from "../../pages/Cart/AddToCart";
// import { useAddToCartMutation } from "../../features/cart/cartAPI";
// import { useNavigate } from "react-router-dom";
// const Wishlist = () => {
// const navigate = useNavigate();
// const [addToCart] = useAddToCartMutation();
// const { data, isLoading, isError } = useGetWishlistQuery();
// const [removeFromWishlist] = useRemoveFromWishlistMutation();
// const [confirmModal, setConfirmModal] = useState({
// show: false,
// productId: null,
// productName: "",
// });
// const wishlistItems = data?.data?.wishlist || [];
// // Loading
// if (isLoading)
// return (
// <p className="text-center mt-20 text-xl font-medium text-gray-600">
// Loading your wishlist...
// </p>
// );
// if (isError) return <WishlistEmpty />;
// // Empty
// if (wishlistItems.length === 0) return <WishlistEmpty />;
// const handleRemove = async (productId) => {
// try {
// await removeFromWishlist(productId).unwrap();
// setConfirmModal({ show: false, productId: null, productName: "" });
// } catch (error) {
// console.log("Remove failed", error);
// }
// };
// const fallbackImage = "/default-product.png";
// return (
// <div className="max-w-7xl mx-auto px-4 py-12 mt-24 bg-gradient-to-br from-gray-50 to-gray-100">
// <h1 className="text-2xl font-extrabold mb-10 text-center text-gray-800 tracking-tight">
// My Wishlist
// </h1>
// <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
// {wishlistItems.map((item) => {
// const image =
// item?.product?.images?.gallery?.[0] ||
// item?.product?.variants?.[0]?.images?.[0] ||
// fallbackImage;
// return (
// <div
// key={item.id}
// className="bg-white rounded-2xl shadow-md hover:shadow-lg flex flex-col"
// >
// {/* Product Image */}
// <div className="relative h-44 bg-gray-100 overflow-hidden group">
// <img
// src={image}
// onError={(e) => (e.target.src = fallbackImage)}
// alt={item?.product?.name || "Product"}
// className="object-cover h-full w-full transition-transform duration-500 group-hover:scale-105"
// />
// {/* Remove Button */}
// <button
// onClick={() =>
// setConfirmModal({
// show: true,
// productId: item.productId,
// productName: item?.product?.name || "this product",
// })
// }
// className="absolute top-3 right-3 bg-white shadow p-1.5 rounded-full hover:bg-red-500 hover:text-white transition-all text-sm"
// >
// ✖
// </button>
// </div>
// {/* Product Info */}
// <div className="p-4 flex flex-col flex-1">
// <h2 className="text-md font-semibold text-gray-800 mb-1 truncate">
// {item?.product?.name || "Unknown Product"}
// </h2>
// {/* Price */}
// {item?.product?.basePrice && (
// <p className="text-lg font-bold text-primary-dark mb-1">
// ₹ {item.product.basePrice.toLocaleString()}
// </p>
// )}
// {/* Added Date */}
// <p className="text-gray-500 text-xs mb-3">
// Added on{" "}
// <span className="font-medium">
// {new Date(item.createdAt).toLocaleDateString()}
// </span>
// </p>
// {/* Add To Cart Component */}
// {/* Buttons */}
// <div className="mt-auto flex justify-end w-full">
// <button
// className="text-primary-default font-semibold text-sm underline hover:text-primary-dark transition-colors"
// onClick={async () => {
// const token = localStorage.getItem("token");
// if (!token) {
// alert("Please login first to add product to cart");
// navigate("/login");
// return;
// }
// try {
// // 1⃣ Add to Cart API call
// await addToCart({
// productId: item.productId,
// quantity: 1,
// }).unwrap();
// // 2⃣ Remove from Wishlist API call
// await removeFromWishlist(item.productId).unwrap();
// // 3⃣ Redirect to Cart page
// navigate("/cart");
// } catch (err) {
// console.log("Error:", err);
// }
// }}
// >
// Add To Bag
// </button>
// </div>
// </div>
// </div>
// );
// })}
// </div>
// {/* Confirmation Modal */}
// {confirmModal.show && (
// <div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
// <div className="bg-white rounded-xl shadow-lg p-8 w-96 sm:w-4/5 max-w-md text-center">
// <h3 className="text-base font-semibold mb-6">
// Are you sure you want to delete <br />
// <span className="text-amber-800">
// {confirmModal.productName}
// </span>{" "}
// from your wishlist?
// </h3>
// <div className="flex justify-center gap-6 mt-6">
// <button
// onClick={() => handleRemove(confirmModal.productId)}
// className="px-6 py-3 bg-primary-default text-white rounded-lg font-semibold hover:bg-primary-dark transition"
// >
// Yes
// </button>
// <button
// onClick={() =>
// setConfirmModal({
// show: false,
// productId: null,
// productName: "",
// })
// }
// className="px-6 py-3 bg-gray-300 text-gray-800 rounded-lg font-semibold hover:bg-gray-400 transition"
// >
// No
// </button>
// </div>
// </div>
// </div>
// )}
// </div>
// );
// };
// export default Wishlist;
import React, { useState } from "react";
import {
useGetWishlistQuery,
useRemoveFromWishlistMutation,
} from "../../features/wishlist/wishlistApi";
import { useAddToCartMutation } from "../../features/cart/cartAPI";
import WishlistEmpty from "./WishlistEmpty";
import { useNavigate } from "react-router-dom";
const Wishlist = () => {
const navigate = useNavigate();
const { data, isLoading, isError } = useGetWishlistQuery();
const [addToCart] = useAddToCartMutation();
const [removeFromWishlist] = useRemoveFromWishlistMutation();
const [confirm, setConfirm] = useState(null);
const wishlist = data?.data?.wishlist || [];
const fallbackImage = "/default-product.png";
if (isLoading) {
return (
<div className="mt-32 text-center text-gray-500 text-lg">
Loading wishlist
</div>
);
}
if (isError || wishlist.length === 0) {
return <WishlistEmpty />;
}
const handleAddToCart = async (item) => {
const token = localStorage.getItem("token");
if (!token) {
alert("Please login first to add product to cart");
navigate("/login");
return;
}
try {
await addToCart({
productId: item.productId,
quantity: 1,
}).unwrap();
await removeFromWishlist(item.productId).unwrap();
navigate("/cart");
} catch (err) {
console.error(err);
}
};
return (
<div className="max-w-7xl mx-auto px-4 py-12 mt-32 bg-gradient-to-br from-gray-50 to-gray-100">
<h1 className="text-2xl font-semibold text-gray-600 mb-10">
My Wishlist ({wishlist.length} items)
</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
{wishlist.map((item) => {
const image =
item?.product?.images?.gallery?.[0] ||
item?.product?.variants?.[0]?.images?.[0] ||
fallbackImage;
return (
<div
key={item.id}
className="border border-gray-200 rounded-lg bg-white"
>
{/* Image */}
<div className="relative h-56 bg-gray-50">
<img
src={image}
onError={(e) => (e.target.src = fallbackImage)}
alt={item?.product?.name}
className="h-full w-full object-contain p-6"
/>
{/* Remove */}
<button
onClick={() => setConfirm(item)}
className="absolute top-3 right-3 text-gray-400
hover:text-gray-700 text-sm"
>
Remove
</button>
</div>
{/* Content */}
<div className="px-5 py-4 space-y-2">
<p className="text-sm text-gray-500">
{item?.product?.category || "Product"}
</p>
<h2 className="text-base font-medium text-gray-900 line-clamp-2">
{item?.product?.name}
</h2>
<div className="flex items-center justify-between pt-3">
<p className="text-lg font-semibold text-gray-900">
{item?.product?.basePrice?.toLocaleString()}
</p>
<button
onClick={() => handleAddToCart(item)}
className="px-4 py-2 text-sm font-medium
border border-gray-900
text-gray-900
hover:bg-gray-900 hover:text-white
transition"
>
Add to Cart
</button>
</div>
</div>
</div>
);
})}
</div>
{/* Confirm Modal */}
{confirm && (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50">
<div className="bg-white w-96 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Remove item?
</h3>
<p className="text-sm text-gray-500 mb-6">
Are you sure you want to remove{" "}
<span className="font-medium text-gray-900">
{confirm.product?.name}
</span>{" "}
from your wishlist?
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => setConfirm(null)}
className="px-4 py-2 text-sm border border-gray-300"
>
Cancel
</button>
<button
onClick={async () => {
await removeFromWishlist(confirm.productId).unwrap();
setConfirm(null);
}}
className="px-4 py-2 text-sm bg-gray-900 text-white"
>
Remove
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Wishlist;

View File

@@ -0,0 +1,26 @@
import React from "react";
import { Link } from "react-router-dom";
import Button from "../common/Button";
const WishlistEmpty = () => {
return (
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-50 px-4 py-24 mt-20">
<img
src="/empty-wishlist.png"
alt="Empty Wishlist"
className="w-36 md:w-36 mb-6"
/>
<h1 className="text-3xl font-bold text-gray-800 mb-2 text-center">
Your Wishlist is Empty
</h1>
<p className="text-gray-500 mb-6 text-center max-w-sm">
Tap the heart icon on products to save them for later.
</p>
<Link to="/">
<Button>Shop Now</Button>
</Link>
</div>
);
};
export default WishlistEmpty;

View File

@@ -0,0 +1,36 @@
import { useCancelOrderMutation } from "../../features/orders/ordersApi";
const CancelOrderButton = ({ orderId, status }) => {
const [cancelOrder, { isLoading }] = useCancelOrderMutation();
const isCancelable = ["PENDING", "CONFIRMED"].includes(status);
const handleCancel = async () => {
if (!window.confirm("Are you sure you want to cancel this order?")) return;
try {
await cancelOrder(orderId).unwrap();
alert("Order cancelled successfully");
} catch (err) {
alert(err?.data?.message || "Failed to cancel order");
}
};
return (
<button
onClick={handleCancel}
disabled={!isCancelable || isLoading}
className={`px-4 py-2 rounded-xl font-medium transition
${
isCancelable
? "bg-red-600 hover:bg-red-700 text-white"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}
`}
>
{isLoading ? "Cancelling..." : "Cancel Order"}
</button>
);
};
export default CancelOrderButton;

View File

@@ -0,0 +1,134 @@
import { FaHome, FaUser, FaInfoCircle } from "react-icons/fa";
const DeliveryAndPriceCard = ({
order,
setShowAddressModal,
setShowUserModal,
}) => {
return (
<div className="space-y-6 max-w-md mx-auto ">
{/* Delivery Details */}
<div className="bg-white rounded-2xl shadow p-5 space-y-4 border">
<h2 className="text-lg font-semibold text-gray-800">
Delivery details
</h2>
{/* Address */}
<div
className="flex items-center justify-between bg-gray-50 px-4 py-3 rounded-lg cursor-pointer hover:bg-gray-100 transition"
onClick={() => setShowAddressModal(true)}
>
<div className="flex items-center gap-3">
<FaHome className="text-gray-600 text-xl" />
<p className="text-gray-700 truncate max-w-xs">
{order.address.addressLine1}, {order.address.addressLine2},{" "}
{order.address.city}...
</p>
</div>
<span className="text-gray-400">{">"}</span>
</div>
{/* User */}
<div
className="flex items-center justify-between bg-gray-50 px-4 py-3 rounded-lg cursor-pointer hover:bg-gray-100 transition"
onClick={() => setShowUserModal(true)}
>
<div className="flex items-center gap-3">
<FaUser className="text-gray-600 text-xl" />
<p className="text-gray-700 truncate max-w-xs">
{order.address.firstName} {order.address.lastName}{" "}
{order.address.phone}
</p>
</div>
<span className="text-gray-400">{">"}</span>
</div>
</div>
{/* Price Details */}
<div className="bg-white rounded-2xl shadow p-5 space-y-4 border">
<h2 className="text-lg font-semibold text-gray-800">Price details</h2>
<div className="space-y-2 text-gray-700">
<div className="flex justify-between">
<span>Listing price</span>
<span className="line-through">
{Number(order.listingPrice || order.subtotal).toFixed(2)}
</span>
</div>
<div className="flex justify-between">
<span>
Special price{" "}
<FaInfoCircle className="inline text-gray-400 ml-1" />
</span>
<span className="font-medium text-gray-900">
{Number(order.totalAmount).toFixed(2)}
</span>
</div>
<div className="flex justify-between">
<span>Shipping price</span>
<span className="line-through">
{Number(order.shippingAmount || order.shippingAmount).toFixed(2)}
</span>
</div>
<div className="flex justify-between">
<span>Discount price</span>
<span className="line-through">
{Number(order.discountAmount || order.discountAmount).toFixed(2)}
</span>
</div>
{order.taxAmount && (
<div className="flex justify-between">
<span>Total fees</span>
<span>{Number(order.taxAmount).toFixed(2)}</span>
</div>
)}
{order.discountAmount > 0 && (
<div className="flex justify-between text-green-600">
<span>Other discount</span>
<span>-{Number(order.discountAmount).toFixed(2)}</span>
</div>
)}
</div>
<div className="border-t border-gray-200 pt-3 flex justify-between font-bold text-gray-900">
<span>Total amount</span>
<span className="text-orange-800">
{Number(order.totalAmount).toFixed(2)}
</span>
</div>
{/* Payment Method */}
</div>
<div className="mt-3 flex items-center justify-between bg-gray-50 px-4 py-3 rounded-lg border ">
<span className="text-gray-700 font-medium">Payment method</span>
<span className="flex items-center gap-2 text-gray-900">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8c1.104 0 2 .896 2 2s-.896 2-2 2-2-.896-2-2 .896-2 2-2z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 2v2m0 16v2m10-10h-2M4 12H2m15.364-6.364l-1.414 1.414M6.05 17.95l-1.414 1.414m0-12.728l1.414 1.414M17.95 17.95l1.414 1.414"
/>
</svg>
Cash On Delivery
</span>
</div>
</div>
);
};
export default DeliveryAndPriceCard;

View File

@@ -0,0 +1,457 @@
// import React, { useState } from "react";
// import { useGetOrdersQuery } from "../../features/orders/ordersApi";
// const statusColor = {
// PENDING: "bg-yellow-100 text-yellow-800",
// PAID: "bg-blue-100 text-blue-800",
// SHIPPED: "bg-purple-100 text-purple-800",
// DELIVERED: "bg-green-100 text-green-800",
// CANCELLED: "bg-red-100 text-red-800",
// RETURNED: "bg-gray-100 text-gray-800",
// };
// // LEFT FILTERS
// const OrderFilters = ({ filters, setFilters }) => {
// const toggleStatus = (status) => {
// if (filters.status.includes(status)) {
// setFilters({
// ...filters,
// status: filters.status.filter((s) => s !== status),
// });
// } else {
// setFilters({ ...filters, status: [...filters.status, status] });
// }
// };
// return (
// <div className="sticky top-24 p-6 border rounded-3xl bg-white shadow-lg space-y-6">
// <h2 className="text-xl font-semibold text-gray-800">Filter Orders</h2>
// <div>
// <p className="font-medium text-gray-700 mb-2">Status</p>
// <div className="flex flex-col gap-2">
// {["PENDING", "ON_THE_WAY", "DELIVERED", "CANCELLED", "RETURNED"].map(
// (status) => (
// <label
// key={status}
// className="flex items-center gap-3 cursor-pointer hover:text-blue-600"
// >
// <input
// type="checkbox"
// checked={filters.status.includes(status)}
// onChange={() => toggleStatus(status)}
// className="h-4 w-4 accent-blue-500"
// />
// <span className="capitalize">{status.replace("_", " ")}</span>
// </label>
// )
// )}
// </div>
// </div>
// <div>
// <p className="font-medium text-gray-700 mb-2">Order Time</p>
// <select
// value={filters.time}
// onChange={(e) => setFilters({ ...filters, time: e.target.value })}
// className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:outline-none"
// >
// <option value="30_DAYS">Last 30 Days</option>
// <option value="2024">2024</option>
// <option value="2023">2023</option>
// </select>
// </div>
// </div>
// );
// };
// // ORDER CARD
// const OrderCard = ({ order }) => {
// return (
// <div className="bg-white border border-gray-200 rounded-3xl shadow hover:shadow-xl transition p-6 space-y-4">
// {/* Header */}
// <div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3">
// <div>
// <p className="text-sm text-gray-400">Order ID</p>
// <p className="font-semibold text-gray-800">{order.orderNumber}</p>
// <p className="text-xs text-gray-400">
// {new Date(order.createdAt).toLocaleString()}
// </p>
// </div>
// <div className="flex items-center gap-4 flex-wrap">
// <span
// className={`px-4 py-1 rounded-full text-sm font-medium ${
// statusColor[order.status] || "bg-gray-100 text-gray-800"
// }`}
// >
// {order.status.replace("_", " ")}
// </span>
// <span className="font-bold text-lg text-gray-900">
// ₹{Number(order.totalAmount).toFixed(2)}
// </span>
// </div>
// </div>
// {/* Items */}
// <div className="divide-y divide-gray-200">
// {order.items.map((item) => (
// <div
// key={item.id}
// className="flex items-center justify-between py-3"
// >
// <div>
// <p className="font-medium text-gray-800">{item.productName}</p>
// <p className="text-sm text-gray-500">Qty: {item.quantity}</p>
// </div>
// <p className="font-semibold text-gray-900">
// ₹{Number(item.price).toFixed(2)}
// </p>
// </div>
// ))}
// </div>
// {/* Footer */}
// <div className="grid md:grid-cols-2 gap-4 mt-4 text-sm text-gray-700">
// <div>
// <p className="font-medium text-gray-800">Shipping Address</p>
// <p className="mt-1">
// {order.address.firstName} {order.address.lastName},{" "}
// {order.address.addressLine1}, {order.address.city},{" "}
// {order.address.state} - {order.address.postalCode}
// </p>
// </div>
// <div className="md:text-right">
// <p>
// <span className="font-medium">Payment:</span>{" "}
// {order.paymentMethod}
// </p>
// <p>
// <span className="font-medium">Payment Status:</span>{" "}
// {order.paymentStatus}
// </p>
// </div>
// </div>
// </div>
// );
// };
// // MAIN COMPONENT
// const MyOrders = () => {
// const { data, isLoading, isError } = useGetOrdersQuery({
// page: 1,
// limit: 50,
// });
// const [filters, setFilters] = useState({
// status: [],
// time: "30_DAYS",
// });
// const orders = data?.orders || [];
// const filteredOrders = orders.filter((order) => {
// if (filters.status.length && !filters.status.includes(order.status)) return false;
// const orderDate = new Date(order.createdAt);
// const now = new Date();
// if (filters.time === "30_DAYS" && (now - orderDate) / (1000 * 60 * 60 * 24) > 30)
// return false;
// if (filters.time === "2024" && orderDate.getFullYear() !== 2024) return false;
// if (filters.time === "2023" && orderDate.getFullYear() !== 2023) return false;
// return true;
// });
// if (isLoading)
// return (
// <div className="min-h-[60vh] flex items-center justify-center text-lg">
// Loading orders...
// </div>
// );
// if (isError || !orders.length)
// return (
// <div className="min-h-[60vh] flex items-center justify-center text-gray-500">
// No orders found
// </div>
// );
// return (
// <div className="max-w-7xl mx-auto px-4 py-12 mt-24">
// <h1 className="text-4xl font-bold mb-10 text-gray-800">My Orders</h1>
// <div className="grid grid-cols-1 md:grid-cols-4 gap-8">
// {/* Filters */}
// <OrderFilters filters={filters} setFilters={setFilters} />
// {/* Orders */}
// <div className="md:col-span-3 flex flex-col gap-6 max-h-[75vh] overflow-y-auto">
// {filteredOrders.length === 0 ? (
// <div className="text-gray-500 text-center py-20">
// No orders match your filters
// </div>
// ) : (
// filteredOrders.map((order) => <OrderCard key={order.id} order={order} />)
// )}
// </div>
// </div>
// </div>
// );
// };
// export default MyOrders;
import React, { useState } from "react";
import { useGetOrdersQuery } from "../../features/orders/ordersApi";
import { useNavigate } from "react-router-dom";
import { statusColor } from "../../constants/statusColor";
// const statusColor = {
// PENDING: "bg-yellow-100 text-yellow-800",
// PAID: "bg-blue-100 text-blue-800",
// SHIPPED: "bg-purple-100 text-purple-800",
// DELIVERED: "bg-green-100 text-green-800",
// CANCELLED: "bg-red-100 text-red-800",
// RETURNED: "bg-gray-100 text-gray-800",
// };
// LEFT FILTERS
const OrderFilters = ({ filters, setFilters }) => {
const toggleStatus = (status) => {
if (filters.status.includes(status)) {
setFilters({
...filters,
status: filters.status.filter((s) => s !== status),
});
} else {
setFilters({ ...filters, status: [...filters.status, status] });
}
};
return (
<div className="sticky top-24 p-6 border rounded-3xl bg-white shadow-lg space-y-6">
<h2 className="text-xl font-semibold text-gray-800">Filter Orders</h2>
<div>
<p className="font-medium text-gray-700 mb-2">Status</p>
<div className="flex flex-col gap-2">
{["PENDING", "ON_THE_WAY", "DELIVERED", "CANCELLED", "RETURNED"].map(
(status) => (
<label
key={status}
className="flex items-center gap-3 cursor-pointer hover:text-blue-600"
>
<input
type="checkbox"
checked={filters.status.includes(status)}
onChange={() => toggleStatus(status)}
className="h-4 w-4 accent-blue-500"
/>
<span className="capitalize">{status.replace("_", " ")}</span>
</label>
)
)}
</div>
</div>
<div>
<p className="font-medium text-gray-700 mb-2">Order Time</p>
<select
value={filters.time}
onChange={(e) => setFilters({ ...filters, time: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:outline-none"
>
<option value="30_DAYS">Last 30 Days</option>
<option value="2024">2024</option>
<option value="2023">2023</option>
</select>
</div>
</div>
);
};
// ORDER CARD
const OrderCard = ({ order }) => {
const navigate = useNavigate();
return (
<div
className="bg-white border border-gray-200 rounded-3xl shadow hover:shadow-xl transition p-6 space-y-4 cursor-pointer"
onClick={() => navigate(`/my-orders/${order.id}`)}
>
{/* Header */}
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-3">
<div>
<p className="text-sm text-gray-400">Order ID</p>
<p className="font-semibold text-gray-800">{order.orderNumber}</p>
<p className="text-xs text-gray-400">
{new Date(order.createdAt).toLocaleString()}
</p>
</div>
<div className="flex items-center gap-4 flex-wrap">
<span
className={`px-4 py-1 rounded-full text-sm font-medium ${
statusColor[order.status] || "bg-gray-100 text-gray-800"
}`}
>
{order.status.replace("_", " ")}
</span>
<span className="font-bold text-lg text-gray-900">
{Number(order.totalAmount).toFixed(2)}
</span>
</div>
</div>
{/* Items */}
<div className="divide-y divide-gray-200">
{order.items.map((item) => (
<div key={item.id} className="flex items-center justify-between py-3">
<div>
<p className="font-medium text-gray-800">{item.productName}</p>
<p className="text-sm text-gray-500">Qty: {item.quantity}</p>
</div>
<p className="font-semibold text-gray-900">
{Number(item.price).toFixed(2)}
</p>
</div>
))}
</div>
{/* Footer */}
<div className="grid md:grid-cols-2 gap-4 mt-4 text-sm text-gray-700">
<div>
<p className="font-medium text-gray-800">Shipping Address</p>
<p className="mt-1">
{order.address.firstName} {order.address.lastName},{" "}
{order.address.addressLine1}, {order.address.city},{" "}
{order.address.state} - {order.address.postalCode}
</p>
</div>
<div className="md:text-right">
<p>
<span className="font-medium">Payment:</span> {order.paymentMethod}
</p>
<p>
<span className="font-medium">Payment Status:</span>{" "}
{order.paymentStatus}
</p>
</div>
</div>
</div>
);
};
// MAIN COMPONENT
const MyOrders = () => {
const { data, isLoading, isError } = useGetOrdersQuery({
page: 1,
limit: 50,
});
const [filters, setFilters] = useState({
status: [],
time: "30_DAYS",
});
const [searchQuery, setSearchQuery] = useState("");
const orders = data?.orders || [];
const filteredOrders = orders
.filter((order) => {
if (filters.status.length && !filters.status.includes(order.status))
return false;
const orderDate = new Date(order.createdAt);
const now = new Date();
if (
filters.time === "30_DAYS" &&
(now - orderDate) / (1000 * 60 * 60 * 24) > 30
)
return false;
if (filters.time === "2024" && orderDate.getFullYear() !== 2024)
return false;
if (filters.time === "2023" && orderDate.getFullYear() !== 2023)
return false;
return true;
})
.filter((order) => {
// SEARCH FILTER
if (!searchQuery) return true;
const q = searchQuery.toLowerCase();
const inOrderId = order.orderNumber.toLowerCase().includes(q);
const inCustomer = `${order.address.firstName} ${order.address.lastName}`
.toLowerCase()
.includes(q);
const inProduct = order.items.some((item) =>
item.productName.toLowerCase().includes(q)
);
return inOrderId || inCustomer || inProduct;
});
if (isLoading)
return (
<div className="min-h-[60vh] flex items-center justify-center text-lg">
Loading orders...
</div>
);
if (isError || !orders.length)
return (
<div className="min-h-[60vh] flex items-center justify-center text-gray-500">
No orders found
</div>
);
return (
<div className="max-w-7xl mx-auto px-4 py-12 mt-24">
<h1 className="text-2xl font-bold mb-6 text-primary-dark">My Orders</h1>
{/* SEARCH BAR */}
{/* SEARCH BAR */}
<div className="mb-8 flex justify-center">
<div className="w-full md:w-1/2 flex">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search by Order ID, Product, or Customer"
className="flex-1 border border-gray-300 rounded-l-xl px-4 py-3 shadow-sm focus:outline-none focus:ring-1 focus:ring-orange-700"
/>
<button
onClick={() => {}}
className="bg-primary-default text-white px-5 py-3 rounded-r-xl font-medium hover:bg-primary-dark transition"
>
Search
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Filters */}
<OrderFilters filters={filters} setFilters={setFilters} />
{/* Orders */}
<div className="md:col-span-3 flex flex-col gap-6 max-h-[75vh] overflow-y-auto">
{filteredOrders.length === 0 ? (
<div className="text-gray-500 text-center py-20">
No orders match your filters
</div>
) : (
filteredOrders.map((order) => (
<OrderCard key={order.id} order={order} />
))
)}
</div>
</div>
</div>
);
};
export default MyOrders;

View File

@@ -0,0 +1,13 @@
const OrderDetails = ({ order }) => {
return (
<div className="bg-gray-50 rounded-xl p-4 text-left space-y-2 text-sm">
<p><span className="font-semibold">Order ID:</span> {order.orderId}</p>
<p><span className="font-semibold">Total Amount:</span> {order.total}</p>
<p><span className="font-semibold">Payment:</span> {order.paymentMethod}</p>
<p><span className="font-semibold">Address:</span> {order.address}</p>
<p><span className="font-semibold">ETA:</span> {order.eta}</p>
</div>
);
};
export default OrderDetails;

View File

@@ -0,0 +1,395 @@
import React, { useState } from "react";
import { skipToken } from "@reduxjs/toolkit/query";
import { useParams, Link } from "react-router-dom";
import {
useGetOrderByIdQuery,
useGetOrderTrackingQuery,
} from "../../features/orders/ordersApi";
import {
FaBoxOpen,
FaShippingFast,
FaTruck,
FaCheckCircle,
FaMapMarkerAlt,
} from "react-icons/fa";
import { statusColor } from "../../constants/statusColor";
import { FaInfoCircle, FaUser } from "react-icons/fa";
import DeliveryAndPriceCard from "./DeliveryAndPriceCard";
import CancelOrderButton from "./CancelOrderButton";
const getStepIcon = (title) => {
const lower = title.toLowerCase();
if (lower.includes("placed") || lower.includes("confirmed")) return "📦";
if (lower.includes("shipped")) return "🚚";
if (lower.includes("out")) return "🚛";
if (lower.includes("delivered")) return "✅";
return "📦";
};
const OrderDetailsPage = () => {
const { id } = useParams();
const { data, isLoading, isError } = useGetOrderByIdQuery(id);
const [showAddressModal, setShowAddressModal] = useState(false);
const [showUserModal, setShowUserModal] = useState(false);
const [showAllUpdates, setShowAllUpdates] = useState(false);
const order = data?.data?.order;
const { data: trackingData } = useGetOrderTrackingQuery(
order
? {
orderId: order.id,
pincode: order.address?.postalCode,
shippingMethod: "STANDARD",
}
: skipToken,
);
const fullTimeline = trackingData?.data?.tracking?.timeline || [];
const visibleTimeline = showAllUpdates
? fullTimeline
: fullTimeline.slice(0, 3);
// ⛔ Returns must come AFTER all hooks
if (isLoading)
return <p className="text-center py-20 text-lg">Loading order...</p>;
if (isError)
return (
<p className="text-center py-20 text-red-500 text-lg">
Failed to load order.
</p>
);
if (!order) return null;
const orderSteps = ["PENDING", "SHIPPED", "DELIVERED"];
const currentStepIndex = orderSteps.indexOf(order.status);
return (
<div className="max-w-7xl mx-auto px-4 mt-20 py-12">
<Link
to="/my-orders"
className="text-blue-600 hover:underline mb-6 inline-block font-medium"
>
Back to Orders
</Link>
<h1 className="text-xl font-bold text-gray-500 mb-2">
{order.orderNumber}
</h1>
<p className="text-gray-500 mb-8">
{new Date(order.createdAt).toLocaleString("en-IN", {
day: "2-digit",
month: "2-digit",
year: "numeric",
})}
<span
className={`px-2 py-1 rounded-full text-sm font-medium ${
statusColor[order.status]
}`}
>
{order.status.replace("_", " ")}
</span>
</p>
<div className="grid md:grid-cols-3 gap-8">
{/* LEFT: Order Items + Track + Actions */}
{/* LEFT: Order Items + Track + Actions */}
<div className="md:col-span-2 space-y-6 ">
{/* Payment / Total Section */}
{/* <div className="bg-white rounded-3xl shadow-lg p-6 flex justify-between items-center hover:shadow-xl transition">
<p className="text-gray-700">
Pay online for a smooth doorstep experience
</p>
<button className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-xl font-medium">
Pay ₹{Number(order.totalAmount).toFixed(2)}
</button>
</div> */}
{/* Order Items */}
<div className="bg-white rounded-3xl shadow-lg p-6 hover:shadow-xl transition border">
{order.items.map((item) => (
<div
key={item.id}
className="flex justify-between py-3 items-center"
>
<div>
<p className="font-medium text-gray-800">
{item.productName}
</p>
<p className="text-gray-500 text-sm">
{item.quantity}, {item.color || "Black"}
</p>
<p className="text-gray-500 text-sm">
Seller: {item.seller || "N/A"}
</p>
</div>
<img
src={
item.productImage ||
item.productGallery?.[0] ||
"/placeholder.png"
}
alt={item.productName}
className="w-16 h-16 object-contain"
/>
</div>
))}
<p className="mt-2 text-2xl font-semibold text-gray-900">
{Number(order.totalAmount).toFixed(2)}{" "}
<span className="text-green-600 ml-2 text-xs">
{order.offer || "1 offer"}
</span>
</p>
<div className="mt-8 border-t-2">
{fullTimeline.length > 0 && (
<div className="mt-10 relative bg-gradient-to-br from-white to-gray-50 p-6 rounded-3xl border shadow-sm">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<h2 className="text-lg font-bold text-gray-800 flex items-center gap-2">
📦 Track Your Order
</h2>
{fullTimeline.length > 3 && (
<button
onClick={() => setShowAllUpdates(!showAllUpdates)}
className="text-sm font-medium text-blue-600 hover:text-blue-800 transition"
>
{showAllUpdates ? "Show less ↑" : "See more updates →"}
</button>
)}
</div>
{/* Timeline Wrapper */}
<div className="relative pl-1">
{/* ✅ PERFECT CENTERED LINE */}
<ul className="space-y-10">
{visibleTimeline.map((step, index) => {
const isCompleted = step.completed;
const isCurrent =
!step.completed && fullTimeline[index - 1]?.completed;
return (
<li
key={index}
className="relative flex items-start gap-6"
>
{/* ✅ Connector Line (NOT for last step) */}
{index !== visibleTimeline.length - 1 && (
<div className="absolute left-5 top-10 h-full w-[3px] bg-gray-400"></div>
)}
{/* Icon Circle */}
<div
className={`h-10 w-10 flex items-center justify-center rounded-full border-2 z-10 transition-all duration-300 text-lg
${
step.completed
? "bg-green-300 border-green-600 text-white"
: "bg-white border-gray-300 text-gray-400"
}`}
>
{getStepIcon(step.title)}
</div>
{/* Content */}
<div className="flex-1">
<div className="flex justify-between items-center">
<p
className={`font-semibold text-base
${
isCompleted
? "text-gray-900"
: isCurrent
? "text-blue-600"
: "text-gray-400"
}`}
>
{step.title}
</p>
{step.timestamp && (
<span className="text-xs text-gray-400">
{new Date(step.timestamp).toLocaleString(
"en-IN",
{
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
},
)}
</span>
)}
</div>
<p
className={`text-sm mt-1 ${
isCompleted
? "text-gray-600"
: "text-gray-400"
}`}
>
{step.description}
</p>
{/* Current Badge */}
{isCurrent && (
<span className="inline-block mt-3 px-3 py-1 text-xs font-medium bg-blue-50 text-blue-600 rounded-full border border-blue-200">
In Progress
</span>
)}
{/* Delivered UI */}
{step.title.toLowerCase().includes("delivered") &&
step.completed && (
<div className="mt-4 bg-green-50 border border-green-200 p-4 rounded-xl text-green-700 font-medium">
🎉 Your package has been delivered
successfully!
</div>
)}
</div>
</li>
);
})}
</ul>
</div>
</div>
)}
</div>
{/* <CancelOrderButton/> */}
{/* <CancelOrderButton orderId={order.id} status={order.status} /> */}
<div className="mt-6 grid grid-cols-2 gap-4">
{/* Cancel Order */}
<CancelOrderButton orderId={order.id} status={order.status} />
{/* Chat with Us */}
<button
onClick={() => alert("Chat support coming soon")}
className="w-full px-4 py-3 rounded-xl font-semibold
bg-[#f0ece2] text-primary-dark border border-primary-dark
hover:bg-primary-light hover:text-white hover:border-primary-default
transition flex items-center justify-center gap-2"
>
💬 Chat with Us
</button>
</div>
</div>
</div>
{/* RIGHT: Delivery Address + User Details */}
{/* <DeliveryAndPriceCard /> */}
<DeliveryAndPriceCard
order={order}
setShowAddressModal={setShowAddressModal}
setShowUserModal={setShowUserModal}
/>
</div>
{/* ADDRESS MODAL */}
{showAddressModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-white rounded-3xl shadow-2xl max-w-2xl w-full mx-4 animate-fadeIn">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b">
<h2 className="text-xl font-semibold text-gray-800 flex items-center gap-2">
Delivery Address
</h2>
<button
onClick={() => setShowAddressModal(false)}
className="text-gray-400 hover:text-gray-600 text-xl"
>
</button>
</div>
{/* Content */}
<div className="p-6 space-y-5">
{/* Address Card */}
<div className="bg-gray-50 rounded-2xl p-5 space-y-2 border">
<p className="text-gray-700">
{order.address.addressLine1}
{order.address.addressLine2 &&
`, ${order.address.addressLine2}`}
</p>
<p className="text-gray-700">
{order.address.city}, {order.address.state} {" "}
<span className="font-medium">
{order.address.postalCode}
</span>
</p>
<p className="text-gray-700">{order.address.country}</p>
</div>
</div>
</div>
</div>
)}
{/* USER MODAL */}
{showUserModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-white rounded-3xl shadow-2xl max-w-2xl w-full mx-4 animate-fadeIn">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b">
<h2 className="text-xl font-semibold text-gray-800">
User Details
</h2>
<button
onClick={() => setShowUserModal(false)}
className="text-gray-400 hover:text-gray-600 text-xl"
>
</button>
</div>
{/* Content */}
<div className="p-6">
<div className="bg-gradient-to-br from-gray-50 to-white rounded-3xl p-6 border shadow-sm space-y-5">
{/* Name */}
<div className="flex items-start gap-4">
<div className="h-12 w-12 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-lg font-semibold">
{order.address.firstName?.[0]}
{order.address.lastName?.[0]}
</div>
<div>
<p className="text-sm text-gray-500">Full Name</p>
<p className="text-gray-800 font-semibold text-lg">
{order.address.firstName} {order.address.lastName}
</p>
</div>
</div>
<div className="border-t" />
{/* Phone */}
<div className="flex items-center justify-between">
<p className="text-gray-500">Phone</p>
<p className="text-gray-800 font-medium">
{order.address.phone}
</p>
</div>
{/* Email */}
<div className="flex items-center justify-between">
<p className="text-gray-500">Email</p>
<p className="text-gray-800 font-medium truncate max-w-[250px]">
{order.address.email || "N/A"}
</p>
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default OrderDetailsPage;

View File

@@ -0,0 +1,72 @@
const OrderFilters = ({ filters, setFilters }) => {
return (
<div className="bg-white border rounded-2xl p-6 space-y-6">
{/* Order Status */}
<div>
<h3 className="font-semibold mb-3">Order Status</h3>
{["ON_THE_WAY", "DELIVERED", "CANCELLED", "RETURNED"].map((status) => (
<label key={status} className="flex items-center gap-2 mb-2">
<input
type="checkbox"
checked={filters.status.includes(status)}
onChange={() =>
setFilters((prev) => ({
...prev,
status: prev.status.includes(status)
? prev.status.filter((s) => s !== status)
: [...prev.status, status],
}))
}
/>
<span className="text-sm capitalize">
{status.replaceAll("_", " ").toLowerCase()}
</span>
</label>
))}
</div>
{/* Order Time */}
<div>
<h3 className="font-semibold mb-3">Order Time</h3>
<label className="flex items-center gap-2 mb-2">
<input
type="radio"
name="time"
checked={filters.time === "30_DAYS"}
onChange={() =>
setFilters((prev) => ({ ...prev, time: "30_DAYS" }))
}
/>
Last 30 days
</label>
<label className="flex items-center gap-2 mb-2">
<input
type="radio"
name="time"
checked={filters.time === "2024"}
onChange={() =>
setFilters((prev) => ({ ...prev, time: "2024" }))
}
/>
2024
</label>
<label className="flex items-center gap-2">
<input
type="radio"
name="time"
checked={filters.time === "2023"}
onChange={() =>
setFilters((prev) => ({ ...prev, time: "2023" }))
}
/>
2023
</label>
</div>
</div>
);
};
export default OrderFilters;

View File

@@ -0,0 +1,18 @@
import { CheckCircle } from "lucide-react";
const OrderSuccessCard = ({ children }) => {
return (
<div className="bg-white max-w-lg w-full rounded-2xl shadow-xl p-8 text-center">
<CheckCircle className="mx-auto text-green-500 w-20 h-20 mb-4" />
<h1 className="text-2xl font-bold mb-2">
Order Placed Successfully 🎉
</h1>
<p className="text-gray-600 mb-6">
Thank you for your purchase. Your order has been confirmed.
</p>
{children}
</div>
);
};
export default OrderSuccessCard;

View File

@@ -0,0 +1,251 @@
// import { CheckCircle, Truck, Package } from "lucide-react";
// const steps = [
// {
// id: 1,
// title: "Order Confirmed",
// desc: "We have received your order",
// icon: CheckCircle,
// },
// {
// id: 2,
// title: "Shipped",
// desc: "Your order is on the way",
// icon: Truck,
// },
// {
// id: 3,
// title: "Out for Delivery",
// desc: "Courier is near your location",
// icon: Package,
// },
// {
// id: 4,
// title: "Delivered",
// desc: "Package delivered successfully",
// icon: CheckCircle,
// },
// ];
// const OrderTrackingTimeline = ({ timeline = [] }) => {
// if (!timeline.length) {
// return (
// <div className="bg-white p-8 rounded-2xl shadow text-center">
// No tracking updates available.
// </div>
// );
// }
// return (
// <div className="relative space-y-6">
// {timeline.map((step, index) => {
// const isActive = step.completed;
// return (
// <div
// key={step.status}
// className={`relative flex items-start gap-6 p-6 rounded-2xl transition-all
// ${
// isActive
// ? "bg-white shadow-xl border border-primary-default/20"
// : "bg-gray-100 border border-gray-200"
// }
// `}
// >
// {/* Vertical Line */}
// {index !== timeline.length - 1 && (
// <div
// className={`absolute left-9 top-20 h-full w-[3px] rounded-full
// ${isActive ? "bg-primary-default" : "bg-gray-300"}
// `}
// />
// )}
// {/* Icon */}
// <div
// className={`flex items-center justify-center w-14 h-14 rounded-full text-2xl
// ${
// isActive
// ? "bg-primary-default text-white"
// : "bg-gray-300 text-gray-600"
// }
// `}
// >
// {step.icon || "📦"}
// </div>
// {/* Content */}
// <div className="flex-1">
// <h3 className="text-xl font-bold">{step.title}</h3>
// <p className="mt-1 text-sm text-gray-500">
// {step.description}
// </p>
// {step.timestamp && (
// <p className="mt-2 text-xs text-gray-400">
// {new Date(step.timestamp).toLocaleString()}
// </p>
// )}
// </div>
// </div>
// );
// })}
// </div>
// );
// };
// export default OrderTrackingTimeline;
// OrderTrackingTimeline.jsx - YOUR BRAND COLORS + STUNNING DESIGN 🎨
import React, { useState, useEffect } from "react";
import {
Check,
Package,
Clock,
MapPin,
Sparkles,
TrendingUp,
} from "lucide-react";
const OrderTrackingTimeline = ({ timeline = [] }) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!timeline.length) {
return (
<div className="relative overflow-hidden bg-gradient-to-br from-neutral-900 via-primary-dark to-neutral-900 p-8 rounded-2xl border border-primary-default/30">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,rgba(176,127,93,0.1),transparent)]" />
<div className="relative text-center">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-2xl bg-gradient-to-br from-primary-default/20 to-secondary/20 border border-primary-light/30 mb-4 backdrop-blur">
<Package size={36} className="text-primary-light" />
</div>
<h3 className="text-xl font-bold text-white mb-2">
Awaiting Updates
</h3>
<p className="text-primary-light/80 text-sm">
Your tracking information will appear here
</p>
</div>
</div>
);
}
const completedCount = timeline.filter((s) => s.completed).length;
const progress = (completedCount / timeline.length) * 100;
return (
<div className="space-y-4">
{/* Compact Progress Header */}
<div className="relative p-4 rounded-2xl bg-gradient-to-r from-primary-dark via-primary-default to-secondary shadow-navbar">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-lg bg-white/10 flex items-center justify-center">
<TrendingUp size={20} className="text-white" />
</div>
<div>
<h2 className="text-lg font-bold text-white">
Delivery Progress
</h2>
<p className="text-white/80 text-xs">Real-time tracking</p>
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-white">
{Math.round(progress)}%
</div>
</div>
</div>
{/* Slim Progress Bar */}
<div className="h-2 bg-white/20 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-accent via-secondary-light to-secondary rounded-full transition-all duration-700"
style={{ width: `${progress}%` }}
/>
</div>
</div>
{/* Timeline */}
<div className="space-y-3">
{timeline.map((step, index) => {
const isCompleted = step.completed;
const isLast = index === timeline.length - 1;
return (
<div key={step.status} className="relative">
{/* Connector Line */}
{!isLast && (
<div className="absolute left-6 top-14 w-[3px] h-full">
<div
className={`w-full h-full rounded-full ${
isCompleted
? "bg-gradient-to-b from-accent to-primary-default"
: "bg-neutral-300"
}`}
/>
</div>
)}
{/* Card */}
<div
className={`relative rounded-xl p-4 flex items-start gap-4 transition-all ${
isCompleted
? "bg-accent/5 border border-accent/30 shadow-card"
: "bg-white border border-neutral-200"
}`}
>
{/* Icon */}
<div
className={`w-12 h-12 rounded-xl flex items-center justify-center ${
isCompleted
? "bg-gradient-to-br from-accent to-primary-default text-white"
: "bg-neutral-200 text-neutral-500"
}`}
>
{isCompleted ? (
<Check size={20} strokeWidth={3} />
) : (
<span className="text-xl opacity-60">
{step.icon || "📦"}
</span>
)}
</div>
{/* Content */}
<div className="flex-1">
<h3
className={`text-base font-semibold ${
isCompleted ? "text-primary-dark" : "text-neutral-400"
}`}
>
{step.title}
</h3>
<p
className={`text-xs mt-1 ${
isCompleted ? "text-neutral-600" : "text-neutral-400"
}`}
>
{step.description}
</p>
{step.timestamp && (
<p className="text-[11px] mt-2 text-neutral-500">
{new Date(step.timestamp).toLocaleString("en-IN")}
</p>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
);
};
export default OrderTrackingTimeline;

View File

@@ -0,0 +1,28 @@
import { Truck, Home } from "lucide-react";
import { useNavigate } from "react-router-dom";
const SuccessActions = ({ orderId }) => {
const navigate = useNavigate();
return (
<div className="flex gap-4 mt-6">
<button
onClick={() =>
navigate("/track-order", { state: { orderId } })
}
className="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-primary-default text-white font-semibold hover:bg-primary-dark transition"
>
<Truck /> Track Order
</button>
<button
onClick={() => navigate("/")}
className="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl border font-semibold hover:bg-gray-50 transition"
>
<Home /> Home
</button>
</div>
);
};
export default SuccessActions;

View File

@@ -0,0 +1,67 @@
// const TrackingStep = ({ icon: Icon, label, active }) => {
// return (
// <div
// className={`flex items-center gap-4 p-4 rounded-xl border ${
// active
// ? "border-primary-default bg-primary/5"
// : "border-gray-200"
// }`}
// >
// <Icon
// className={`w-6 h-6 ${
// active ? "text-primary-default" : "text-gray-400"
// }`}
// />
// <span
// className={`font-medium ${
// active ? "text-gray-900" : "text-gray-400"
// }`}
// >
// {label}
// </span>
// </div>
// );
// };
// export default TrackingStep;
const TrackingStep = ({ icon: Icon, label, active, current }) => {
return (
<div className="flex items-start gap-6 group">
{/* Icon Circle */}
<div
className={`relative z-10 flex items-center justify-center w-12 h-12 rounded-full transition-all duration-300
${
active
? "bg-primary-default text-white shadow-lg"
: "bg-gray-200 text-gray-400"
}
${current ? "ring-4 ring-primary-default/30 scale-110" : ""}
`}
>
<Icon className="w-6 h-6" />
</div>
{/* Content */}
<div className="flex flex-col">
<span
className={`text-lg font-semibold transition-colors
${active ? "text-gray-800" : "text-gray-400"}
`}
>
{label}
</span>
{current && (
<span className="text-sm text-primary-default mt-1 animate-pulse">
Current Status
</span>
)}
</div>
</div>
);
};
export default TrackingStep;

View File

@@ -0,0 +1,109 @@
// import { useState } from "react";
// const PinCodeCheck = () => {
// const [pin, setPin] = useState("");
// const [isChecked, setIsChecked] = useState(false);
// const handleCheck = () => {
// // Simulate pin code check
// setIsChecked(true);
// };
// return (
// <div className="mt-6">
// <h2 className="text-lg font-semibold mb-2">Check Delivery & Exchange</h2>
// <div className="flex gap-3">
// <input
// type="text"
// placeholder="Enter PIN code"
// value={pin}
// onChange={(e) => setPin(e.target.value)}
// className="border px-4 py-2 rounded-lg flex-1"
// />
// <button
// onClick={handleCheck}
// className="bg-primary-default text-white px-4 py-2 rounded-lg hover:bg-primary-dark transition"
// >
// Check
// </button>
// </div>
// {isChecked && (
// <div className="mt-4 flex justify-between items-center bg-gray-50 p-4 rounded-lg shadow">
// <div className="flex items-center gap-3">
// <img src="/logo.png" alt="Logo" className="w-10 h-10" />
// <span className="font-medium">Free delivery within 2-3 days</span>
// </div>
// <div className="font-medium text-gray-700">Easy Exchange in 10 days</div>
// </div>
// )}
// </div>
// );
// };
// export default PinCodeCheck;
import { useState } from "react";
import { MapPin, Truck, RotateCcw } from "lucide-react";
const PinCodeCheck = () => {
const [pin, setPin] = useState("");
const [isChecked, setIsChecked] = useState(false);
const handleCheck = () => {
setIsChecked(true);
};
return (
<div className="mt-6 p-6 rounded-xl border bg-white shadow-sm hover:shadow-md transition">
{/* Title */}
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<MapPin className="text-primary-default" size={22} />
Check Delivery & Exchange
</h2>
{/* Input + Button */}
<div className="flex gap-3">
<input
type="text"
placeholder="Enter PIN code"
value={pin}
onChange={(e) => setPin(e.target.value)}
className="border border-gray-300 px-4 py-3 rounded-lg flex-1 focus:ring-2 focus:ring-primary-default focus:border-primary-default outline-none transition"
/>
<button
onClick={handleCheck}
className="bg-primary-default text-white px-6 py-3 rounded-lg hover:bg-primary-dark transition font-semibold"
>
Check
</button>
</div>
{/* Result Box */}
{isChecked && (
<div className="mt-6 p-5 rounded-lg bg-gray-50 shadow-inner border animate-fadeIn">
{/* Delivery */}
<div className="flex items-center gap-3 mb-3">
<Truck className="text-green-600" size={22} />
<span className="text-gray-800 font-medium">
Free delivery within <span className="font-bold">23 days</span>
</span>
</div>
{/* Exchange */}
<div className="flex items-center gap-3">
<RotateCcw className="text-blue-600" size={22} />
<span className="text-gray-800 font-medium">
Easy Exchange within <span className="font-bold">10 days</span>
</span>
</div>
</div>
)}
</div>
);
};
export default PinCodeCheck;

View File

@@ -0,0 +1,292 @@
import React, { useState } from "react";
import { useParams, Link } from "react-router-dom";
import Pagination from "../ui/Pagination";
import Breadcrumb from "../ui/Breadcrumb";
import CategoryGrid from "../home/Category/CategoryGrid";
import SareeFilters from "../sarees/SareeFilters";
import { useGetProductsByCategoryQuery } from "../../features/products/productsAPI";
import {
useAddToWishlistMutation,
useRemoveFromWishlistMutation,
useGetWishlistQuery,
} from "../../features/wishlist/wishlistAPI";
import { Heart, Eye } from "lucide-react";
const ProductCategoryPage = () => {
const { parent, category, subCategory } = useParams();
const categorySlug = subCategory || category;
const { data: wishlistData } = useGetWishlistQuery();
const [addToWishlist] = useAddToWishlistMutation();
const [removeFromWishlist] = useRemoveFromWishlistMutation();
const wishlistIds =
wishlistData?.data?.wishlist?.map(
(item) => item.product?._id || item.productId,
) ||
wishlistData?.data?.products?.map((item) => item._id) ||
[];
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 12;
const { data, isLoading, isError } = useGetProductsByCategoryQuery({
categorySlug,
limit: itemsPerPage,
skip: (currentPage - 1) * itemsPerPage,
});
const handleWishlist = async (e, productId) => {
e.preventDefault();
e.stopPropagation();
try {
if (wishlistIds.includes(productId)) {
await removeFromWishlist(productId).unwrap();
} else {
await addToWishlist(productId).unwrap();
}
} catch (error) {
console.error("Wishlist error:", error);
}
};
const calculateDiscount = (basePrice, comparePrice) => {
if (!comparePrice || comparePrice <= basePrice) return 0;
return Math.round(((comparePrice - basePrice) / comparePrice) * 100);
};
// Calculate number of offers (mock data - you can replace with real logic)
const getOffersCount = () => {
return Math.floor(Math.random() * 3) + 1; // Random 1-3 offers
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="text-center">
<div className="w-12 h-12 border-3 border-gray-200 border-t-gray-900 rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-gray-600 text-sm">Loading products...</p>
</div>
</div>
);
}
if (isError) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="text-center max-w-md p-8">
<p className="text-lg text-gray-900 font-semibold mb-2">
Unable to load products
</p>
<p className="text-gray-600 text-sm">Please try again later</p>
</div>
</div>
);
}
const products = data?.data?.products || [];
const totalProducts = data?.data?.totalCount || products.length;
const totalPages = Math.ceil(totalProducts / itemsPerPage);
return (
<div className="min-h-screen bg-white">
{/* Header */}
<div className="bg-gray-50">
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8 pt-32 pb-8">
<Breadcrumb />
<CategoryGrid />
<div className="flex justify-center my-8">
<hr className="w-48 h-[2px] bg-primary-dark border-0" />
</div>
<h1 className="text-3xl md:text-4xl font-extrabold text-[#f03861] text-center mb-2 mt-10">
{categorySlug
.replace(/-/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase())}{" "}
Collection
</h1>
<p className="text-gray-600 text-center mb-8">
Explore our newest and most loved products curated just for you.
</p>
{/* Filters */}
<SareeFilters />
</div>
</div>
{/* Products Grid */}
<div className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8 py-8">
{products.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 lg:gap-6 ">
{products.map((product) => {
const discount = calculateDiscount(
product.basePrice,
product.compareAtPrice,
);
const offersCount = getOffersCount();
return (
<Link
key={product._id}
to={`/product/${product.slug}`}
className="group bg-white rounded-lg overflow-hidden hover:shadow-xl transition-shadow duration-300 border border-gray-200"
>
{/* Image Container */}
<div className="relative bg-gray-50 overflow-hidden aspect-[3/4]">
<img
src={
product.images?.primary ||
product.images?.gallery?.[0] ||
"/placeholder.jpg"
}
alt={product.name}
className="w-full h-full object-cover object-top group-hover:scale-105 transition-transform duration-500"
loading="lazy"
/>
{/* Badges */}
<div className="absolute top-3 left-3 flex flex-col gap-2">
{product.isFeatured && (
<div className="bg-white text-gray-900 text-xs font-bold px-3 py-1.5 rounded">
NEW
</div>
)}
</div>
<div className="absolute bottom-3 left-3 flex flex-col gap-2">
{/* Offers Badge */}
{offersCount > 0 && (
<div className="bg-gray-800/90 backdrop-blur-sm text-white text-xs font-semibold px-3 py-1.5 rounded">
{offersCount} OFFER{offersCount > 1 ? "S" : ""} FOR
YOU
</div>
)}
</div>
{/* Action Buttons */}
<div className="absolute top-3 right-3 flex flex-col gap-2">
{/* Quick View */}
{/* Wishlist */}
<button
onClick={(e) => handleWishlist(e, product._id)}
className="w-9 h-9 bg-white rounded-full flex items-center justify-center shadow-md hover:shadow-lg transition-shadow"
title="Add to Wishlist"
>
<Heart
size={16}
className={
wishlistIds.includes(product._id)
? "text-red-500 fill-red-500"
: "text-gray-700"
}
/>
</button>
</div>
</div>
{/* Product Info */}
<div className="p-4">
{/* Brand/Category */}
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">
{product.brand || categorySlug.replace(/-/g, " ")}
</p>
{/* Product Name */}
<h3 className="text-lg font-medium text-primary-dark line-clamp-2 min-h-[2.5rem] transition-colors">
{product.name}
</h3>
<p className="text-sm font-medium text-gray-500 line-clamp-2 min-h-[2.5rem] transition-colors">
{product.description}
</p>
{/* Price */}
<div className="flex items-center gap-2 mb-1">
<span className="text-lg font-bold text-gray-900">
{product.basePrice.toLocaleString()}
</span>
{product.compareAtPrice && (
<span className="text-sm text-gray-400 line-through">
{product.compareAtPrice.toLocaleString()}
</span>
)}
{/* Discount */}
{discount > 0 && (
<p className="text-xs font-semibold text-orange-600">
{discount}% OFF
</p>
)}
</div>
{/* Color Variants */}
{product.variants?.length > 0 && (
<div className="flex items-center gap-1.5 ">
{product.variants.slice(0, 5).map((variant, idx) => {
const colorValue =
variant.color?.toLowerCase() || "#e5e7eb";
return (
<div
key={variant._id || idx}
className="w-5 h-5 rounded-full border-2 border-gray-700 hover:border-gray-500 transition-colors cursor-pointer"
style={{ backgroundColor: colorValue }}
title={variant.color}
/>
);
})}
{product.variants.length > 5 && (
<span className="text-xs text-gray-500 ml-1">
+{product.variants.length - 5}
</span>
)}
</div>
)}
</div>
</Link>
);
})}
</div>
) : (
<div className="text-center py-20">
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg
className="w-10 h-10 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
/>
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">
No products found
</h3>
<p className="text-gray-600">Try adjusting your filters</p>
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-12">
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
</div>
)}
</div>
</div>
);
};
export default ProductCategoryPage;

View File

@@ -0,0 +1,12 @@
const ProductDeclaration = () => (
<div className="mt-8">
<h2 className="text-xl font-semibold">Product Declaration</h2>
<p className="text-gray-700 mt-2">
Manufacturer: Vedant Fashions Limited
<br />
Address: Paridhan Garment Park, 19, Canal South Road, SDF-1, 4th Floor, A501-A502, Kolkata, West Bengal 700015, India
</p>
</div>
);
export default ProductDeclaration;

View File

@@ -0,0 +1,117 @@
import { useParams } from "react-router-dom";
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import Breadcrumb from "../../components/ui/Breadcrumb";
import ProductImages from "./ProductImages";
import ProductInfo from "./ProductInfo";
import ScrollToTop from "../ui/ScrollToTop";
import { RatingPage } from "../../pages/RatingPage";
import { useGetProductBySlugQuery } from "../../features/products/productsAPI";
import {
useAddToWishlistMutation,
useRemoveFromWishlistMutation,
useGetWishlistQuery,
} from "../../features/wishlist/wishlistApi";
import { addRecentProduct } from "../../features/recentlyViewed/recentlyViewedApi";
import RecommendedProducts from "./RecommendedProducts";
import RecommendedForYou from "../home/RecommendedForYou/RecommendedForYou";
const ProductDetails = () => {
const { slug } = useParams();
const dispatch = useDispatch();
const { data, isLoading, isError } = useGetProductBySlugQuery(slug);
const { data: wishlistData } = useGetWishlistQuery();
const [addToWishlist] = useAddToWishlistMutation();
const [removeFromWishlist] = useRemoveFromWishlistMutation();
// const product = data?.data?.product;
// ✅ Track product view with proper error handling
useEffect(() => {
if (data?.data?.product) {
const product = data.data.product;
// ✅ Ensure all required fields exist and are valid
const productToSave = {
id: product._id,
name: product.name || "Unnamed Product",
image:
product.images?.primary ||
(Array.isArray(product.images?.gallery) &&
product.images.gallery[0]) ||
product.image ||
"/placeholder.jpg",
price:
product.basePrice != null && !isNaN(product.basePrice)
? parseFloat(product.basePrice)
: 0,
url: `/product/${slug}`,
};
console.log("Recently viewed product:", productToSave);
// ✅ Debug log to see what's being saved
console.log("Saving product to recently viewed:", productToSave);
dispatch(addRecentProduct(productToSave));
}
}, [data, dispatch, slug]);
if (isLoading) return <p className="mt-20 text-center">Loading product...</p>;
if (isError || !data?.data?.product)
return <p className="mt-20 text-center">Product not found</p>;
const product = data.data.product;
const wishlistIds =
wishlistData?.data?.wishlist?.map(
(item) => item.product?._id || item.productId,
) || [];
const isWishlisted = wishlistIds.includes(product._id);
const toggleWishlist = async () => {
try {
if (isWishlisted) {
await removeFromWishlist(product._id).unwrap();
} else {
await addToWishlist(product._id).unwrap();
}
} catch (err) {
console.error("Wishlist error:", err);
}
};
return (
<div className="px-5 md:px-20 py-10 mt-24">
<Breadcrumb />
<div className="flex flex-col md:flex-row gap-10">
<div className="md:w-4/5">
<ProductImages
product={product}
isWishlisted={isWishlisted}
onToggleWishlist={toggleWishlist}
/>
<ScrollToTop />
</div>
<div className="md:w-1/2">
<ProductInfo
product={product}
isWishlisted={isWishlisted}
onToggleWishlist={toggleWishlist}
/>
</div>
</div>
<RecommendedProducts />
<RecommendedForYou />
<div className="mt-16">
<RatingPage />
</div>
</div>
);
};
export default ProductDetails;

View File

@@ -0,0 +1,70 @@
const ProductDetailsSection = ({ product }) => {
// Prepare weight & dimensions display
const weight = product.weight?.value
? `${product.weight.value} ${product.weight.unit}`
: null;
const dimensions =
product.dimensions?.length &&
product.dimensions?.width &&
product.dimensions?.height
? `${product.dimensions.length} x ${product.dimensions.width} x ${product.dimensions.height} ${product.dimensions.unit}`
: null;
return (
<div className="mt-6 sm:mt-10 bg-gray-50 p-4 sm:p-6 rounded-xl border border-gray-200 w-full max-w-3xl mx-auto">
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 mb-4">Product Details</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 sm:gap-x-10 gap-y-4 text-gray-800">
{/* Weight */}
{weight && (
<div>
<p className="font-medium text-gray-600 text-sm sm:text-base">Weight</p>
<p className="text-gray-900 text-sm sm:text-base">{weight}</p>
</div>
)}
{/* Dimensions */}
{dimensions && (
<div>
<p className="font-medium text-gray-600 text-sm sm:text-base">Dimensions</p>
<p className="text-gray-900 text-sm sm:text-base">{dimensions}</p>
</div>
)}
{/* Tags */}
{product.tags?.length > 0 && (
<div>
<p className="font-medium text-gray-600 text-sm sm:text-base">Tags</p>
<p className="text-gray-900 text-sm sm:text-base">{product.tags.join(", ")}</p>
</div>
)}
{/* Meta Keywords */}
{product.metaKeywords?.length > 0 && (
<div>
<p className="font-medium text-gray-600 text-sm sm:text-base">Meta Keywords</p>
<p className="text-gray-900 text-sm sm:text-base">{product.metaKeywords.join(", ")}</p>
</div>
)}
</div>
{/* Description */}
<div className="mt-6 sm:mt-8 w-full">
<p className="font-semibold text-gray-800 text-base sm:text-lg mb-1">Description</p>
<p className="text-gray-700 text-sm sm:text-base leading-relaxed break-words">
{product.description}
</p>
</div>
{/* Disclaimer */}
<div className="mt-4 sm:mt-6 w-full">
<p className="font-semibold text-gray-800 text-base sm:text-lg mb-1">Disclaimer</p>
<p className="text-gray-600 text-sm sm:text-base leading-relaxed break-words">
Product color may slightly vary due to photographic lighting sources or screen settings.
</p>
</div>
</div>
);
};
export default ProductDetailsSection;

View File

@@ -0,0 +1,120 @@
// import { useState } from "react";
// import { Heart } from "lucide-react";
// const ProductImages = ({ product }) => {
// const [mainImg, setMainImg] = useState(
// product?.images ? product.images[0] : product?.image
// );
// return (
// <div className="flex flex-col md:flex-row gap-6 md:gap-10">
// {/* Thumbnails */}
// <div className="flex md:flex-col gap-4">
// {product.images?.map((img, idx) => (
// <img
// key={idx}
// src={img}
// onMouseEnter={() => setMainImg(img)}
// className="w-20 h-20 md:w-24 md:h-24 object-cover rounded-xl border cursor-pointer hover:scale-105 transition-all"
// />
// ))}
// </div>
// {/* Main Image */}
// <div className="relative flex-1">
// <img
// src={mainImg}
// alt={product.title}
// className="w-full h-[600px] md:h-[750px] object-cover rounded-2xl shadow-xl transition-transform duration-300"
// />
// {/* Discount Badge */}
// {product.discount && (
// <span className="absolute top-5 left-5 bg-primary-default text-white px-4 py-2 rounded-lg text-sm font-semibold shadow-lg">
// {product.discount}% OFF
// </span>
// )}
// {/* Wishlist */}
// <button className="absolute top-5 right-5 bg-white p-3 rounded-full shadow-lg hover:bg-gray-100 transition">
// <Heart size={22} className="text-red-500" />
// </button>
// </div>
// </div>
// );
// };
// export default ProductImages;
import { useState } from "react";
import { Heart } from "lucide-react";
const ProductImages = ({ product, isWishlisted, onToggleWishlist }) => {
const images = Array.isArray(product?.images?.gallery)
? product.images.gallery
: [];
const [mainImg, setMainImg] = useState(images[0]);
if (!images.length) {
return (
<div className="flex items-center justify-center h-[400px] bg-gray-100 rounded-xl">
<p className="text-gray-500">No images available</p>
</div>
);
}
return (
<div className="flex flex-col md:flex-row gap-6 md:gap-10">
{/* Thumbnails */}
<div className="flex md:flex-col gap-4">
{images.map((img, idx) => (
<img
key={idx}
src={img}
onMouseEnter={() => setMainImg(img)}
className={`w-20 h-20 md:w-24 md:h-24 object-cover rounded-xl border cursor-pointer
${mainImg === img ? "border-black scale-105" : "border-gray-200"}
transition-all`}
/>
))}
</div>
{/* Main Image */}
<div className="relative flex-1">
<img
src={mainImg}
alt={product.name}
className="w-full h-[600px] md:h-[750px] object-cover rounded-2xl shadow-xl"
/>
{/* Discount Badge */}
{product.discount && (
<span className="absolute top-5 left-5 bg-black text-white px-4 py-2 rounded-lg text-sm font-semibold shadow-lg">
{product.discount}% OFF
</span>
)}
{/* Wishlist */}
{/* <button className="absolute top-5 right-5 bg-white p-3 rounded-full shadow-lg hover:bg-gray-100 transition">
<Heart size={22} className="text-red-500" />
</button> */}
{/* Wishlist */}
<button
onClick={onToggleWishlist}
className="absolute top-5 right-5 bg-white p-3 rounded-full shadow-lg hover:bg-gray-100 transition"
>
<Heart
size={22}
className={
isWishlisted ? "text-red-600 fill-red-600" : "text-gray-400"
}
/>
</button>
</div>
</div>
);
};
export default ProductImages;

View File

@@ -0,0 +1,163 @@
import { useState } from "react";
import { Heart } from "lucide-react";
import { useNavigate } from "react-router-dom";
import PinCodeCheck from "./PinCodeCheck";
import ProductDetailsSection from "./ProductDetailsSection";
import ProductDeclaration from "./ProductDeclaration";
import Accordion from "../ui/Accordion";
import ShippingReturns from "./ShippingReturns";
import { useAddToCartMutation } from "../../features/cart/cartAPI";
const ProductInfo = ({ product, isWishlisted, onToggleWishlist }) => {
const navigate = useNavigate();
const [quantity, setQuantity] = useState(1);
const [addToCart, { isLoading }] = useAddToCartMutation();
const handleQuantity = (type) => {
if (type === "inc") setQuantity((prev) => prev + 1);
else if (type === "dec" && quantity > 1) setQuantity((prev) => prev - 1);
};
const handleAddToCart = async () => {
const token = localStorage.getItem("token");
// 🚨 USER NOT LOGGED IN
if (!token) {
alert("Please login first to add product to cart");
navigate("/login");
return;
}
try {
await addToCart({
productId: product._id,
quantity,
}).unwrap();
alert("Item added to cart ✅");
} catch (error) {
console.error("Add to cart failed:", error);
alert("Failed to add item ❌");
}
};
return (
<div className="flex-1 flex flex-col gap-6">
<h1 className="text-3xl md:text-3xl font-bold text-primary-dark">
{product.name}
</h1>
<p className="text-gray-600">
{product.description || "No description available."}
</p>
{/* Price */}
<div className="flex items-center gap-4 mt-2">
<p className="text-3xl font-bold text-primary">{product.basePrice}</p>
{/* <p className="line-through text-gray-400 text-lg">
₹{product.compareAtPrice}
</p> */}
</div>
{/* Colors */}
{product.colors?.length > 0 && (
<div className="flex items-center gap-3 mt-4">
<span className="text-gray-700 font-medium">Colors:</span>
{product.colors.map((c, idx) => (
<span
key={idx}
className="w-6 h-6 rounded-full border-2 border-gray-300 cursor-pointer hover:ring-2 hover:ring-primary transition"
style={{ backgroundColor: c }}
/>
))}
</div>
)}
{/* Quantity Selector */}
<div className="flex items-center gap-4 mt-4">
<span className="text-gray-700 font-medium">Quantity:</span>
<div className="flex items-center border border-gray-300 rounded-lg overflow-hidden">
<button
onClick={() => handleQuantity("dec")}
className="px-4 py-2 hover:bg-gray-100 transition font-semibold"
>
-
</button>
<span className="px-5 py-2 bg-gray-50 font-medium">{quantity}</span>
<button
onClick={() => handleQuantity("inc")}
className="px-4 py-2 hover:bg-gray-100 transition font-semibold"
>
+
</button>
</div>
</div>
{/* Add to Cart */}
{/* <div className="flex flex-col sm:flex-row gap-4 mt-6 sticky top-40"> */}
<div className="flex flex-col sm:flex-row gap-4 mt-6">
<button
onClick={handleAddToCart}
disabled={isLoading}
className="bg-primary-default text-white px-6 py-3 rounded-lg
hover:bg-primary-dark transition font-medium shadow-lg
disabled:opacity-50"
>
{isLoading ? "Adding..." : "Add to Cart"}
</button>
{/* <button
onClick={onToggleWishlist}
className={`border px-6 py-3 rounded-lg flex items-center gap-2
${
isWishlisted
? "border-red-600 text-red-600"
: "border-gray-400 text-gray-600"
}`}
>
<Heart size={18} className={isWishlisted ? "fill-red-600" : ""} />
{isWishlisted ? "Wishlisted" : "Add to Wishlist"}
</button> */}
<button
onClick={() => {
const token = localStorage.getItem("token");
if (!token) {
alert("Please login first to use wishlist");
navigate("/login");
return;
}
onToggleWishlist();
}}
className={`border px-6 py-3 rounded-lg flex items-center gap-2
${
isWishlisted
? "border-red-600 text-red-600"
: "border-gray-400 text-gray-600"
}`}
>
<Heart size={18} className={isWishlisted ? "fill-red-600" : ""} />
{isWishlisted ? "Wishlisted" : "Add to Wishlist"}
</button>
</div>
{/* Accordion Sections */}
<div className="mt-6 flex flex-col gap-4">
<Accordion title="Product Details">
<ProductDetailsSection product={product} />
</Accordion>
{/* <Accordion title="Product Declaration">
<ProductDeclaration />
</Accordion> */}
<Accordion title="Shipping & Returns">
<ShippingReturns />
</Accordion>
</div>
</div>
);
};
export default ProductInfo;

View File

@@ -0,0 +1,84 @@
import { useParams, Link } from "react-router-dom";
import { useGetProductRecommendationsQuery } from "../../features/products/productsAPI";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "../../styles/recommendedSwiper.css";
const RecommendedProducts = () => {
const { slug } = useParams();
const { data: recommendations, isLoading } =
useGetProductRecommendationsQuery({ slug, limit: 12 });
if (isLoading)
return <p className="text-center mt-10 text-gray-500">Loading...</p>;
if (!recommendations?.length) return null;
return (
<section className="mt-36">
<h2 className="text-3xl text-center font-bold text-[#f03861] mb-10">
You may also like this!
</h2>
<Swiper
modules={[Navigation, Pagination]}
navigation
pagination={{ clickable: true }}
spaceBetween={28}
slidesPerView={1.2}
style={{ padding: "0 56px" }} // 👈 REQUIRED
breakpoints={{
640: { slidesPerView: 2 },
768: { slidesPerView: 3 },
1024: { slidesPerView: 4 },
}}
className="recommended-swiper"
>
{recommendations.map((product) => (
<SwiperSlide key={product._id}>
<Link
to={`/product/${product.slug}`}
className="block bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-300 hover:-translate-y-1"
>
{/* Image */}
<div className="relative h-80 overflow-hidden">
{product.isFeatured && (
<span className="absolute top-3 left-3 z-10 bg-[#f03861] text-white text-xs font-semibold px-3 py-1 rounded-full shadow">
Featured
</span>
)}
<img
src={
product.images?.primary ||
product.images?.gallery?.[0] ||
"/placeholder.jpg"
}
alt={product.name}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-500"
/>
</div>
{/* Content */}
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 truncate">
{product.name}
</h3>
<p className="mt-1 text-lg font-bold text-gray-900">
{product.basePrice}
</p>
</div>
</Link>
</SwiperSlide>
))}
</Swiper>
</section>
);
};
export default RecommendedProducts;

View File

@@ -0,0 +1,12 @@
const ShippingReturns = () => (
<div className="mt-8">
<h2 className="text-xl font-semibold">Shipping & Returns</h2>
<p className="text-gray-700 mt-2">
All ready-to-ship products are shipped within 48 hours of placing the order.
Standard delivery dates are provided in the order confirmation email. Easy Exchange/Return Policy is available for all garments except Accessories.
</p>
<p className="text-blue-600 mt-2">For details, visit Shipping & Returns page.</p>
</div>
);
export default ShippingReturns;

View File

@@ -0,0 +1,108 @@
import { useState } from "react";
import { useChangePasswordMutation } from "../../features/auth/authApi";
// import toast from "../ui/"; // optional: for toast notifications
const ChangePassword = ({setActiveTab }) => {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [changePassword, { isLoading }] = useChangePasswordMutation();
const handleSubmit = async (e) => {
e.preventDefault();
if (newPassword !== confirmPassword) {
// toast.error("New password and confirm password do not match");
return;
}
try {
await changePassword({
currentPassword,
newPassword,
}).unwrap();
// toast.success("Password changed successfully!");
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} catch (err) {
console.error(err);
// toast.error(err?.data?.message || "Failed to change password");
}
};
return (
<div className=" mx-auto p-6 mt-3 bg-white rounded-2xl shadow-lg">
<h2 className="text-2xl font-bold mb-6 text-left text-primary-dark">
Change Password
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-gray-700 mb-1">Current Password</label>
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
placeholder="Enter current password"
className="w-full border border-gray-300 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-900"
required
/>
</div>
<div>
<label className="block text-gray-700 mb-1">New Password</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Enter new password"
className="w-full border border-gray-300 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-900"
required
/>
</div>
<div>
<label className="block text-gray-700 mb-1">
Confirm New Password
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm new password"
className="w-full border border-gray-300 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-900"
required
/>
</div>
{/* <button
type="submit"
disabled={isLoading}
className="w-full bg-primary-default text-white p-3 rounded-xl font-semibold hover:bg-primary-dark transition"
>
{isLoading ? "Changing..." : "Change Password"}
</button> */}
<div className="flex gap-4 mt-4">
<button
type="submit"
disabled={isLoading}
className="flex-1 bg-primary-default text-white p-3 rounded-xl font-semibold hover:bg-primary-dark transition"
>
{isLoading ? "Changing..." : "Change Password"}
</button>
<button
type="button"
onClick={() => setActiveTab("profile")}
className="flex-1 border border-gray-300 text-gray-700 p-3 rounded-xl font-semibold hover:bg-gray-100 transition"
>
Cancel
</button>
</div>
</form>
</div>
);
};
export default ChangePassword;

View File

@@ -0,0 +1,134 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
useGetProfileQuery,
useUpdateProfileMutation,
} from "../../features/auth/authApi";
const EditProfilePage = ({ setActiveTab }) => {
const navigate = useNavigate();
const { data: profileData, isLoading } = useGetProfileQuery();
const [updateProfile, { isLoading: updating }] = useUpdateProfileMutation();
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
phone: "",
username: "",
avatar: null,
});
// Populate form when data is loaded
useEffect(() => {
if (profileData?.data?.user) {
const { firstName, lastName, email, phone, avatar, username } =
profileData.data.user;
setForm({ firstName, lastName, email, phone, avatar, username });
}
}, [profileData]);
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
await updateProfile(form).unwrap();
alert("Profile updated successfully!");
setActiveTab("profile");
} catch (err) {
console.error(err);
alert("Failed to update profile");
}
};
if (isLoading) return <p className="text-center py-10">Loading profile...</p>;
return (
<div className=" mx-auto p-6 mt-3 bg-white rounded-2xl shadow-lg">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Edit Profile</h2>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<input
type="text"
name="firstName"
placeholder="First Name"
value={form.firstName}
onChange={handleChange}
className="input-field"
/>
<input
type="text"
name="lastName"
placeholder="Last Name"
value={form.lastName}
onChange={handleChange}
className="input-field"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<input
type="email"
name="email"
placeholder="Email"
value={form.email}
onChange={handleChange}
className="input-field w-full"
/>
<input
type="text"
name="username"
placeholder="Username"
value={form.username}
onChange={handleChange}
className="input-field"
/>
</div>
<input
type="text"
name="phone"
placeholder="Phone"
value={form.phone}
onChange={handleChange}
className="input-field w-full"
/>
{/* Avatar Upload (optional) */}
<div>
<label className="block text-gray-700 font-medium mb-1">Avatar</label>
<input
type="file"
name="avatar"
onChange={(e) => setForm({ ...form, avatar: e.target.files[0] })}
className="w-full border rounded-lg p-2"
/>
</div>
<div className="flex gap-4 mt-4">
<button
type="submit"
disabled={updating}
className="flex-1 bg-primary-default text-white py-2 rounded-lg font-medium hover:bg-primary-dark transition"
>
{updating ? "Updating..." : "Save Changes"}
</button>
<button
type="button"
onClick={() => navigate("/profile")}
className="flex-1 border border-gray-300 text-gray-700 py-2 rounded-lg font-medium hover:bg-gray-100 transition"
>
Cancel
</button>
</div>
</form>
</div>
);
};
export default EditProfilePage;

View File

@@ -0,0 +1,29 @@
const OrdersSkeleton = () => {
return (
<div className="space-y-6 animate-pulse">
{[1, 2, 3].map((i) => (
<div
key={i}
className="bg-white border rounded-2xl p-6 space-y-4"
>
<div className="flex justify-between">
<div className="h-4 w-40 bg-gray-200 rounded" />
<div className="h-6 w-20 bg-gray-200 rounded-full" />
</div>
<div className="space-y-3">
<div className="h-4 w-3/4 bg-gray-200 rounded" />
<div className="h-4 w-1/2 bg-gray-200 rounded" />
</div>
<div className="flex justify-between pt-4">
<div className="h-4 w-24 bg-gray-200 rounded" />
<div className="h-4 w-20 bg-gray-200 rounded" />
</div>
</div>
))}
</div>
);
};
export default OrdersSkeleton;

View File

@@ -0,0 +1,215 @@
// import { Package, Calendar, IndianRupee } from "lucide-react";
// import { useGetOrdersQuery } from "../../features/orders/ordersApi";
// import OrdersSkeleton from "./OrdersSkeleton";
// const OrdersTab = () => {
// const { data, isLoading, isError } = useGetOrdersQuery({});
// if (isLoading) return <OrdersSkeleton />;
// if (isError)
// return (
// <p className="text-center text-red-600 font-medium py-10">
// Failed to load orders
// </p>
// );
// const orders = data?.orders ?? [];
// if (!orders.length) {
// return (
// <div className="text-center py-16">
// <Package className="mx-auto mb-4 text-gray-400" size={42} />
// <p className="text-gray-500">No orders found</p>
// </div>
// );
// }
// return (
// <div className="space-y-6">
// {orders.map((order) => (
// <div
// key={order.id}
// className="bg-white rounded-2xl border shadow-sm hover:shadow-md transition p-6"
// >
// {/* Header */}
// <div className="flex justify-between items-center mb-4">
// <div>
// <p className="text-sm text-gray-500">Order ID</p>
// <p className="font-semibold">{order.orderNumber}</p>
// </div>
// <span
// className={`px-3 py-1 rounded-full text-xs font-medium
// ${
// order.status === "PENDING"
// ? "bg-yellow-100 text-yellow-700"
// : "bg-green-100 text-green-700"
// }`}
// >
// {order.status}
// </span>
// </div>
// {/* Items */}
// <div className="divide-y">
// {order.items.map((item) => (
// <div
// key={item.id}
// className="flex justify-between py-3 text-sm"
// >
// <div>
// <p className="font-medium">{item.productName}</p>
// <p className="text-gray-500">Qty: {item.quantity}</p>
// </div>
// <p className="font-semibold">₹{item.price}</p>
// </div>
// ))}
// </div>
// {/* Footer */}
// <div className="flex justify-between items-center mt-5 pt-4 border-t text-sm">
// <div className="flex items-center gap-2 text-gray-500">
// <Calendar size={16} />
// {new Date(order.createdAt).toLocaleDateString()}
// </div>
// <div className="flex items-center gap-1 font-semibold">
// <IndianRupee size={16} />
// {order.totalAmount}
// </div>
// </div>
// </div>
// ))}
// </div>
// );
// };
// export default OrdersTab;
import {
Package,
Calendar,
IndianRupee,
Truck,
CheckCircle,
XCircle,
} from "lucide-react";
import { useGetOrdersQuery } from "../../features/orders/ordersApi";
import OrdersSkeleton from "./OrdersSkeleton";
// Status colors
const statusColors = {
PENDING: "bg-yellow-100 text-yellow-800",
SHIPPED: "bg-blue-100 text-blue-800",
DELIVERED: "bg-green-100 text-green-800",
CANCELLED: "bg-red-100 text-red-800",
};
const OrdersTab = () => {
const { data, isLoading, isError } = useGetOrdersQuery({});
if (isLoading) return <OrdersSkeleton />;
if (isError)
return (
<p className="text-center text-red-600 font-medium py-10">
Failed to load orders
</p>
);
const orders = data?.orders ?? [];
if (!orders.length)
return (
<div className="text-center py-16">
<Package className="mx-auto mb-4 text-gray-400" size={48} />
<p className="text-gray-500 text-lg">No orders found</p>
</div>
);
return (
<div className="space-y-6">
{orders.map((order) => (
<div
key={order.id}
className="bg-white rounded-3xl border border-gray-100 shadow-lg hover:shadow-2xl transition p-6"
>
{/* Header */}
<div className="flex justify-between items-start mb-4">
<div>
<p className="text-sm text-gray-400">Order ID</p>
<p className="font-semibold text-gray-800">{order.orderNumber}</p>
<p className="text-xs text-gray-500 mt-1">
Payment: {order.paymentMethod}
</p>
</div>
<span
className={`px-3 py-1 rounded-full text-xs font-medium ${
statusColors[order.status]
}`}
>
{order.status}
</span>
</div>
{/* Items */}
<div className="divide-y divide-gray-100">
{order.items.map((item) => (
<div
key={item.id}
className="flex justify-between py-3 items-center"
>
<div>
<p className="font-medium text-gray-700">
{item.productName}
</p>
<p className="text-gray-400 text-sm mt-1">
Qty: {item.quantity}
</p>
</div>
<p className="font-semibold text-gray-900">
{Number(item.price) * item.quantity}
</p>
</div>
))}
</div>
{/* Footer */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mt-5 pt-4 border-t border-gray-100 text-sm">
<div className="flex items-center gap-2 text-gray-500 mb-2 md:mb-0">
<Calendar size={16} />
{new Date(order.createdAt).toLocaleDateString()}
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-1 font-semibold text-gray-800">
<IndianRupee size={16} />
{order.totalAmount}
</div>
{/* Dynamic status icons */}
{order.status === "SHIPPED" && (
<div className="flex items-center gap-1 text-blue-600 font-medium">
<Truck size={16} /> Tracking
</div>
)}
{order.status === "DELIVERED" && (
<div className="flex items-center gap-1 text-green-600 font-medium">
<CheckCircle size={16} /> Delivered
</div>
)}
{order.status === "CANCELLED" && (
<div className="flex items-center gap-1 text-red-600 font-medium">
<XCircle size={16} /> Cancelled
</div>
)}
</div>
</div>
</div>
))}
</div>
);
};
export default OrdersTab;

View File

@@ -0,0 +1,20 @@
import { Link } from "react-router-dom";
const ProfileActions = ({setActiveTab }) => (
<div className="flex gap-4 mt-4">
<button
onClick={() => setActiveTab("edit-profile")}
className="flex-1 bg-primary-default text-white py-2 rounded-lg text-center font-medium hover:bg-primary-dark transition"
>
Edit Profile
</button>
<button
onClick={() => setActiveTab("change-password")}
className="flex-1 border border-primary-default text-primary-default py-2 rounded-lg text-center font-medium hover:bg-primary-default hover:text-white transition"
>
Change Password
</button>
</div>
);
export default ProfileActions;

View File

@@ -0,0 +1,88 @@
import ProfileHeader from "./ProfileHeader";
import ProfileInfoCard from "./ProfileInfoCard";
import ProfileStats from "./ProfileStats";
import ProfileActions from "./ProfileActions";
import Wishlist from "../navbar/Wishlist";
import Addresses from "./addresses/Addresses";
import AddAddressForm from "./addresses/AddAddressForm";
import AddressSection from "./addresses/AddressSection";
import OrdersTab from "./OrdersTab";
import EditProfilePage from "./EditProfilePage";
import ChangePassword from "./ChangePassword";
const ProfileContent = ({ activeTab, setActiveTab, user }) => {
return (
<div className="md:w-3/4 flex-1 space-y-6 border border-gray-300 rounded-md">
{activeTab === "profile" && (
<>
<ProfileHeader user={user} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<ProfileInfoCard
title="Phone"
value={user?.phone || "Not provided"}
/>
<ProfileInfoCard
title="Email"
value={user?.email || "Not provided"}
/>
<ProfileInfoCard
title="City"
value={user?.city || "Not provided"}
/>
<ProfileInfoCard
title="State"
value={user?.state || "Not provided"}
/>
<ProfileInfoCard
title="Address"
value={user?.address || "Not provided"}
/>
</div>
<ProfileStats
orders={user?.orders || []}
wishlist={user?.wishlist || []}
reviews={user?.reviews || []}
/>
{/* <ProfileActions /> */}
<ProfileActions setActiveTab={setActiveTab} />
{/* {activeTab === "orders" && <OrdersTab />} */}
</>
)}
{activeTab === "edit-profile" && (
<EditProfilePage setActiveTab={setActiveTab} />
)}
{activeTab === "change-password" && (
<ChangePassword />
)}
{activeTab === "wishlist" && (
<div className="bg-white p-6 rounded-2xl shadow-lg">
<h2 className="text-2xl font-bold text-primary-dark mb-4">
My Wishlist
</h2>
<Wishlist />
</div>
)}
{activeTab === "orders" && (
<div className="bg-white p-6 rounded-2xl shadow-lg">
<h2 className="text-2xl font-bold text-primary-dark mb-4">
My Orders
</h2>
<OrdersTab />
</div>
)}
{activeTab === "address" && <AddressSection />}
</div>
);
};
export default ProfileContent;

View File

@@ -0,0 +1,27 @@
import { motion } from "framer-motion";
const ProfileHeader = ({ user }) => {
if (!user) return null; // or show a loader/fallback
return (
<motion.div
className="flex items-center gap-4 p-6 bg-white rounded-xl shadow-md"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<img
src={user.avatar || "/default-avatar.png"}
alt="Profile"
className="w-24 h-24 rounded-full object-cover border-2 border-primary-default"
/>
<div>
<h2 className="text-2xl font-bold text-gray-900">
{`${user.firstName || ""} ${user.lastName || ""}`}
</h2>
<p className="text-gray-500">{user.email || ""}</p>
</div>
</motion.div>
);
};
export default ProfileHeader;

View File

@@ -0,0 +1,8 @@
const ProfileInfoCard = ({ title, value }) => (
<div className="bg-white p-4 rounded-xl shadow-md flex justify-between items-center">
<span className="text-gray-700 font-medium">{title}</span>
<span className="text-gray-900 font-semibold">{value || "Not provided"}</span>
</div>
);
export default ProfileInfoCard;

View File

@@ -0,0 +1,51 @@
import { useState } from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { useGetProfileQuery } from "../../features/auth/authApi";
// import { useDispatch } from "react-redux";
import { logout, setCredentials } from "../../features/auth/authSlice";
import ProfileSidebar from "./ProfileSidebar";
import ProfileContent from "./ProfileContent";
const ProfilePage = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { data, isLoading, isError } = useGetProfileQuery();
const [activeTab, setActiveTab] = useState("profile"); // <--- state lifted
if (isLoading) return <p className="text-center py-10">Loading profile...</p>;
if (isError)
return (
<p className="text-center py-10 text-red-500">Failed to load profile</p>
);
const user = data?.data?.user;
const handleLogout = () => {
dispatch(logout());
localStorage.clear();
navigate("/");
};
return (
<div className="px-4 md:px-20 py-10 mt-24 flex flex-col md:flex-row gap-6">
{/* Sidebar */}
<ProfileSidebar
activeTab={activeTab} // pass current tab
setActiveTab={setActiveTab}
onLogout={handleLogout}
/>
{/* Content */}
{/* <ProfileContent activeTab="profile" user={user} /> */}
{/* Content */}
<ProfileContent
activeTab={activeTab}
setActiveTab={setActiveTab}
user={user}
/>
</div>
);
};
export default ProfilePage;

View File

@@ -0,0 +1,63 @@
import { User, Heart, MapPin, LogOut, Menu, ShoppingBag } from "lucide-react";
import { useState } from "react";
const ProfileSidebar = ({ activeTab, setActiveTab, onLogout }) => {
const [openMobile, setOpenMobile] = useState(false);
const tabs = [
{ id: "profile", label: "My Profile", icon: <User size={18} /> },
{ id: "wishlist", label: "My Wishlist", icon: <Heart size={18} /> },
{ id: "address", label: "Address", icon: <MapPin size={18} /> },
{ id: "orders", label: "Orders", icon: <ShoppingBag size={18} /> },
];
return (
<>
{/* Mobile Toggle */}
<div className="md:hidden flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Profile</h2>
<button
className="p-2 bg-gray-200 rounded-md"
onClick={() => setOpenMobile(!openMobile)}
>
<Menu size={20} />
</button>
</div>
{/* Sidebar */}
<div
className={`bg-white shadow-lg rounded-2xl p-4 md:sticky md:top-28 md:w-1/4 flex flex-col space-y-4 transition-all border bottom-7
${openMobile ? "block" : "hidden"} md:block`}
>
{tabs.map((tab) => (
<div
key={tab.id}
className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer transition
${
activeTab === tab.id
? "bg-primary-default text-white shadow-md"
: "hover:bg-primary-default hover:text-white"
}`}
onClick={() => {
setActiveTab(tab.id);
setOpenMobile(false); // close mobile menu
}}
>
{tab.icon}
<span className="font-medium">{tab.label}</span>
</div>
))}
<div
className="flex items-center gap-3 p-3 rounded-xl cursor-pointer text-red-500 hover:bg-red-500 hover:text-white transition"
onClick={onLogout}
>
<LogOut size={18} />
Logout
</div>
</div>
</>
);
};
export default ProfileSidebar;

View File

@@ -0,0 +1,20 @@
const ProfileStats = ({ orders, wishlist, reviews }) => {
const stats = [
{ label: "Orders", value: orders.length },
{ label: "Wishlist", value: wishlist.length },
{ label: "Reviews", value: reviews.length },
];
return (
<div className="grid grid-cols-3 gap-4">
{stats.map((stat, i) => (
<div key={i} className="bg-white p-4 rounded-xl shadow-md text-center">
<h3 className="text-xl font-bold text-gray-900">{stat.value}</h3>
<p className="text-gray-500">{stat.label}</p>
</div>
))}
</div>
);
};
export default ProfileStats;

View File

@@ -0,0 +1,82 @@
import { useAddAddressMutation } from "../../../features/auth/authApi";
import { useState } from "react";
const AddAddressForm = () => {
const [addAddress, { isLoading }] = useAddAddressMutation();
const [formData, setFormData] = useState({
type: "SHIPPING",
isDefault: false,
firstName: "",
lastName: "",
company: "",
addressLine1: "",
addressLine2: "",
city: "",
state: "",
postalCode: "",
country: "",
phone: "",
});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
[name]: type === "checkbox" ? checked : value,
});
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
await addAddress(formData).unwrap();
alert("Address added successfully");
} catch (err) {
console.error(err);
alert("Failed to add address");
}
};
return (
<form
onSubmit={handleSubmit}
className="bg-white p-8 rounded-3xl shadow-lg max-w-2xl mx-auto space-y-4"
>
<h2 className="text-2xl font-bold mb-4">Add New Address</h2>
<div className="grid grid-cols-2 gap-4">
<input name="firstName" onChange={handleChange} placeholder="First Name" className="inp" />
<input name="lastName" onChange={handleChange} placeholder="Last Name" className="inp" />
<input name="company" onChange={handleChange} placeholder="Company" className="inp" />
<input name="phone" onChange={handleChange} placeholder="Phone" className="inp" />
</div>
<input name="addressLine1" onChange={handleChange} placeholder="Address Line 1" className="inp w-full" />
<input name="addressLine2" onChange={handleChange} placeholder="Address Line 2" className="inp w-full" />
<div className="grid grid-cols-3 gap-4">
<input name="city" onChange={handleChange} placeholder="City" className="inp" />
<input name="state" onChange={handleChange} placeholder="State" className="inp" />
<input name="postalCode" onChange={handleChange} placeholder="Postal Code" className="inp" />
</div>
<input name="country" onChange={handleChange} placeholder="Country" className="inp w-full" />
<div className="flex items-center gap-3 mt-3">
<input type="checkbox" name="isDefault" onChange={handleChange} />
<label>Make this default address</label>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full bg-blue-600 text-white py-3 rounded-2xl text-lg font-semibold hover:bg-blue-700"
>
{isLoading ? "Saving..." : "Add Address"}
</button>
</form>
);
};
export default AddAddressForm;

View File

@@ -0,0 +1,83 @@
import { MapPin, Phone, Home, Briefcase, Star } from "lucide-react";
import { useDeleteAddressMutation } from "../../../features/auth/authApi";
const AddressCard = ({ address, onEdit }) => {
const [deleteAddress] = useDeleteAddressMutation();
const handleDelete = async () => {
if (confirm("Are you sure you want to delete this address?")) {
await deleteAddress(address.id);
alert("Address deleted!");
onClose();
}
};
return (
<div
className="bg-gradient-to-br from-white/80 to-gray-100/80
backdrop-blur-lg border border-gray-200 p-6 rounded-3xl shadow-lg
hover:shadow-2xl transition-all duration-300 w-full"
>
{/* Header */}
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-3">
<Home size={22} className="text-blue-500" />
<h3 className="text-xl font-bold text-gray-900">
{address.firstName} {address.lastName}
</h3>
</div>
{address.isDefault && (
<span
className="flex items-center gap-1 bg-yellow-400 text-white
px-3 py-1 rounded-full font-semibold shadow-md text-sm"
>
<Star size={14} /> Default
</span>
)}
</div>
{/* Company */}
{address.company && (
<p className="flex items-center gap-2 text-purple-600 font-medium mb-2">
<Briefcase size={16} /> {address.company}
</p>
)}
{/* Address */}
<div className="text-gray-700 space-y-1">
<p className="flex items-center gap-2">
<MapPin size={16} className="text-red-500" />
{address.addressLine1}, {address.addressLine2}
</p>
<p>
{address.city}, {address.state} - {address.postalCode}
</p>
<p>{address.country}</p>
<p className="flex items-center gap-2 font-medium">
<Phone size={16} className="text-green-500" /> {address.phone}
</p>
</div>
{/* Actions */}
<div className="flex justify-end gap-3 mt-5">
<button
onClick={() => onEdit(address)}
className="px-5 py-2 bg-blue-600 text-white rounded-xl font-semibold hover:bg-blue-700"
>
Edit
</button>
<button
onClick={handleDelete}
className="px-5 py-2 bg-red-600 text-white rounded-xl font-semibold hover:bg-red-700"
>
Delete
</button>
</div>
</div>
);
};
export default AddressCard;

Some files were not shown because too many files have changed in this diff Show More