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;
|
||||||