first commit
95
.gitignore
vendored
Normal 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
@@ -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
@@ -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
@@ -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
38
package.json
Normal 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
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
public/Frame (1).png
Normal file
|
After Width: | Height: | Size: 767 B |
BIN
public/Frame.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/add-to-cart.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
public/default-product.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
public/empty-wishlist.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/featured-product.avif
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
public/hero1.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
public/hero2.png
Normal file
|
After Width: | Height: | Size: 10 MiB |
BIN
public/hero3.png
Normal file
|
After Width: | Height: | Size: 12 MiB |
BIN
public/hero4.png
Normal file
|
After Width: | Height: | Size: 11 MiB |
BIN
public/hero5.jpg
Normal file
|
After Width: | Height: | Size: 9.4 MiB |
BIN
public/logo-small.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/logo-small2.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/logo.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/most-loved-banner.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/not-found.jpg
Normal file
|
After Width: | Height: | Size: 305 KiB |
BIN
public/order-success.png
Normal file
|
After Width: | Height: | Size: 300 KiB |
BIN
public/rating.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
1
public/vite.svg
Normal 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
@@ -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
@@ -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
@@ -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 |
210
src/components/Filters/FilterPanel.jsx
Normal 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;
|
||||
48
src/components/Offerings/OfferingCard.jsx
Normal 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;
|
||||
31
src/components/Offerings/OfferingsSection.jsx
Normal 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;
|
||||
9
src/components/common/Badge.jsx
Normal 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;
|
||||
18
src/components/common/Button.jsx
Normal 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;
|
||||
0
src/components/common/Card.jsx
Normal file
37
src/components/common/Checkbox.jsx
Normal 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;
|
||||
66
src/components/common/EmptyState.jsx
Normal 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;
|
||||
15
src/components/common/Input.jsx
Normal 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;
|
||||
7
src/components/common/Loader.jsx
Normal 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;
|
||||
0
src/components/common/Modal.jsx
Normal file
14
src/components/common/ProductGridSkeleton.jsx
Normal 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;
|
||||
0
src/components/common/Select.jsx
Normal file
27
src/components/common/Table.jsx
Normal 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;
|
||||
18
src/components/home/Category/Category.jsx
Normal 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;
|
||||
51
src/components/home/Category/CategoryCard.jsx
Normal 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;
|
||||
78
src/components/home/Category/CategoryCarousel.jsx
Normal 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;
|
||||
64
src/components/home/Category/CategoryGrid.jsx
Normal 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;
|
||||
|
||||
1
src/components/home/Category/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./Category";
|
||||
130
src/components/home/HeroSection/HeroSection.jsx
Normal 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;
|
||||
23
src/components/home/Home.jsx
Normal 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;
|
||||
62
src/components/home/MostLoved/MostLoved.jsx
Normal 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;
|
||||
177
src/components/home/MostLoved/MostLovedCard.jsx
Normal 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;
|
||||
187
src/components/home/NewGems/NewArrivalCard.jsx
Normal 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;
|
||||
61
src/components/home/NewGems/NewGems.jsx
Normal 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;
|
||||
157
src/components/home/RecommendedForYou/RecommendedForYou.jsx
Normal 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;
|
||||
166
src/components/layout/Footer.jsx
Normal 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;
|
||||
18
src/components/navbar/BellIcon.jsx
Normal 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;
|
||||
46
src/components/navbar/CartIcon.jsx
Normal 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;
|
||||
21
src/components/navbar/Logo.jsx
Normal 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;
|
||||
144
src/components/navbar/MenuLinks.jsx
Normal 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;
|
||||
132
src/components/navbar/MobileMenu.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
145
src/components/navbar/Navbar.jsx
Normal 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;
|
||||
20
src/components/navbar/SearchBar.jsx
Normal 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;
|
||||
124
src/components/navbar/TopPromoBanner.jsx
Normal 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;
|
||||
80
src/components/navbar/UserIcon.jsx
Normal 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;
|
||||
353
src/components/navbar/Wishlist.jsx
Normal 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;
|
||||
26
src/components/navbar/WishlistEmpty.jsx
Normal 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;
|
||||
36
src/components/order/CancelOrderButton.jsx
Normal 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;
|
||||
134
src/components/order/DeliveryAndPriceCard.jsx
Normal 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;
|
||||
457
src/components/order/MyOrders.jsx
Normal 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;
|
||||
13
src/components/order/OrderDetails.jsx
Normal 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;
|
||||
395
src/components/order/OrderDetailsPage.jsx
Normal 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;
|
||||
72
src/components/order/OrderFilters.jsx
Normal 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;
|
||||
18
src/components/order/OrderSuccessCard.jsx
Normal 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;
|
||||
251
src/components/order/OrderTrackingTimeline.jsx
Normal 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;
|
||||
28
src/components/order/SuccessActions.jsx
Normal 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;
|
||||
67
src/components/order/TrackingStep.jsx
Normal 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;
|
||||
109
src/components/product/PinCodeCheck.jsx
Normal 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">2–3 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;
|
||||
292
src/components/product/ProductCategoryPage.jsx
Normal 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;
|
||||
12
src/components/product/ProductDeclaration.jsx
Normal 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;
|
||||
117
src/components/product/ProductDetails.jsx
Normal 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;
|
||||
70
src/components/product/ProductDetailsSection.jsx
Normal 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;
|
||||
120
src/components/product/ProductImages.jsx
Normal 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;
|
||||
163
src/components/product/ProductInfo.jsx
Normal 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;
|
||||
84
src/components/product/RecommendedProducts.jsx
Normal 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;
|
||||
12
src/components/product/ShippingReturns.jsx
Normal 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;
|
||||
108
src/components/profile/ChangePassword.jsx
Normal 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;
|
||||
134
src/components/profile/EditProfilePage.jsx
Normal 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;
|
||||
29
src/components/profile/OrdersSkeleton.jsx
Normal 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;
|
||||
215
src/components/profile/OrdersTab.jsx
Normal 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;
|
||||
20
src/components/profile/ProfileActions.jsx
Normal 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;
|
||||
88
src/components/profile/ProfileContent.jsx
Normal 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;
|
||||
27
src/components/profile/ProfileHeader.jsx
Normal 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;
|
||||
8
src/components/profile/ProfileInfoCard.jsx
Normal 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;
|
||||
51
src/components/profile/ProfilePage.jsx
Normal 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;
|
||||
63
src/components/profile/ProfileSidebar.jsx
Normal 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;
|
||||
20
src/components/profile/ProfileStats.jsx
Normal 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;
|
||||
82
src/components/profile/addresses/AddAddressForm.jsx
Normal 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;
|
||||
83
src/components/profile/addresses/AddressCard.jsx
Normal 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;
|
||||