Initial commit

This commit is contained in:
2026-09-16 17:36:46 +05:30
commit c9f429d123
117 changed files with 14145 additions and 0 deletions

8
client/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules/
.next/
dist/
build/
.env
.env.local
.env.*.local
*.log

17
client/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Antigravity Guitars India | Crafted Sound. Pure Perfection.</title>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,600;0,700;1,400&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
</head>
<body class="bg-slate-50 text-slate-900 font-sans antialiased selection:bg-amber-500 selection:text-white">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3239
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
client/package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "guitar-ecommerce-client",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@reduxjs/toolkit": "^2.2.3",
"@tanstack/react-query": "^5.29.2",
"axios": "^1.6.8",
"clsx": "^2.1.0",
"framer-motion": "^11.1.7",
"lucide-react": "^0.368.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.3",
"react-redux": "^9.1.1",
"react-router-dom": "^6.22.3",
"tailwind-merge": "^2.2.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"vite": "^5.2.8"
}
}

7
client/postcss.config.js Normal file
View File

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

120
client/src/App.jsx Normal file
View File

@@ -0,0 +1,120 @@
import React, { useState } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
// Scroll to top on page change
import ScrollToTop from './components/ScrollToTop';
// Layouts & Global Components
import Navbar from './components/Navbar';
import MobileNav from './components/MobileNav';
import Footer from './components/Footer';
import ProductQuickView from './components/ProductQuickView';
import GuitarFinderModal from './components/GuitarFinderModal';
import CompareDrawer from './components/CompareDrawer';
import ToastNotification from './components/ToastNotification';
// Customer Pages
import HomePage from './pages/customer/HomePage';
import ShopPage from './pages/customer/ShopPage';
import ProductDetailPage from './pages/customer/ProductDetailPage';
import ComparePage from './pages/customer/ComparePage';
import WishlistPage from './pages/customer/WishlistPage';
import CartPage from './pages/customer/CartPage';
import CheckoutPage from './pages/customer/CheckoutPage';
import OrderConfirmationPage from './pages/customer/OrderConfirmationPage';
import OrderTrackingPage from './pages/customer/OrderTrackingPage';
import UserDashboardPage from './pages/customer/UserDashboardPage';
import ContactPage from './pages/customer/ContactPage';
import { AboutUsPage, PolicyPage } from './pages/customer/CMSPages';
import LoginPage from './pages/customer/LoginPage';
// Admin Pages & Dedicated Admin Login
import AdminLoginPage from './pages/admin/AdminLoginPage';
import AdminLayout from './pages/admin/AdminLayout';
import AdminDashboard from './pages/admin/AdminDashboard';
import AdminProducts from './pages/admin/AdminProducts';
import AdminInventory from './pages/admin/AdminInventory';
import AdminOrders from './pages/admin/AdminOrders';
import AdminCategories from './pages/admin/AdminCategories';
import AdminBrands from './pages/admin/AdminBrands';
import AdminCoupons from './pages/admin/AdminCoupons';
import AdminBanners from './pages/admin/AdminBanners';
import AdminSettings from './pages/admin/AdminSettings';
import AdminMessages from './pages/admin/AdminMessages';
import AdminEmailTemplates from './pages/admin/AdminEmailTemplates';
// Protected Admin Route guard (redirects to /admin/login if not admin)
const ProtectedAdminRoute = ({ children }) => {
const { userInfo } = useSelector((state) => state.auth);
if (!userInfo || userInfo.role !== 'Admin') {
return <Navigate to="/admin/login" replace />;
}
return children;
};
const CustomerLayoutWrapper = ({ children }) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<div className="flex flex-col min-h-screen bg-slate-50 text-slate-900 font-sans">
<Navbar onOpenMobileMenu={() => setMobileMenuOpen(true)} />
<MobileNav isOpen={mobileMenuOpen} onClose={() => setMobileMenuOpen(false)} />
<main className="flex-1">{children}</main>
<Footer />
<ProductQuickView />
<GuitarFinderModal />
<CompareDrawer />
<ToastNotification />
</div>
);
};
const App = () => {
return (
<>
<ScrollToTop />
<Routes>
{/* Customer Website Routes */}
<Route path="/" element={<CustomerLayoutWrapper><HomePage /></CustomerLayoutWrapper>} />
<Route path="/shop" element={<CustomerLayoutWrapper><ShopPage /></CustomerLayoutWrapper>} />
<Route path="/product/:slug" element={<CustomerLayoutWrapper><ProductDetailPage /></CustomerLayoutWrapper>} />
<Route path="/compare" element={<CustomerLayoutWrapper><ComparePage /></CustomerLayoutWrapper>} />
<Route path="/wishlist" element={<CustomerLayoutWrapper><WishlistPage /></CustomerLayoutWrapper>} />
<Route path="/cart" element={<CustomerLayoutWrapper><CartPage /></CustomerLayoutWrapper>} />
<Route path="/checkout" element={<CustomerLayoutWrapper><CheckoutPage /></CustomerLayoutWrapper>} />
<Route path="/order-confirmation/:id" element={<CustomerLayoutWrapper><OrderConfirmationPage /></CustomerLayoutWrapper>} />
<Route path="/track-order/:id" element={<CustomerLayoutWrapper><OrderTrackingPage /></CustomerLayoutWrapper>} />
<Route path="/dashboard" element={<CustomerLayoutWrapper><UserDashboardPage /></CustomerLayoutWrapper>} />
<Route path="/contact" element={<CustomerLayoutWrapper><ContactPage /></CustomerLayoutWrapper>} />
<Route path="/about" element={<CustomerLayoutWrapper><AboutUsPage /></CustomerLayoutWrapper>} />
<Route path="/privacy-policy" element={<CustomerLayoutWrapper><PolicyPage title="Privacy Policy" /></CustomerLayoutWrapper>} />
<Route path="/terms-conditions" element={<CustomerLayoutWrapper><PolicyPage title="Terms & Conditions" /></CustomerLayoutWrapper>} />
<Route path="/login" element={<CustomerLayoutWrapper><LoginPage /></CustomerLayoutWrapper>} />
{/* Dedicated Admin Login Route (Requirement 3) */}
<Route path="/admin/login" element={<AdminLoginPage />} />
{/* Protected Admin Portal Routes */}
<Route path="/admin" element={<ProtectedAdminRoute><AdminLayout /></ProtectedAdminRoute>}>
<Route index element={<AdminDashboard />} />
<Route path="products" element={<AdminProducts />} />
<Route path="inventory" element={<AdminInventory />} />
<Route path="orders" element={<AdminOrders />} />
<Route path="categories" element={<AdminCategories />} />
<Route path="brands" element={<AdminBrands />} />
<Route path="coupons" element={<AdminCoupons />} />
<Route path="banners" element={<AdminBanners />} />
<Route path="settings" element={<AdminSettings />} />
<Route path="messages" element={<AdminMessages />} />
<Route path="email-templates" element={<AdminEmailTemplates />} />
</Route>
{/* Fallback */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</>
);
};
export default App;

30
client/src/api/axios.js Normal file
View File

@@ -0,0 +1,30 @@
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json'
}
});
// Request interceptor to attach JWT token
api.interceptors.request.use(
(config) => {
const user = JSON.parse(localStorage.getItem('userInfo'));
if (user && user.token) {
config.headers.Authorization = `Bearer ${user.token}`;
}
// Guest Session ID for cart
let sessionId = localStorage.getItem('guestSessionId');
if (!sessionId) {
sessionId = 'session_' + Math.random().toString(36).substring(2, 9);
localStorage.setItem('guestSessionId', sessionId);
}
config.headers['x-session-id'] = sessionId;
return config;
},
(error) => Promise.reject(error)
);
export default api;

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
import { X, Sliders, ArrowRight } from 'lucide-react';
import { removeFromCompare, clearCompare } from '../store/slices/compareSlice';
const CompareDrawer = () => {
const dispatch = useDispatch();
const { items } = useSelector((state) => state.compare);
if (items.length === 0) return null;
return (
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-40 w-11/12 max-w-3xl bg-white/95 border border-slate-200/90 backdrop-blur-md rounded-2xl p-4 shadow-xl flex items-center justify-between gap-4 text-slate-900">
<div className="flex items-center gap-3">
<div className="p-2.5 bg-amber-50 border border-amber-200 text-amber-600 rounded-xl">
<Sliders className="w-5 h-5" />
</div>
<div className="text-left">
<div className="text-xs font-bold text-slate-900">Compare Guitars ({items.length}/4)</div>
<div className="text-[11px] text-slate-500 hidden sm:block">Side-by-side woods, pickups & price comparison</div>
</div>
</div>
<div className="flex items-center gap-2 overflow-x-auto">
{items.map((item) => (
<div key={item._id} className="relative group flex-shrink-0">
<img src={item.images?.[0]} alt="" className="w-10 h-10 object-cover rounded-lg border border-slate-200" />
<button
onClick={() => dispatch(removeFromCompare(item._id))}
className="absolute -top-1.5 -right-1.5 p-0.5 bg-rose-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-sm"
>
<X className="w-3 h-3" />
</button>
</div>
))}
</div>
<div className="flex items-center gap-3">
<button
onClick={() => dispatch(clearCompare())}
className="text-xs text-slate-500 hover:text-slate-900 hidden sm:block"
>
Clear
</button>
<Link
to="/compare"
className="py-2 px-4 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs flex items-center gap-1.5 shadow-sm"
>
Compare Now <ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
);
};
export default CompareDrawer;

View File

@@ -0,0 +1,155 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Instagram, Facebook, Youtube, Twitter, Mail, Phone, MapPin, ShieldCheck, Truck, RotateCcw, Headphones } from 'lucide-react';
import api from '../api/axios';
const Footer = () => {
const [settings, setSettings] = useState({
siteName: 'Antigravity Guitars India',
tagline: 'Crafted Sound. Pure Perfection.',
businessName: 'Antigravity Music Private Limited',
gstin: '29ABCDE1234F1Z5',
addressLine1: '100 Brigade Road, Ashok Nagar',
addressLine2: 'Suite 400, Music Square',
city: 'Bengaluru',
state: 'Karnataka',
pincode: '560025',
country: 'India',
phone: '+91 98765 43210',
contactEmail: 'support@antigravityguitars.in'
});
const [socials, setSocials] = useState([]);
useEffect(() => {
api.get('/settings').then(res => setSettings(res.data.data)).catch(() => {});
api.get('/social-media').then(res => setSocials(res.data.data)).catch(() => {});
}, []);
return (
<footer className="bg-slate-900 text-slate-400 text-xs border-t border-slate-800">
{/* Features Bar */}
<div className="border-b border-slate-800/80 py-10 bg-slate-950/40">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="flex items-center gap-4">
<div className="p-3 bg-amber-500/10 text-amber-400 rounded-2xl border border-amber-500/20">
<Truck className="w-6 h-6" />
</div>
<div>
<div className="font-bold text-white text-sm">Free Courier Delivery</div>
<div className="text-xs text-slate-400">On all India orders over ₹5,000</div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="p-3 bg-amber-500/10 text-amber-400 rounded-2xl border border-amber-500/20">
<ShieldCheck className="w-6 h-6" />
</div>
<div>
<div className="font-bold text-white text-sm">100% Authentic Guitars</div>
<div className="text-xs text-slate-400">Official Indian brand warranty</div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="p-3 bg-amber-500/10 text-amber-400 rounded-2xl border border-amber-500/20">
<RotateCcw className="w-6 h-6" />
</div>
<div>
<div className="font-bold text-white text-sm">10-Day Hassle Returns</div>
<div className="text-xs text-slate-400">Safe studio trial period</div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="p-3 bg-amber-500/10 text-amber-400 rounded-2xl border border-amber-500/20">
<Headphones className="w-6 h-6" />
</div>
<div>
<div className="font-bold text-white text-sm">Luthier Support India</div>
<div className="text-xs text-slate-400">Expert setup & advice</div>
</div>
</div>
</div>
</div>
{/* Main Footer Links */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 grid grid-cols-1 md:grid-cols-4 gap-10">
{/* Brand Info */}
<div className="space-y-4">
<div className="flex items-center space-x-3">
<div className="w-9 h-9 bg-amber-500 rounded-xl flex items-center justify-center font-bold text-white text-lg">AG</div>
<span className="text-lg font-serif font-bold text-white">{settings.siteName}</span>
</div>
<p className="text-xs text-slate-400 leading-relaxed">
{settings.tagline}. India's premier music destination providing hand-crafted acoustic, electric, and custom shop guitars.
</p>
<div className="text-[11px] text-amber-400 font-semibold">
GSTIN: {settings.gstin || '29ABCDE1234F1Z5'}
</div>
</div>
{/* Quick Links */}
<div>
<h4 className="text-xs font-bold text-white uppercase tracking-widest mb-4">Explore Instruments</h4>
<ul className="space-y-2.5 text-xs">
<li><Link to="/shop" className="hover:text-amber-400 transition-colors">All Guitars Catalog</Link></li>
<li><Link to="/shop?category=electric-guitars" className="hover:text-amber-400 transition-colors">Electric Guitars</Link></li>
<li><Link to="/shop?category=acoustic-guitars" className="hover:text-amber-400 transition-colors">Acoustic Guitars</Link></li>
<li><Link to="/shop?category=bass-guitars" className="hover:text-amber-400 transition-colors">Bass Guitars</Link></li>
<li><Link to="/shop?category=amplifiers" className="hover:text-amber-400 transition-colors">Amplifiers & Pedals</Link></li>
</ul>
</div>
{/* Customer Care */}
<div>
<h4 className="text-xs font-bold text-white uppercase tracking-widest mb-4">Customer Care</h4>
<ul className="space-y-2.5 text-xs">
<li><Link to="/contact" className="hover:text-amber-400 transition-colors">Contact Us</Link></li>
<li><Link to="/about" className="hover:text-amber-400 transition-colors">About Antigravity India</Link></li>
<li><Link to="/dashboard" className="hover:text-amber-400 transition-colors">Track Order Status</Link></li>
<li><Link to="/privacy-policy" className="hover:text-amber-400 transition-colors">Privacy Policy</Link></li>
<li><Link to="/terms-conditions" className="hover:text-amber-400 transition-colors">Terms of Service</Link></li>
</ul>
</div>
{/* Requirement 17: Indian Business Address & Contact Info (Replaces VIP Tone Club) */}
<div className="space-y-3">
<h4 className="text-xs font-bold text-white uppercase tracking-widest mb-2">Indian Headquarters & Address</h4>
<div className="space-y-2 text-xs text-slate-300">
<div className="font-bold text-white">{settings.businessName || settings.siteName}</div>
<div className="flex items-start gap-2">
<MapPin className="w-4 h-4 text-amber-500 flex-shrink-0 mt-0.5" />
<span>
{settings.addressLine1 || '100 Brigade Road'}, {settings.addressLine2 || 'Ashok Nagar'},<br />
{settings.city || 'Bengaluru'}, {settings.state || 'Karnataka'} – {settings.pincode || '560025'}, {settings.country || 'India'}
</span>
</div>
<div className="flex items-center gap-2">
<Phone className="w-4 h-4 text-amber-500 flex-shrink-0" />
<span>{settings.phone || '+91 98765 43210'}</span>
</div>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-amber-500 flex-shrink-0" />
<span>{settings.contactEmail || 'support@antigravityguitars.in'}</span>
</div>
</div>
<div className="pt-2">
<div className="text-xs font-bold text-white mb-2">Connect With Us</div>
<div className="flex gap-2">
<a href="https://instagram.com" target="_blank" rel="noreferrer" className="p-2 bg-slate-800 rounded-lg hover:text-amber-400"><Instagram className="w-4 h-4" /></a>
<a href="https://facebook.com" target="_blank" rel="noreferrer" className="p-2 bg-slate-800 rounded-lg hover:text-amber-400"><Facebook className="w-4 h-4" /></a>
<a href="https://youtube.com" target="_blank" rel="noreferrer" className="p-2 bg-slate-800 rounded-lg hover:text-amber-400"><Youtube className="w-4 h-4" /></a>
<a href="https://x.com" target="_blank" rel="noreferrer" className="p-2 bg-slate-800 rounded-lg hover:text-amber-400"><Twitter className="w-4 h-4" /></a>
</div>
</div>
</div>
</div>
<div className="border-t border-slate-800/80 py-6 text-center text-xs text-slate-500">
© {new Date().getFullYear()} {settings.businessName || settings.siteName}. All Rights Reserved. Built with MERN Stack Architecture.
</div>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,193 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { X, Sparkles, ChevronRight, RotateCcw, ShoppingBag } from 'lucide-react';
import { closeGuitarFinder, showToast } from '../store/slices/uiSlice';
import { addToCartAsync } from '../store/slices/cartSlice';
import { formatINR } from '../utils/formatCurrency';
import api from '../api/axios';
const GuitarFinderModal = () => {
const dispatch = useDispatch();
const { guitarFinderOpen } = useSelector((state) => state.ui);
const [step, setStep] = useState(1);
const [answers, setAnswers] = useState({
skillLevel: 'Intermediate',
acousticElectric: 'Electric',
maxBudget: 100000, // ₹1,000,00
handOrientation: 'Right-Handed'
});
const [recommendations, setRecommendations] = useState([]);
const [loading, setLoading] = useState(false);
if (!guitarFinderOpen) return null;
const handleNext = async () => {
if (step < 3) {
setStep(step + 1);
} else {
setLoading(true);
try {
const res = await api.post('/products/guitar-finder', answers);
setRecommendations(res.data.data || []);
setStep(4);
} catch (err) {
dispatch(showToast({ message: 'Failed to generate recommendations', type: 'error' }));
} finally {
setLoading(false);
}
}
};
const handleReset = () => {
setStep(1);
setRecommendations([]);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => dispatch(closeGuitarFinder())} />
<div className="relative w-full max-w-2xl bg-white border border-slate-200 rounded-3xl p-6 md:p-8 shadow-2xl z-10 text-slate-900">
<button
onClick={() => dispatch(closeGuitarFinder())}
className="absolute top-6 right-6 p-2 text-slate-400 hover:text-slate-900 rounded-full bg-slate-100"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center gap-3 mb-6">
<div className="p-3 bg-amber-50 border border-amber-200 text-amber-600 rounded-2xl">
<Sparkles className="w-6 h-6" />
</div>
<div className="text-left">
<h2 className="text-xl font-bold text-slate-900 font-serif">Tone Matcher & Guitar Finder India</h2>
<p className="text-xs text-slate-500">Answer 3 quick questions to discover your ideal instrument</p>
</div>
</div>
{/* Step 1: Type */}
{step === 1 && (
<div className="space-y-6 animate-fadeIn text-left">
<div className="text-sm font-bold text-slate-800">1. What type of instrument are you looking for?</div>
<div className="grid grid-cols-2 gap-4">
{['Electric', 'Acoustic', 'Bass', 'Classical'].map((type) => (
<button
key={type}
onClick={() => setAnswers({ ...answers, acousticElectric: type })}
className={`p-4 rounded-2xl border text-left transition-all ${
answers.acousticElectric === type
? 'border-amber-500 bg-amber-50 text-slate-900 font-bold shadow-sm'
: 'border-slate-200 bg-slate-50 text-slate-600 hover:border-slate-300'
}`}
>
<div className="text-base mb-1 font-bold">{type} Guitar</div>
<div className="text-xs text-slate-500">Curated tone builds</div>
</button>
))}
</div>
</div>
)}
{/* Step 2: Budget (₹ INR) */}
{step === 2 && (
<div className="space-y-6 animate-fadeIn text-left">
<div className="text-sm font-bold text-slate-800">2. What is your target budget range?</div>
<div className="space-y-3">
{[
{ label: 'Under ₹30,000 (Player Standard)', value: 30000 },
{ label: '₹30,000 - ₹1,00,000 (Custom Deluxe)', value: 100000 },
{ label: '₹1,00,000+ (Master Custom Shop)', value: 300000 }
].map((b) => (
<button
key={b.value}
onClick={() => setAnswers({ ...answers, maxBudget: b.value })}
className={`w-full p-4 rounded-2xl border text-left transition-all ${
answers.maxBudget === b.value
? 'border-amber-500 bg-amber-50 text-slate-900 font-bold shadow-sm'
: 'border-slate-200 bg-slate-50 text-slate-600 hover:border-slate-300'
}`}
>
<div className="text-sm font-bold">{b.label}</div>
</button>
))}
</div>
</div>
)}
{/* Step 3: Handedness */}
{step === 3 && (
<div className="space-y-6 animate-fadeIn text-left">
<div className="text-sm font-bold text-slate-800">3. Select your playing hand orientation</div>
<div className="grid grid-cols-2 gap-4">
{['Right-Handed', 'Left-Handed'].map((orient) => (
<button
key={orient}
onClick={() => setAnswers({ ...answers, handOrientation: orient })}
className={`p-4 rounded-2xl border text-center transition-all ${
answers.handOrientation === orient
? 'border-amber-500 bg-amber-50 text-slate-900 font-bold shadow-sm'
: 'border-slate-200 bg-slate-50 text-slate-600 hover:border-slate-300'
}`}
>
<span className="text-sm font-bold">{orient}</span>
</button>
))}
</div>
</div>
)}
{/* Step 4: Results */}
{step === 4 && (
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-2 text-left">
<div className="flex items-center justify-between text-xs text-slate-500">
<span>Found {recommendations.length} matching instruments</span>
<button onClick={handleReset} className="flex items-center gap-1 text-amber-600 font-bold hover:underline">
<RotateCcw className="w-3.5 h-3.5" /> Retake Test
</button>
</div>
{recommendations.map((p) => (
<div key={p._id} className="p-4 bg-slate-50 border border-slate-200 rounded-2xl flex items-center justify-between gap-4 shadow-sm">
<img src={p.images?.[0]} alt="" className="w-16 h-16 object-cover rounded-xl border border-slate-200" />
<div className="flex-1">
<div className="text-xs font-bold text-amber-600">{p.brand?.name}</div>
<h4 className="text-sm font-bold text-slate-900 line-clamp-1">{p.name}</h4>
<div className="text-xs font-extrabold text-slate-900">{formatINR(p.salePrice || p.price)}</div>
</div>
<button
onClick={() => {
dispatch(addToCartAsync({ productId: p._id, quantity: 1 }));
dispatch(showToast({ message: `${p.name} added to cart!`, type: 'success' }));
dispatch(closeGuitarFinder());
}}
className="p-2.5 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs flex items-center gap-1 shadow-sm"
>
<ShoppingBag className="w-4 h-4" /> Add
</button>
</div>
))}
</div>
)}
{/* Footer controls */}
{step < 4 && (
<div className="mt-8 flex items-center justify-between border-t border-slate-100 pt-6">
<div className="text-xs text-slate-400 font-semibold">Step {step} of 3</div>
<button
onClick={handleNext}
disabled={loading}
className="py-3 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs flex items-center gap-2 shadow-sm"
>
{loading ? 'Matching...' : step === 3 ? 'Generate Matches' : 'Next Step'}
<ChevronRight className="w-4 h-4" />
</button>
</div>
)}
</div>
</div>
);
};
export default GuitarFinderModal;

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { X, Home, ShoppingBag, Heart, Compass, Sliders, User, Shield } from 'lucide-react';
import { useSelector, useDispatch } from 'react-redux';
import { openGuitarFinder } from '../store/slices/uiSlice';
const MobileNav = ({ isOpen, onClose }) => {
const dispatch = useDispatch();
const { userInfo } = useSelector((state) => state.auth);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 lg:hidden">
{/* Overlay */}
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={onClose} />
{/* Drawer */}
<div className="fixed inset-y-0 left-0 w-4/5 max-w-xs bg-white border-r border-slate-200 p-6 flex flex-col justify-between overflow-y-auto text-slate-900 shadow-xl">
<div>
<div className="flex items-center justify-between pb-6 border-b border-slate-100">
<div className="flex items-center space-x-2.5">
<div className="w-8 h-8 bg-amber-500 rounded-xl flex items-center justify-center font-bold text-white">AG</div>
<span className="font-serif font-bold text-lg text-slate-900">ANTIGRAVITY</span>
</div>
<button onClick={onClose} className="p-2 text-slate-400 hover:text-slate-900">
<X className="w-6 h-6" />
</button>
</div>
<div className="py-6 space-y-4 font-semibold text-slate-700">
<Link to="/" onClick={onClose} className="flex items-center gap-3 py-2 text-base hover:text-amber-600">
<Home className="w-5 h-5 text-slate-500" /> Home
</Link>
<Link to="/shop" onClick={onClose} className="flex items-center gap-3 py-2 text-base hover:text-amber-600">
<ShoppingBag className="w-5 h-5 text-slate-500" /> Shop All Guitars
</Link>
<button
onClick={() => { onClose(); dispatch(openGuitarFinder()); }}
className="w-full flex items-center gap-3 py-2 text-base text-amber-600 font-bold"
>
<Compass className="w-5 h-5 text-amber-600" /> Guitar Finder Tool
</button>
<Link to="/compare" onClick={onClose} className="flex items-center gap-3 py-2 text-base hover:text-amber-600">
<Sliders className="w-5 h-5 text-slate-500" /> Product Comparison
</Link>
<Link to="/wishlist" onClick={onClose} className="flex items-center gap-3 py-2 text-base hover:text-amber-600">
<Heart className="w-5 h-5 text-slate-500" /> Saved Wishlist
</Link>
{userInfo && userInfo.role === 'Admin' && (
<Link to="/admin" onClick={onClose} className="flex items-center gap-3 py-2 text-base text-amber-600 font-bold">
<Shield className="w-5 h-5" /> Admin Dashboard
</Link>
)}
</div>
</div>
<div className="pt-6 border-t border-slate-100">
{userInfo ? (
<Link to="/dashboard" onClick={onClose} className="flex items-center gap-3 p-3 bg-slate-50 border border-slate-200 rounded-2xl">
<img src={userInfo.avatar} alt="" className="w-9 h-9 rounded-full object-cover" />
<div>
<div className="text-xs font-bold text-slate-900">{userInfo.name}</div>
<div className="text-[11px] text-slate-500 truncate">{userInfo.email}</div>
</div>
</Link>
) : (
<Link
to="/login"
onClick={onClose}
className="block w-full py-3 text-center text-sm font-bold text-white bg-amber-500 rounded-2xl shadow-sm"
>
Sign In / Register
</Link>
)}
</div>
</div>
</div>
);
};
export default MobileNav;

View File

@@ -0,0 +1,286 @@
import React, { useState, useEffect, useRef } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { Search, ShoppingBag, Heart, Sliders, User, Menu, Shield, LogOut, Compass, ChevronDown, Sparkles } from 'lucide-react';
import { logout } from '../store/slices/authSlice';
import { openGuitarFinder } from '../store/slices/uiSlice';
import api from '../api/axios';
const Navbar = ({ onOpenMobileMenu }) => {
const navigate = useNavigate();
const location = useLocation();
const dispatch = useDispatch();
const { userInfo } = useSelector((state) => state.auth);
const { items: cartItems } = useSelector((state) => state.cart);
const { products: wishlistProducts } = useSelector((state) => state.wishlist);
const { items: compareItems } = useSelector((state) => state.compare);
const [searchQuery, setSearchQuery] = useState('');
const [suggestions, setSuggestions] = useState([]);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [categories, setCategories] = useState([]);
const [profileDropdownOpen, setProfileDropdownOpen] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
api.get('/categories').then((res) => setCategories(res.data.data)).catch(() => {});
}, []);
// Click outside listener for profile dropdown
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setProfileDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleSearchChange = async (e) => {
const query = e.target.value;
setSearchQuery(query);
if (query.trim().length > 1) {
try {
const res = await api.get(`/products?search=${encodeURIComponent(query)}&limit=5`);
setSuggestions(res.data.data.products || []);
setIsSearchOpen(true);
} catch (err) {
setSuggestions([]);
}
} else {
setSuggestions([]);
setIsSearchOpen(false);
}
};
const handleSearchSubmit = (e) => {
e.preventDefault();
if (searchQuery.trim()) {
setIsSearchOpen(false);
navigate(`/shop?search=${encodeURIComponent(searchQuery.trim())}`);
}
};
const totalCartCount = cartItems.reduce((acc, item) => acc + item.quantity, 0);
const isActiveRoute = (path) => location.pathname === path;
return (
<header className="sticky top-0 z-40 bg-white/95 backdrop-blur-md border-b border-slate-200/80 shadow-sm">
{/* Top Shipping Announcement Bar */}
<div className="bg-slate-900 text-slate-100 text-xs font-semibold py-2 px-4 text-center tracking-wider flex items-center justify-center gap-2">
<span>🇮🇳 FREE EXPRESS COURIER DELIVERY ACROSS INDIA ON ORDERS OVER ₹5,000</span>
<span className="hidden md:inline text-amber-400">| CODE: <strong>WELCOME10</strong></span>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-20">
{/* Logo & Mobile Menu Toggle */}
<div className="flex items-center space-x-4">
<button
onClick={onOpenMobileMenu}
className="lg:hidden p-2 text-slate-600 hover:text-slate-900 rounded-lg hover:bg-slate-100"
>
<Menu className="w-6 h-6" />
</button>
<Link to="/" className="flex items-center space-x-3 group">
<div className="w-10 h-10 bg-amber-500 rounded-xl flex items-center justify-center font-bold text-white text-xl shadow-md shadow-amber-500/20 group-hover:scale-105 transition-transform">
AG
</div>
<div>
<span className="text-xl font-extrabold tracking-tight text-slate-900 font-serif group-hover:text-amber-600 transition-colors">
ANTIGRAVITY
</span>
<span className="block text-[10px] tracking-widest text-amber-600 font-bold uppercase">
GUITARS INDIA
</span>
</div>
</Link>
</div>
{/* Main Navigation Links */}
<nav className="hidden lg:flex items-center space-x-6 text-sm font-semibold text-slate-700">
<Link
to="/"
className={`transition-colors hover:text-amber-600 ${isActiveRoute('/') ? 'text-amber-600 font-bold border-b-2 border-amber-500 pb-1' : ''}`}
>
Home
</Link>
<Link
to="/shop"
className={`transition-colors hover:text-amber-600 ${isActiveRoute('/shop') ? 'text-amber-600 font-bold border-b-2 border-amber-500 pb-1' : ''}`}
>
Shop All
</Link>
<Link
to="/shop?category=electric-guitars"
className="hover:text-amber-600 transition-colors"
>
Guitars
</Link>
<Link
to="/shop?category=amplifiers"
className="hover:text-amber-600 transition-colors"
>
Accessories
</Link>
<Link
to="/shop?isNewArrival=true"
className="hover:text-amber-600 transition-colors text-amber-600 font-bold"
>
New Arrivals
</Link>
<Link
to="/shop?isBestSeller=true"
className="hover:text-amber-600 transition-colors"
>
Deals
</Link>
<button
onClick={() => dispatch(openGuitarFinder())}
className="flex items-center gap-1.5 text-amber-700 hover:text-amber-800 font-bold px-3 py-1.5 bg-amber-50 border border-amber-200 rounded-full transition-all hover:bg-amber-100"
>
<Compass className="w-4 h-4 text-amber-600" />
Guitar Finder
</button>
<Link to="/about" className="hover:text-amber-600 transition-colors">About</Link>
<Link to="/contact" className="hover:text-amber-600 transition-colors">Contact</Link>
</nav>
{/* Actions: Search, Wishlist, Compare, Cart, Account */}
<div className="flex items-center space-x-4">
{/* Search Input */}
<div className="relative hidden md:block w-56 lg:w-64">
<form onSubmit={handleSearchSubmit}>
<input
type="text"
placeholder="Search guitars, brands..."
value={searchQuery}
onChange={handleSearchChange}
className="w-full bg-slate-100 border border-slate-200 rounded-full py-2 pl-9 pr-4 text-xs text-slate-800 focus:outline-none focus:border-amber-500 focus:bg-white transition-all placeholder:text-slate-400"
/>
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-2.5" />
</form>
{/* Suggestions Dropdown */}
{isSearchOpen && suggestions.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-2 bg-white border border-slate-200 rounded-2xl shadow-xl overflow-hidden z-50">
{suggestions.map((p) => (
<Link
key={p._id}
to={`/product/${p.slug}`}
onClick={() => setIsSearchOpen(false)}
className="flex items-center gap-3 p-3 hover:bg-slate-50 transition-colors border-b border-slate-100 last:border-0"
>
<img src={p.images[0]} alt={p.name} className="w-10 h-10 object-cover rounded-lg" />
<div>
<div className="text-xs font-bold text-slate-900 truncate max-w-[170px]">{p.name}</div>
<div className="text-xs text-amber-600 font-extrabold">₹{(p.salePrice || p.price).toLocaleString('en-IN')}</div>
</div>
</Link>
))}
</div>
)}
</div>
{/* Compare Icon */}
<Link to="/compare" className="relative p-2 text-slate-700 hover:text-amber-600 transition-colors" title="Compare Guitars">
<Sliders className="w-5 h-5" />
{compareItems.length > 0 && (
<span className="absolute -top-1 -right-1 bg-amber-500 text-white font-bold text-[10px] w-4 h-4 rounded-full flex items-center justify-center">
{compareItems.length}
</span>
)}
</Link>
{/* Wishlist Icon */}
<Link to="/wishlist" className="relative p-2 text-slate-700 hover:text-rose-600 transition-colors" title="Wishlist">
<Heart className="w-5 h-5" />
{wishlistProducts.length > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white font-bold text-[10px] w-4 h-4 rounded-full flex items-center justify-center">
{wishlistProducts.length}
</span>
)}
</Link>
{/* Cart Icon */}
<Link to="/cart" className="relative p-2 text-slate-700 hover:text-amber-600 transition-colors" title="Shopping Cart">
<ShoppingBag className="w-5 h-5" />
{totalCartCount > 0 && (
<span className="absolute -top-1 -right-1 bg-amber-500 text-white font-bold text-[10px] w-4 h-4 rounded-full flex items-center justify-center shadow-sm">
{totalCartCount}
</span>
)}
</Link>
{/* Fixed User Profile Dropdown (Requirement 18) */}
{userInfo ? (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setProfileDropdownOpen(!profileDropdownOpen)}
className="flex items-center gap-2 p-1 bg-slate-100 border border-slate-200 rounded-full pr-3 hover:border-amber-500 transition-all"
>
<img src={userInfo.avatar} alt={userInfo.name} className="w-7 h-7 rounded-full object-cover" />
<span className="text-xs font-bold text-slate-800 truncate max-w-[80px]">{userInfo.name}</span>
<ChevronDown className="w-3.5 h-3.5 text-slate-500" />
</button>
{profileDropdownOpen && (
<div className="absolute right-0 top-full mt-2 w-52 bg-white border border-slate-200 rounded-2xl shadow-xl py-2 z-50 animate-fadeIn">
<div className="px-4 py-2 border-b border-slate-100">
<div className="text-xs font-bold text-slate-900">{userInfo.name}</div>
<div className="text-[11px] text-slate-500 truncate">{userInfo.email}</div>
</div>
{userInfo.role === 'Admin' && (
<Link
to="/admin"
onClick={() => setProfileDropdownOpen(false)}
className="flex items-center gap-2 px-4 py-2 text-xs font-bold text-amber-600 hover:bg-slate-50"
>
<Shield className="w-4 h-4" /> Admin Dashboard
</Link>
)}
<Link
to="/dashboard"
onClick={() => setProfileDropdownOpen(false)}
className="flex items-center gap-2 px-4 py-2 text-xs font-medium text-slate-700 hover:bg-slate-50"
>
<User className="w-4 h-4" /> My Account & Orders
</Link>
<button
onClick={() => {
setProfileDropdownOpen(false);
dispatch(logout());
}}
className="w-full text-left flex items-center gap-2 px-4 py-2 text-xs text-rose-600 hover:bg-slate-50 border-t border-slate-100 mt-1 font-semibold"
>
<LogOut className="w-4 h-4" /> Logout
</button>
</div>
)}
</div>
) : (
<Link
to="/login"
className="text-xs font-bold text-white bg-amber-500 hover:bg-amber-600 px-4 py-2 rounded-full transition-all shadow-sm"
>
Sign In
</Link>
)}
</div>
</div>
</div>
</header>
);
};
export default Navbar;

View File

@@ -0,0 +1,175 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { Heart, Eye, Sliders, ShoppingBag, Star } from 'lucide-react';
import { addToCartAsync } from '../store/slices/cartSlice';
import { toggleWishlistAsync } from '../store/slices/wishlistSlice';
import { addToCompare, removeFromCompare } from '../store/slices/compareSlice';
import { openQuickView, showToast } from '../store/slices/uiSlice';
import { formatINR } from '../utils/formatCurrency';
const ProductCard = ({ product }) => {
const dispatch = useDispatch();
const { products: wishlistProducts } = useSelector((state) => state.wishlist);
const { items: compareItems } = useSelector((state) => state.compare);
const { userInfo } = useSelector((state) => state.auth);
const isWishlisted = wishlistProducts.some((p) => (p._id || p) === product._id);
const isCompared = compareItems.some((p) => p._id === product._id);
const handleWishlist = (e) => {
e.preventDefault();
if (!userInfo) {
dispatch(showToast({ message: 'Please login to save to wishlist', type: 'error' }));
return;
}
dispatch(toggleWishlistAsync(product._id));
dispatch(showToast({ message: isWishlisted ? 'Removed from wishlist' : 'Added to wishlist', type: 'success' }));
};
const handleCompare = (e) => {
e.preventDefault();
if (isCompared) {
dispatch(removeFromCompare(product._id));
dispatch(showToast({ message: 'Removed from compare list', type: 'info' }));
} else {
if (compareItems.length >= 4) {
dispatch(showToast({ message: 'Max 4 items allowed for comparison', type: 'error' }));
return;
}
dispatch(addToCompare(product));
dispatch(showToast({ message: 'Added to compare list', type: 'success' }));
}
};
const handleAddToCart = (e) => {
e.preventDefault();
if (product.stock <= 0) return;
dispatch(addToCartAsync({ productId: product._id, quantity: 1 }));
dispatch(showToast({ message: `${product.name} added to cart!`, type: 'success' }));
};
const displayPrice = product.salePrice > 0 ? product.salePrice : product.price;
const hasDiscount = product.salePrice > 0 && product.salePrice < product.price;
const discountPercent = hasDiscount
? Math.round(((product.price - product.salePrice) / product.price) * 100)
: 0;
return (
<div className="group relative bg-white border border-slate-200/90 rounded-2xl overflow-hidden hover:border-amber-500/50 transition-all duration-300 flex flex-col justify-between hover:shadow-lg shadow-sm">
{/* Product Image & Hover Overlay */}
<div className="relative aspect-[4/3] overflow-hidden bg-slate-100">
<Link to={`/product/${product.slug}`}>
<img
src={product.images?.[0] || 'https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?w=600'}
alt={product.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
{product.images?.[1] && (
<img
src={product.images[1]}
alt=""
className="absolute inset-0 w-full h-full object-cover opacity-0 group-hover:opacity-100 transition-opacity duration-500"
/>
)}
</Link>
{/* Badges (Requirement 10) */}
<div className="absolute top-3 left-3 flex flex-col gap-1.5 z-10">
{hasDiscount && (
<span className="bg-amber-600 text-white font-bold text-[10px] uppercase px-2.5 py-0.5 rounded-full shadow-sm">
-{discountPercent}% OFF
</span>
)}
{product.isNewArrival && (
<span className="bg-slate-900 text-white font-bold text-[10px] uppercase px-2.5 py-0.5 rounded-full shadow-sm">
NEW
</span>
)}
{product.isBestSeller && (
<span className="bg-amber-500 text-white font-bold text-[10px] uppercase px-2.5 py-0.5 rounded-full shadow-sm">
BEST SELLER
</span>
)}
</div>
{/* Quick Action Floating Buttons */}
<div className="absolute top-3 right-3 flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-10">
<button
onClick={handleWishlist}
className={`p-2 rounded-full backdrop-blur-md border shadow-sm transition-all ${
isWishlisted
? 'bg-rose-500 border-rose-500 text-white'
: 'bg-white/90 border-slate-200 text-slate-600 hover:text-rose-600 hover:bg-white'
}`}
title="Wishlist"
>
<Heart className="w-4 h-4" fill={isWishlisted ? 'currentColor' : 'none'} />
</button>
<button
onClick={handleCompare}
className={`p-2 rounded-full backdrop-blur-md border shadow-sm transition-all ${
isCompared
? 'bg-amber-500 border-amber-500 text-white'
: 'bg-white/90 border-slate-200 text-slate-600 hover:text-amber-600 hover:bg-white'
}`}
title="Compare Specs"
>
<Sliders className="w-4 h-4" />
</button>
<button
onClick={() => dispatch(openQuickView(product))}
className="p-2 bg-white/90 border border-slate-200 text-slate-600 hover:text-slate-900 rounded-full backdrop-blur-md transition-all shadow-sm"
title="Quick View"
>
<Eye className="w-4 h-4" />
</button>
</div>
</div>
{/* Content */}
<div className="p-5 flex-1 flex flex-col justify-between space-y-3">
<div>
<div className="flex items-center justify-between text-xs text-slate-500 mb-1">
<span className="font-bold text-amber-600">{product.brand?.name || 'Custom Shop'}</span>
<div className="flex items-center gap-1 text-amber-500 font-bold">
<Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400" />
<span>{product.rating || '4.9'}</span>
</div>
</div>
<Link to={`/product/${product.slug}`}>
<h3 className="text-sm font-bold text-slate-900 line-clamp-2 hover:text-amber-600 transition-colors leading-snug">
{product.name}
</h3>
</Link>
</div>
<div>
{/* Requirement 19: ₹ INR Currency Format & Requirement 16: Hidden SKU */}
<div className="flex items-baseline gap-2 mb-3">
<span className="text-lg font-extrabold text-slate-900">{formatINR(displayPrice)}</span>
{hasDiscount && (
<span className="text-xs text-slate-400 line-through">{formatINR(product.price)}</span>
)}
</div>
<button
onClick={handleAddToCart}
disabled={product.stock <= 0}
className={`w-full py-2.5 px-4 rounded-xl text-xs font-bold flex items-center justify-center gap-2 transition-all shadow-sm ${
product.stock > 0
? 'bg-slate-900 hover:bg-amber-600 text-white border border-slate-900'
: 'bg-slate-100 text-slate-400 border border-slate-200 cursor-not-allowed'
}`}
>
<ShoppingBag className="w-4 h-4" />
{product.stock > 0 ? 'Add To Cart' : 'Out Of Stock'}
</button>
</div>
</div>
</div>
);
};
export default ProductCard;

View File

@@ -0,0 +1,110 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { X, Star, ShoppingBag, Check } from 'lucide-react';
import { closeQuickView, showToast } from '../store/slices/uiSlice';
import { addToCartAsync } from '../store/slices/cartSlice';
import { formatINR } from '../utils/formatCurrency';
const ProductQuickView = () => {
const dispatch = useDispatch();
const { quickViewProduct } = useSelector((state) => state.ui);
const [selectedImage, setSelectedImage] = useState(0);
if (!quickViewProduct) return null;
const handleAddToCart = () => {
dispatch(addToCartAsync({ productId: quickViewProduct._id, quantity: 1 }));
dispatch(showToast({ message: `${quickViewProduct.name} added to cart`, type: 'success' }));
dispatch(closeQuickView());
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => dispatch(closeQuickView())} />
<div className="relative w-full max-w-4xl bg-white border border-slate-200 rounded-3xl overflow-hidden shadow-2xl z-10 grid grid-cols-1 md:grid-cols-2 text-slate-900">
<button
onClick={() => dispatch(closeQuickView())}
className="absolute top-4 right-4 z-20 p-2 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded-full"
>
<X className="w-5 h-5" />
</button>
{/* Gallery */}
<div className="p-6 bg-slate-50 flex flex-col justify-between border-r border-slate-100">
<div className="aspect-[4/3] rounded-2xl overflow-hidden mb-4 border border-slate-200 bg-white">
<img
src={quickViewProduct.images?.[selectedImage] || quickViewProduct.images?.[0]}
alt={quickViewProduct.name}
className="w-full h-full object-cover"
/>
</div>
{quickViewProduct.images?.length > 1 && (
<div className="flex gap-2">
{quickViewProduct.images.map((img, idx) => (
<button
key={idx}
onClick={() => setSelectedImage(idx)}
className={`w-16 h-16 rounded-xl overflow-hidden border-2 ${
selectedImage === idx ? 'border-amber-500 shadow-sm' : 'border-slate-200 opacity-70'
}`}
>
<img src={img} alt="" className="w-full h-full object-cover" />
</button>
))}
</div>
)}
</div>
{/* Info */}
<div className="p-6 flex flex-col justify-between text-left space-y-4">
<div>
<div className="text-xs font-bold text-amber-600 uppercase tracking-wider mb-1">
{quickViewProduct.brand?.name}
</div>
<h2 className="text-xl font-bold text-slate-900 font-serif mb-2">{quickViewProduct.name}</h2>
<div className="flex items-center gap-2 mb-4">
<div className="flex text-amber-400">
{[...Array(5)].map((_, i) => (
<Star key={i} className="w-4 h-4 fill-amber-400" />
))}
</div>
<span className="text-xs text-slate-500">({quickViewProduct.numReviews} Reviews)</span>
</div>
<div className="text-2xl font-extrabold text-slate-900 mb-4">
{formatINR(quickViewProduct.salePrice || quickViewProduct.price)}
</div>
<p className="text-xs text-slate-600 line-clamp-3 leading-relaxed mb-6">
{quickViewProduct.description}
</p>
{/* Quick Specs */}
{quickViewProduct.specs && (
<div className="bg-slate-50 p-4 rounded-2xl space-y-1.5 text-xs text-slate-600 border border-slate-200">
<div><strong className="text-slate-900">Body Wood:</strong> {quickViewProduct.specs.bodyMaterial}</div>
<div><strong className="text-slate-900">Neck:</strong> {quickViewProduct.specs.neckMaterial}</div>
<div><strong className="text-slate-900">Pickups:</strong> {quickViewProduct.specs.pickupConfiguration}</div>
</div>
)}
</div>
<div className="space-y-3">
<button
onClick={handleAddToCart}
className="w-full py-3.5 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl flex items-center justify-center gap-2 shadow-sm"
>
<ShoppingBag className="w-5 h-5" /> Add To Shopping Cart
</button>
<div className="flex items-center justify-center gap-2 text-xs text-emerald-700 font-semibold">
<Check className="w-4 h-4" /> Ready to dispatch in 24 Hours
</div>
</div>
</div>
</div>
</div>
);
};
export default ProductQuickView;

View File

@@ -0,0 +1,15 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const ScrollToTop = () => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
};
export default ScrollToTop;

View File

@@ -0,0 +1,21 @@
import React from 'react';
export const ProductSkeleton = () => (
<div className="bg-white border border-slate-200 rounded-2xl p-4 animate-pulse space-y-4 shadow-sm">
<div className="aspect-[4/3] bg-slate-100 rounded-xl" />
<div className="space-y-2">
<div className="h-3 bg-slate-100 rounded w-1/3" />
<div className="h-4 bg-slate-100 rounded w-3/4" />
<div className="h-5 bg-slate-100 rounded w-1/2" />
</div>
<div className="h-10 bg-slate-100 rounded-xl" />
</div>
);
export const ProductGridSkeleton = ({ count = 4 }) => (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{[...Array(count)].map((_, i) => (
<ProductSkeleton key={i} />
))}
</div>
);

View File

@@ -0,0 +1,38 @@
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { CheckCircle2, AlertCircle, Info, X } from 'lucide-react';
import { hideToast } from '../store/slices/uiSlice';
const ToastNotification = () => {
const dispatch = useDispatch();
const { toast } = useSelector((state) => state.ui);
useEffect(() => {
if (toast) {
const timer = setTimeout(() => {
dispatch(hideToast());
}, 4000);
return () => clearTimeout(timer);
}
}, [toast, dispatch]);
if (!toast) return null;
const icons = {
success: <CheckCircle2 className="w-5 h-5 text-emerald-600" />,
error: <AlertCircle className="w-5 h-5 text-rose-600" />,
info: <Info className="w-5 h-5 text-amber-600" />
};
return (
<div className="fixed top-24 right-4 z-50 flex items-center gap-3 bg-white border border-slate-200 p-4 rounded-2xl shadow-xl animate-bounce text-slate-900">
{icons[toast.type || 'info']}
<span className="text-xs font-bold text-slate-900">{toast.message}</span>
<button onClick={() => dispatch(hideToast())} className="p-1 text-slate-400 hover:text-slate-900">
<X className="w-4 h-4" />
</button>
</div>
);
};
export default ToastNotification;

25
client/src/index.css Normal file
View File

@@ -0,0 +1,25 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply bg-slate-50 text-slate-900 overflow-x-hidden antialiased;
}
}
/* Light Mode Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #d97706;
}

23
client/src/main.jsx Normal file
View File

@@ -0,0 +1,23 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { store } from './store';
import App from './App';
import './index.css';
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
</Provider>
</React.StrictMode>
);

View File

@@ -0,0 +1,76 @@
import React, { useState, useEffect } from 'react';
import { Image, Trash2 } from 'lucide-react';
import api from '../../api/axios';
const AdminBanners = () => {
const [banners, setBanners] = useState([]);
const [title, setTitle] = useState('');
const [subtitle, setSubtitle] = useState('');
const [desktopImage, setDesktopImage] = useState('https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=1600');
const fetchBanners = () => api.get('/banners').then(res => setBanners(res.data.data));
useEffect(() => { fetchBanners(); }, []);
const handleAdd = async (e) => {
e.preventDefault();
await api.post('/banners', { title, subtitle, desktopImage, buttonText: 'Shop Guitars', buttonLink: '/shop' });
setTitle(''); setSubtitle('');
fetchBanners();
};
const handleDelete = async (id) => {
await api.delete(`/banners/${id}`);
fetchBanners();
};
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold text-slate-900 font-serif">Hero Slider Banners</h1>
<form onSubmit={handleAdd} className="p-6 bg-white border border-slate-200 shadow-sm rounded-3xl space-y-4 max-w-lg text-xs">
<h3 className="font-bold text-slate-800 uppercase tracking-wider">Add Hero Banner</h3>
<input
type="text"
required
placeholder="Banner Headline Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
<input
type="text"
placeholder="Subtitle"
value={subtitle}
onChange={(e) => setSubtitle(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
<input
type="text"
placeholder="Desktop Image URL"
value={desktopImage}
onChange={(e) => setDesktopImage(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
<button type="submit" className="py-2.5 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl shadow-md shadow-amber-500/20">
Save Hero Banner
</button>
</form>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{banners.map((b) => (
<div key={b._id} className="p-4 bg-white border border-slate-200 shadow-sm rounded-2xl space-y-2 relative">
<img src={b.desktopImage} alt="" className="w-full h-32 object-cover rounded-xl" />
<h4 className="font-bold text-slate-900 text-sm">{b.title}</h4>
<p className="text-xs text-slate-500">{b.subtitle}</p>
<button onClick={() => handleDelete(b._id)} className="absolute top-6 right-6 p-2 bg-rose-500 hover:bg-rose-600 text-white rounded-lg transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
);
};
export default AdminBanners;

View File

@@ -0,0 +1,58 @@
import React, { useState, useEffect } from 'react';
import { Plus, Trash2 } from 'lucide-react';
import api from '../../api/axios';
const AdminBrands = () => {
const [brands, setBrands] = useState([]);
const [name, setName] = useState('');
const fetchBrands = () => api.get('/brands').then(res => setBrands(res.data.data));
useEffect(() => { fetchBrands(); }, []);
const handleAdd = async (e) => {
e.preventDefault();
await api.post('/brands', { name, slug: name.toLowerCase().replace(/\s+/g, '-') });
setName('');
fetchBrands();
};
const handleDelete = async (id) => {
await api.delete(`/brands/${id}`);
fetchBrands();
};
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold text-slate-900 font-serif">Guitar Brands Management</h1>
<form onSubmit={handleAdd} className="p-6 bg-white border border-slate-200 shadow-sm rounded-3xl space-y-4 max-w-md text-xs">
<h3 className="font-bold text-slate-800 uppercase tracking-wider">Add New Manufacturer</h3>
<input
type="text"
required
placeholder="Brand Name (e.g. Suhr Guitars)"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
<button type="submit" className="py-2.5 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl shadow-md shadow-amber-500/20">
Add Brand
</button>
</form>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{brands.map((b) => (
<div key={b._id} className="p-4 bg-white border border-slate-200 shadow-sm rounded-2xl flex items-center justify-between">
<span className="font-bold text-slate-900 text-sm">{b.name}</span>
<button onClick={() => handleDelete(b._id)} className="p-2 text-rose-500 hover:bg-rose-50 rounded-lg transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
);
};
export default AdminBrands;

View File

@@ -0,0 +1,143 @@
import React, { useState, useEffect } from 'react';
import { Plus, Trash2 } from 'lucide-react';
import api from '../../api/axios';
const AdminCategories = () => {
const [categories, setCategories] = useState([]);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [image, setImage] = useState('');
const [homeImage, setHomeImage] = useState('');
const [displayOrder, setDisplayOrder] = useState(1);
const [isFeaturedHome, setIsFeaturedHome] = useState(true);
const fetchCats = () => api.get('/categories').then(res => setCategories(res.data.data));
useEffect(() => { fetchCats(); }, []);
const handleAdd = async (e) => {
e.preventDefault();
await api.post('/categories', {
name,
slug: name.toLowerCase().replace(/\s+/g, '-'),
description,
image,
homeImage,
displayOrder: Number(displayOrder),
isFeaturedHome
});
setName(''); setDescription(''); setImage(''); setHomeImage('');
fetchCats();
};
const handleDelete = async (id) => {
if (window.confirm('Delete category?')) {
await api.delete(`/categories/${id}`);
fetchCats();
}
};
return (
<div className="space-y-6 text-slate-900">
<h1 className="text-2xl font-bold text-slate-900 font-serif">Category Management (Homepage 4 Categories)</h1>
<form onSubmit={handleAdd} className="p-6 bg-white border border-slate-200 rounded-3xl space-y-4 max-w-xl text-xs shadow-sm">
<h3 className="font-bold text-slate-900 uppercase">Add New Category</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-slate-700 font-semibold mb-1 block">Category Name</label>
<input
type="text"
required
placeholder="Electric Guitars"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Display Order Priority</label>
<input
type="number"
value={displayOrder}
onChange={(e) => setDisplayOrder(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-slate-900"
/>
</div>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Description</label>
<input
type="text"
placeholder="Short description"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-slate-900"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-slate-700 font-semibold mb-1 block">Category Image URL</label>
<input
type="text"
placeholder="https://..."
value={image}
onChange={(e) => setImage(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Homepage Background Image URL</label>
<input
type="text"
placeholder="https://..."
value={homeImage}
onChange={(e) => setHomeImage(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-slate-900"
/>
</div>
</div>
<label className="flex items-center gap-2 font-semibold cursor-pointer">
<input
type="checkbox"
checked={isFeaturedHome}
onChange={(e) => setIsFeaturedHome(e.target.checked)}
className="rounded text-amber-500"
/>
Show as Featured Category on Home Page (Top 4)
</label>
<button type="submit" className="py-2.5 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl shadow-sm">
Add Category
</button>
</form>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{categories.map((c) => (
<div key={c._id} className="p-4 bg-white border border-slate-200 rounded-2xl flex flex-col justify-between space-y-3 shadow-sm">
<img src={c.homeImage || c.image} alt="" className="w-full h-24 object-cover rounded-xl border border-slate-200" />
<div>
<div className="font-bold text-slate-900 text-sm flex items-center justify-between">
<span>{c.name}</span>
<span className="text-[10px] text-amber-700 bg-amber-50 px-2 py-0.5 rounded font-mono">Order: {c.displayOrder}</span>
</div>
<div className="text-xs text-slate-500 line-clamp-1 mt-0.5">{c.description}</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-100">
<span className="text-[10px] text-slate-400 font-semibold">{c.isFeaturedHome ? 'Featured on Home' : 'Standard'}</span>
<button onClick={() => handleDelete(c._id)} className="p-1 text-rose-600 hover:text-rose-700">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
</div>
);
};
export default AdminCategories;

View File

@@ -0,0 +1,75 @@
import React, { useState, useEffect } from 'react';
import { Tag, Trash2 } from 'lucide-react';
import api from '../../api/axios';
const AdminCoupons = () => {
const [coupons, setCoupons] = useState([]);
const [code, setCode] = useState('');
const [discountValue, setDiscountValue] = useState(10);
const fetchCoupons = () => api.get('/coupons').then(res => setCoupons(res.data.data));
useEffect(() => { fetchCoupons(); }, []);
const handleAdd = async (e) => {
e.preventDefault();
await api.post('/coupons', {
code: code.toUpperCase(),
discountType: 'percentage',
discountValue: Number(discountValue),
endDate: new Date('2028-12-31')
});
setCode('');
fetchCoupons();
};
const handleDelete = async (id) => {
await api.delete(`/coupons/${id}`);
fetchCoupons();
};
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold text-slate-900 font-serif">Coupons & Promo Codes</h1>
<form onSubmit={handleAdd} className="p-6 bg-white border border-slate-200 shadow-sm rounded-3xl space-y-4 max-w-md text-xs">
<h3 className="font-bold text-slate-800 uppercase tracking-wider">Create Promo Code</h3>
<input
type="text"
required
placeholder="Code (e.g. TONE2026)"
value={code}
onChange={(e) => setCode(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 uppercase font-mono focus:outline-none focus:border-amber-500"
/>
<input
type="number"
required
placeholder="Discount Percentage (%)"
value={discountValue}
onChange={(e) => setDiscountValue(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
<button type="submit" className="py-2.5 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl shadow-md shadow-amber-500/20">
Create Coupon
</button>
</form>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs">
{coupons.map((c) => (
<div key={c._id} className="p-4 bg-white border border-slate-200 shadow-sm rounded-2xl flex items-center justify-between">
<div>
<div className="font-bold text-amber-700 font-mono text-sm">{c.code}</div>
<div className="text-slate-500">{c.discountValue}% OFF Discount</div>
</div>
<button onClick={() => handleDelete(c._id)} className="p-2 text-rose-500 hover:bg-rose-50 rounded-lg transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
);
};
export default AdminCoupons;

View File

@@ -0,0 +1,157 @@
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { IndianRupee, ShoppingBag, Users, AlertTriangle, ArrowRight } from 'lucide-react';
import { formatINR } from '../../utils/formatCurrency';
import api from '../../api/axios';
const AdminDashboard = () => {
const [stats, setStats] = useState({
totalSales: 0,
totalOrders: 0,
totalProducts: 0,
lowStockCount: 0,
recentOrders: [],
lowStockItems: []
});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchDashboardStats = async () => {
try {
setLoading(true);
const [ordersRes, overviewRes] = await Promise.all([
api.get('/orders/admin/all'),
api.get('/inventory/overview')
]);
const allOrders = ordersRes.data.data || [];
const overview = overviewRes.data.data || {};
const totalRev = allOrders.reduce((acc, ord) => acc + (ord.totalAmount || 0), 0);
setStats({
totalSales: totalRev,
totalOrders: allOrders.length,
totalProducts: overview.totalProducts || 0,
lowStockCount: overview.lowStockCount || 0,
recentOrders: allOrders.slice(0, 5),
lowStockItems: overview.lowStock || []
});
} catch (err) {
console.error('Failed to fetch admin stats', err);
} finally {
setLoading(false);
}
};
fetchDashboardStats();
}, []);
if (loading) return <div className="text-slate-500 text-xs">Loading Store Dashboard...</div>;
return (
<div className="space-y-8 text-slate-900">
<div>
<h1 className="text-2xl font-bold text-slate-900 font-serif">Executive Store Overview (India)</h1>
<p className="text-xs text-slate-500">Live sales metrics, inventory stock status, and fulfillment updates.</p>
</div>
{/* Metric Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-2 shadow-sm">
<div className="flex items-center justify-between text-xs text-slate-500 font-semibold">
<span>Total Sales Revenue</span>
<IndianRupee className="w-5 h-5 text-amber-600" />
</div>
<div className="text-3xl font-extrabold text-slate-900">{formatINR(stats.totalSales)}</div>
</div>
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-2 shadow-sm">
<div className="flex items-center justify-between text-xs text-slate-500 font-semibold">
<span>Total Orders Placed</span>
<ShoppingBag className="w-5 h-5 text-emerald-600" />
</div>
<div className="text-3xl font-extrabold text-slate-900">{stats.totalOrders}</div>
</div>
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-2 shadow-sm">
<div className="flex items-center justify-between text-xs text-slate-500 font-semibold">
<span>Catalog Guitars</span>
<Users className="w-5 h-5 text-sky-600" />
</div>
<div className="text-3xl font-extrabold text-slate-900">{stats.totalProducts}</div>
</div>
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-2 shadow-sm">
<div className="flex items-center justify-between text-xs text-slate-500 font-semibold">
<span>Low Stock Alerts</span>
<AlertTriangle className="w-5 h-5 text-rose-600" />
</div>
<div className="text-3xl font-extrabold text-rose-600">{stats.lowStockCount}</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Recent Orders Table */}
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-4 shadow-sm">
<div className="flex items-center justify-between">
<h3 className="text-base font-bold text-slate-900 font-serif">Recent Store Orders</h3>
<Link to="/admin/orders" className="text-xs text-amber-600 font-bold hover:underline flex items-center gap-1">
View All <ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
<div className="space-y-3 text-xs">
{stats.recentOrders.map((ord) => (
<div key={ord._id} className="p-3.5 bg-slate-50 border border-slate-200 rounded-2xl flex items-center justify-between">
<div>
<div className="font-bold text-amber-700 font-mono">{ord.orderNumber}</div>
<div className="text-slate-600">{ord.user?.name || 'Customer'}</div>
</div>
<div className="text-right">
<div className="font-bold text-slate-900">{formatINR(ord.totalAmount)}</div>
<span className="text-[10px] text-emerald-700 font-bold">{ord.orderStatus}</span>
</div>
</div>
))}
</div>
</div>
{/* Low Stock Alerts */}
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-4 shadow-sm">
<div className="flex items-center justify-between">
<h3 className="text-base font-bold text-slate-900 font-serif">Low Stock Inventory Alerts</h3>
<Link to="/admin/inventory" className="text-xs text-amber-600 font-bold hover:underline flex items-center gap-1">
Manage Stock <ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
<div className="space-y-3 text-xs">
{stats.lowStockItems.length === 0 ? (
<div className="text-slate-400 py-4 text-center">All guitar stock levels are optimal.</div>
) : (
stats.lowStockItems.map((item) => (
<div key={item._id} className="p-3 bg-slate-50 border border-slate-200 rounded-2xl flex items-center justify-between">
<div className="flex items-center gap-3">
<img src={item.images?.[0]} alt="" className="w-10 h-10 object-cover rounded-lg border border-slate-200" />
<div>
<div className="font-bold text-slate-900 line-clamp-1">{item.name}</div>
<div className="text-slate-400 font-mono text-[11px]">SKU: {item.sku}</div>
</div>
</div>
<div className="text-rose-700 font-bold px-3 py-1 bg-rose-50 border border-rose-200 rounded-full">
{item.stock} Units
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
);
};
export default AdminDashboard;

View File

@@ -0,0 +1,35 @@
import React, { useState, useEffect } from 'react';
import { Mail } from 'lucide-react';
import api from '../../api/axios';
const AdminEmailTemplates = () => {
const [templates, setTemplates] = useState([]);
useEffect(() => {
api.get('/email-templates').then(res => setTemplates(res.data.data));
}, []);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-slate-900 font-serif">Transactional Email Templates</h1>
<p className="text-xs text-slate-500">Preview transactional order confirmation and invoice email formats.</p>
</div>
<div className="space-y-4">
{templates.map((tpl) => (
<div key={tpl._id} className="p-6 bg-white border border-slate-200 shadow-sm rounded-3xl space-y-2 text-xs">
<h3 className="font-bold text-amber-700 text-sm">{tpl.name}</h3>
<div className="text-slate-700"><strong>Subject:</strong> {tpl.subject}</div>
<div className="text-slate-500 font-mono text-[11px]">Placeholders: {tpl.placeholders?.join(', ')}</div>
<div className="p-3.5 bg-slate-50 rounded-xl border border-slate-200 text-slate-600 font-mono line-clamp-3">
{tpl.bodyHtml}
</div>
</div>
))}
</div>
</div>
);
};
export default AdminEmailTemplates;

View File

@@ -0,0 +1,161 @@
import React, { useState, useEffect } from 'react';
import { Layers, RotateCcw, AlertTriangle, CheckCircle2 } from 'lucide-react';
import { useDispatch } from 'react-redux';
import { showToast } from '../../store/slices/uiSlice';
import api from '../../api/axios';
const AdminInventory = () => {
const dispatch = useDispatch();
const [overview, setOverview] = useState({ products: [], lowStock: [], outOfStock: [] });
const [history, setHistory] = useState([]);
const [selectedProduct, setSelectedProduct] = useState('');
const [newStock, setNewStock] = useState(10);
const [changeType, setChangeType] = useState('Restock');
const [reason, setReason] = useState('');
const [submitting, setSubmitting] = useState(false);
const loadData = async () => {
try {
const [ovRes, histRes] = await Promise.all([
api.get('/inventory/overview'),
api.get('/inventory/history')
]);
setOverview(ovRes.data.data);
setHistory(histRes.data.data);
} catch (err) {
console.error(err);
}
};
useEffect(() => {
loadData();
}, []);
const handleAdjustStock = async (e) => {
e.preventDefault();
if (!selectedProduct) return;
try {
setSubmitting(true);
await api.post('/inventory/adjust', {
productId: selectedProduct,
newStock: Number(newStock),
changeType,
reason
});
dispatch(showToast({ message: 'Stock level updated and audit logged!', type: 'success' }));
setReason('');
loadData();
} catch (err) {
dispatch(showToast({ message: 'Failed to update stock', type: 'error' }));
} finally {
setSubmitting(false);
}
};
return (
<div className="space-y-8">
<div>
<h1 className="text-2xl font-bold text-slate-900 font-serif">Stock Audit & Inventory History</h1>
<p className="text-xs text-slate-500">Track all stock changes, perform manual restocks, and inspect change reasons.</p>
</div>
{/* Stock Adjustment Form */}
<form onSubmit={handleAdjustStock} className="p-6 bg-white border border-slate-200 shadow-sm rounded-3xl space-y-4 max-w-2xl">
<h3 className="text-xs font-bold text-slate-800 uppercase tracking-wider">Manual Stock Level Adjustment</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
<div>
<label className="text-slate-600 font-semibold mb-1 block">Select Product</label>
<select
required
value={selectedProduct}
onChange={(e) => setSelectedProduct(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
>
<option value="">Select Guitar</option>
{overview.products?.map((p) => (
<option key={p._id} value={p._id}>{p.name} (Current: {p.stock})</option>
))}
</select>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">New Stock Level</label>
<input
type="number"
required
value={newStock}
onChange={(e) => setNewStock(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Change Type</label>
<select
value={changeType}
onChange={(e) => setChangeType(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
>
<option value="Restock">Restock</option>
<option value="Manual Adjustment">Manual Adjustment</option>
<option value="Order Cancelled">Order Cancelled</option>
<option value="Returned Product">Returned Product</option>
</select>
</div>
</div>
<div className="text-xs">
<label className="text-slate-600 font-semibold mb-1 block">Adjustment Reason / Luthier Note</label>
<input
type="text"
placeholder="e.g. Batch #409 Restocked from Custom Shop Factory"
value={reason}
onChange={(e) => setReason(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<button
type="submit"
disabled={submitting}
className="py-2.5 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs shadow-md shadow-amber-500/20"
>
Submit Stock Audit Change
</button>
</form>
{/* History Audit Log */}
<div className="bg-white border border-slate-200 shadow-sm rounded-3xl p-6 space-y-4">
<h3 className="text-xs font-bold text-slate-800 uppercase tracking-wider">Inventory Change History Audit Log</h3>
<div className="overflow-x-auto">
<table className="w-full text-left text-xs">
<thead className="bg-slate-50 border-b border-slate-200 text-slate-500 font-semibold">
<tr>
<th className="p-3">Product</th>
<th className="p-3">Change Type</th>
<th className="p-3">Qty Changed</th>
<th className="p-3">Prev / New</th>
<th className="p-3">Reason</th>
<th className="p-3">Date</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{history.map((log) => (
<tr key={log._id}>
<td className="p-3 font-bold text-slate-900">{log.product?.name || log.sku}</td>
<td className="p-3"><span className="px-2 py-0.5 bg-amber-50 border border-amber-200 text-amber-700 font-semibold rounded">{log.changeType}</span></td>
<td className="p-3 font-bold text-slate-900">{log.quantityChanged > 0 ? `+${log.quantityChanged}` : log.quantityChanged}</td>
<td className="p-3 text-slate-500">{log.previousStock} → {log.newStock}</td>
<td className="p-3 text-slate-600">{log.reason}</td>
<td className="p-3 text-slate-400">{new Date(log.createdAt).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
};
export default AdminInventory;

View File

@@ -0,0 +1,129 @@
import React, { useState } from 'react';
import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { LayoutDashboard, ShoppingBag, Layers, Tag, Image, Settings, MessageSquare, Mail, Shield, ExternalLink, Menu, X, LogOut } from 'lucide-react';
import { logout } from '../../store/slices/authSlice';
import { showToast } from '../../store/slices/uiSlice';
const AdminLayout = () => {
const location = useLocation();
const navigate = useNavigate();
const dispatch = useDispatch();
const { userInfo } = useSelector((state) => state.auth);
const [sidebarOpen, setSidebarOpen] = useState(false);
const handleAdminLogout = () => {
dispatch(logout());
dispatch(showToast({ message: 'Admin logged out', type: 'info' }));
navigate('/admin/login');
};
const navLinks = [
{ name: 'Dashboard', path: '/admin', icon: LayoutDashboard },
{ name: 'Products Catalog', path: '/admin/products', icon: ShoppingBag },
{ name: 'Inventory & Audit', path: '/admin/inventory', icon: Layers },
{ name: 'Orders Fulfillment', path: '/admin/orders', icon: Tag },
{ name: 'Categories', path: '/admin/categories', icon: Layers },
{ name: 'Guitar Brands', path: '/admin/brands', icon: Shield },
{ name: 'Coupons & Promos', path: '/admin/coupons', icon: Tag },
{ name: 'Hero Banners', path: '/admin/banners', icon: Image },
{ name: 'Contact Inquiries', path: '/admin/messages', icon: MessageSquare },
{ name: 'Email Templates', path: '/admin/email-templates', icon: Mail },
{ name: 'Website Settings', path: '/admin/settings', icon: Settings }
];
return (
<div className="min-h-screen bg-slate-100 text-slate-900 flex font-sans">
{/* Sidebar (Light Theme) */}
<aside className={`fixed inset-y-0 left-0 z-50 w-64 bg-white border-r border-slate-200 p-4 flex flex-col justify-between shadow-sm transition-transform duration-300 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}`}>
<div className="space-y-6">
<div className="flex items-center justify-between border-b border-slate-100 pb-4">
<div className="flex items-center space-x-2.5">
<div className="w-9 h-9 bg-amber-500 rounded-xl flex items-center justify-center font-bold text-white shadow-sm">AG</div>
<div>
<span className="font-serif font-bold text-sm text-slate-900 block">ADMIN PORTAL</span>
<span className="text-[10px] text-amber-600 font-bold uppercase">ANTIGRAVITY INDIA</span>
</div>
</div>
<button onClick={() => setSidebarOpen(false)} className="lg:hidden p-1 text-slate-400">
<X className="w-5 h-5" />
</button>
</div>
<nav className="space-y-1">
{navLinks.map((link) => {
const Icon = link.icon;
const isActive = location.pathname === link.path;
return (
<Link
key={link.path}
to={link.path}
onClick={() => setSidebarOpen(false)}
className={`flex items-center gap-3 px-3.5 py-2.5 rounded-xl text-xs font-semibold transition-all ${
isActive
? 'bg-amber-500 text-white font-bold shadow-sm'
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'
}`}
>
<Icon className="w-4 h-4" />
{link.name}
</Link>
);
})}
</nav>
</div>
{/* Requirement 14: Clear Admin Logout & Preview Storefront */}
<div className="pt-4 border-t border-slate-100 space-y-2">
<Link
to="/"
target="_blank"
className="flex items-center justify-between p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-bold text-amber-700 hover:bg-slate-100 transition-colors"
>
<span>Preview Storefront</span>
<ExternalLink className="w-4 h-4" />
</Link>
<button
onClick={handleAdminLogout}
className="w-full flex items-center justify-between p-2.5 bg-rose-50 border border-rose-100 rounded-xl text-xs font-bold text-rose-600 hover:bg-rose-100 transition-colors"
>
<span>Sign Out Admin</span>
<LogOut className="w-4 h-4" />
</button>
</div>
</aside>
{/* Main Content Area */}
<div className="flex-1 lg:ml-64 flex flex-col min-w-0">
{/* Topbar */}
<header className="h-16 border-b border-slate-200 bg-white/80 backdrop-blur-md px-6 flex items-center justify-between shadow-sm">
<button onClick={() => setSidebarOpen(true)} className="lg:hidden p-2 text-slate-600">
<Menu className="w-6 h-6" />
</button>
<div className="text-xs font-bold text-slate-700">Antigravity Store Administration (India)</div>
<div className="flex items-center gap-3">
<span className="text-xs font-bold text-slate-800">{userInfo?.name || 'Admin'}</span>
<button
onClick={handleAdminLogout}
className="text-xs font-bold text-rose-600 hover:underline flex items-center gap-1"
>
<LogOut className="w-3.5 h-3.5" /> Logout
</button>
</div>
</header>
<main className="p-6 md:p-8 flex-1 overflow-y-auto">
<Outlet />
</main>
</div>
</div>
);
};
export default AdminLayout;

View File

@@ -0,0 +1,88 @@
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { Shield, Lock } from 'lucide-react';
import { login } from '../../store/slices/authSlice';
import { showToast } from '../../store/slices/uiSlice';
const AdminLoginPage = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { loading } = useSelector((state) => state.auth);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
dispatch(login({ email, password }))
.unwrap()
.then((user) => {
if (user.role === 'Admin') {
dispatch(showToast({ message: 'Welcome to Admin Portal', type: 'success' }));
navigate('/admin');
} else {
dispatch(showToast({ message: 'Access Denied: Admin privileges required', type: 'error' }));
}
})
.catch((err) => dispatch(showToast({ message: err || 'Invalid admin credentials', type: 'error' })));
};
return (
<div className="min-h-screen bg-slate-100 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-white border border-slate-200 rounded-3xl p-8 shadow-xl space-y-6">
<div className="text-center space-y-2">
<div className="w-14 h-14 bg-amber-500 rounded-2xl flex items-center justify-center font-bold text-white text-2xl mx-auto shadow-md shadow-amber-500/20">
<Shield className="w-8 h-8" />
</div>
<h1 className="text-2xl font-bold text-slate-900 font-serif">Admin Portal Authentication</h1>
<p className="text-xs text-slate-500">Antigravity Guitars Store Administration</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4 text-xs">
<div>
<label className="text-slate-700 font-semibold mb-1 block">Admin Email Address</label>
<input
type="email"
required
placeholder="admin@guitars.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-slate-900 focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Security Password</label>
<input
type="password"
required
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-slate-900 focus:border-amber-500"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3.5 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs shadow-md transition-all flex items-center justify-center gap-2"
>
<Lock className="w-4 h-4" />
{loading ? 'Authenticating Admin...' : 'Sign In To Admin Portal'}
</button>
</form>
<div className="p-4 bg-slate-50 border border-slate-200 rounded-2xl text-[11px] text-slate-600 space-y-1">
<div className="font-bold text-amber-700">Seeded Admin Quick Credentials:</div>
<div>Email: <code className="text-slate-900 font-bold">admin@guitars.com</code></div>
<div>Password: <code className="text-slate-900 font-bold">Admin@123</code></div>
</div>
</div>
</div>
);
};
export default AdminLoginPage;

View File

@@ -0,0 +1,51 @@
import React, { useState, useEffect } from 'react';
import { MessageSquare, CheckCircle } from 'lucide-react';
import api from '../../api/axios';
const AdminMessages = () => {
const [messages, setMessages] = useState([]);
const fetchMsgs = () => api.get('/contact').then(res => setMessages(res.data.data));
useEffect(() => { fetchMsgs(); }, []);
const markResolved = async (id) => {
await api.put(`/contact/${id}/status`, { status: 'Resolved' });
fetchMsgs();
};
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-slate-900 font-serif">Customer Contact Submissions</h1>
<p className="text-xs text-slate-500">Review and resolve customer inquiries sent through the store contact form.</p>
</div>
<div className="space-y-4">
{messages.length === 0 ? (
<div className="p-8 text-center bg-white border border-slate-200 shadow-sm rounded-3xl text-xs text-slate-500">
No contact submissions found.
</div>
) : (
messages.map((m) => (
<div key={m._id} className="p-5 bg-white border border-slate-200 shadow-sm rounded-2xl flex flex-col sm:flex-row items-start justify-between gap-4 text-xs">
<div className="space-y-1">
<div className="font-bold text-slate-900 text-sm">{m.name} ({m.email})</div>
<div className="text-amber-700 font-semibold">{m.subject}</div>
<p className="text-slate-600 mt-1">{m.message}</p>
</div>
<button
onClick={() => markResolved(m._id)}
className="py-1.5 px-3 bg-slate-100 hover:bg-emerald-50 hover:text-emerald-700 text-slate-700 font-bold rounded-lg border border-slate-200 transition-colors whitespace-nowrap"
>
Mark Resolved
</button>
</div>
))
)}
</div>
</div>
);
};
export default AdminMessages;

View File

@@ -0,0 +1,165 @@
import React, { useState, useEffect } from 'react';
import { Edit3, CheckCircle2, Truck, Package } from 'lucide-react';
import { useDispatch } from 'react-redux';
import { showToast } from '../../store/slices/uiSlice';
import api from '../../api/axios';
import { formatINR } from '../../utils/formatCurrency';
const AdminOrders = () => {
const dispatch = useDispatch();
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedOrder, setSelectedOrder] = useState(null);
const [newStatus, setNewStatus] = useState('');
const [trackingNumber, setTrackingNumber] = useState('');
const [courierProvider, setCourierProvider] = useState('BlueDart Express');
const fetchOrders = async () => {
try {
setLoading(true);
const res = await api.get('/orders/admin/all');
setOrders(res.data.data || []);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchOrders();
}, []);
const handleUpdateStatus = async (e) => {
e.preventDefault();
if (!selectedOrder) return;
try {
await api.put(`/orders/${selectedOrder._id}/status`, {
orderStatus: newStatus,
trackingNumber,
courierProvider,
shippingProvider: courierProvider
});
dispatch(showToast({ message: 'Order status & courier tracking updated!', type: 'success' }));
setSelectedOrder(null);
fetchOrders();
} catch (err) {
dispatch(showToast({ message: 'Failed to update order', type: 'error' }));
}
};
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-slate-900 font-serif">Customer Order Management</h1>
<p className="text-xs text-slate-500">Update order fulfillment status, assign Indian courier tracking codes, and update tracking info.</p>
</div>
<div className="bg-white border border-slate-200 shadow-sm rounded-3xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs">
<thead className="bg-slate-50 border-b border-slate-200 text-slate-500 font-semibold uppercase">
<tr>
<th className="p-4">Order ID</th>
<th className="p-4">Customer</th>
<th className="p-4">Total</th>
<th className="p-4">Payment</th>
<th className="p-4">Fulfillment Status</th>
<th className="p-4 text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{orders.map((ord) => (
<tr key={ord._id} className="hover:bg-slate-50/80 transition-colors">
<td className="p-4 font-bold text-amber-700 font-mono">{ord.orderNumber}</td>
<td className="p-4 text-slate-900 font-semibold">{ord.user?.name || ord.shippingAddress?.fullName || 'Guest'}</td>
<td className="p-4 font-bold text-slate-900">{formatINR(ord.totalAmount)}</td>
<td className="p-4 text-slate-500">
<span className="px-2 py-0.5 bg-slate-100 border border-slate-200 rounded font-semibold text-slate-700">{ord.paymentMethod}</span>
</td>
<td className="p-4">
<span className="px-3 py-1 bg-amber-50 border border-amber-200 text-amber-700 rounded-full font-bold">
{ord.orderStatus}
</span>
</td>
<td className="p-4 text-right">
<button
onClick={() => {
setSelectedOrder(ord);
setNewStatus(ord.orderStatus);
setTrackingNumber(ord.trackingNumber || '');
setCourierProvider(ord.courierProvider || ord.shippingProvider || 'BlueDart Express');
}}
className="py-1.5 px-3 bg-slate-900 hover:bg-slate-800 text-white font-bold rounded-lg text-xs"
>
Update Status
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Modal Status Form */}
{selectedOrder && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="fixed inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setSelectedOrder(null)} />
<form onSubmit={handleUpdateStatus} className="relative w-full max-w-md bg-white border border-slate-200 shadow-xl rounded-3xl p-6 space-y-4 z-10 text-xs">
<h3 className="text-base font-bold text-slate-900 font-serif">Fulfill Order #{selectedOrder.orderNumber}</h3>
<div>
<label className="text-slate-600 font-semibold mb-1 block">New Order Status</label>
<select
value={newStatus}
onChange={(e) => setNewStatus(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
>
{['Pending', 'Payment Confirmed', 'Processing', 'Packed', 'Shipped', 'Out for Delivery', 'Delivered', 'Cancelled'].map((st) => (
<option key={st} value={st}>{st}</option>
))}
</select>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Courier / Delivery Partner</label>
<select
value={courierProvider}
onChange={(e) => setCourierProvider(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
>
{['BlueDart Express', 'Delhivery', 'DTDC Express', 'India Post Speed Post', 'Xpressbees', 'Shadowfax', 'FedEx India'].map((cp) => (
<option key={cp} value={cp}>{cp}</option>
))}
</select>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Tracking / AWB Number</label>
<input
type="text"
placeholder="e.g. BLUEDART-8823-IN"
value={trackingNumber}
onChange={(e) => setTrackingNumber(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 font-mono focus:outline-none focus:border-amber-500"
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<button type="button" onClick={() => setSelectedOrder(null)} className="py-2 px-4 bg-slate-100 text-slate-700 rounded-xl font-semibold border border-slate-200">
Cancel
</button>
<button type="submit" className="py-2 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl shadow-md shadow-amber-500/20">
Save & Notify Customer
</button>
</div>
</form>
</div>
)}
</div>
);
};
export default AdminOrders;

View File

@@ -0,0 +1,165 @@
import React, { useState, useEffect } from 'react';
import { Plus, Search, Edit3, Trash2 } from 'lucide-react';
import ProductFormModal from './ProductFormModal';
import { showToast } from '../../store/slices/uiSlice';
import { useDispatch } from 'react-redux';
import { formatINR } from '../../utils/formatCurrency';
import api from '../../api/axios';
const AdminProducts = () => {
const dispatch = useDispatch();
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingProduct, setEditingProduct] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const fetchProducts = async () => {
try {
setLoading(true);
const res = await api.get('/products?limit=50');
setProducts(res.data.data.products || []);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchProducts();
}, []);
const handleSaveProduct = async (formData) => {
try {
if (editingProduct) {
await api.put(`/products/${editingProduct._id}`, formData);
dispatch(showToast({ message: 'Product updated successfully', type: 'success' }));
} else {
await api.post('/products', formData);
dispatch(showToast({ message: 'Product created successfully', type: 'success' }));
}
setIsModalOpen(false);
fetchProducts();
} catch (err) {
dispatch(showToast({ message: err.response?.data?.message || 'Error saving product', type: 'error' }));
}
};
const handleDeleteProduct = async (id) => {
if (window.confirm('Are you sure you want to delete this guitar product?')) {
try {
await api.delete(`/products/${id}`);
dispatch(showToast({ message: 'Product deleted', type: 'info' }));
fetchProducts();
} catch (err) {
dispatch(showToast({ message: 'Delete failed', type: 'error' }));
}
}
};
const filteredProducts = products.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()));
return (
<div className="space-y-6 text-slate-900">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-slate-900 font-serif">Guitar & Gear Catalog Management</h1>
<p className="text-xs text-slate-500">Add, update, or edit guitar specifications, pricing, and inventory stock.</p>
</div>
<button
onClick={() => { setEditingProduct(null); setIsModalOpen(true); }}
className="py-2.5 px-5 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs flex items-center gap-2 shadow-sm"
>
<Plus className="w-4 h-4" /> Add New Guitar
</button>
</div>
{/* Search Filter */}
<div className="relative max-w-md">
<input
type="text"
placeholder="Search by name, SKU..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-white border border-slate-200 rounded-xl py-2.5 pl-10 pr-4 text-xs text-slate-900 shadow-sm"
/>
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-3" />
</div>
{/* Table */}
<div className="bg-white border border-slate-200 rounded-3xl overflow-hidden shadow-sm">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs">
<thead className="bg-slate-50 border-b border-slate-200 text-slate-500 font-semibold uppercase">
<tr>
<th className="p-4">Instrument</th>
<th className="p-4">Brand</th>
<th className="p-4">Category</th>
<th className="p-4">Regular / Sale Price</th>
<th className="p-4">Stock</th>
<th className="p-4 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredProducts.map((p) => (
<tr key={p._id} className="hover:bg-slate-50">
<td className="p-4">
<div className="flex items-center gap-3">
<img src={p.images?.[0]} alt="" className="w-10 h-10 object-cover rounded-lg border border-slate-200" />
<div>
<div className="font-bold text-slate-900 max-w-[200px] truncate">{p.name}</div>
<div className="text-slate-400 font-mono text-[11px]">SKU: {p.sku}</div>
</div>
</div>
</td>
<td className="p-4 text-slate-700 font-semibold">{p.brand?.name || 'N/A'}</td>
<td className="p-4 text-slate-700">{p.category?.name || 'N/A'}</td>
<td className="p-4 font-bold text-slate-900">
{formatINR(p.salePrice || p.price)}
{p.salePrice > 0 && p.salePrice < p.price && (
<span className="block text-[10px] text-slate-400 line-through font-normal">{formatINR(p.price)}</span>
)}
</td>
<td className="p-4">
<span className={`px-2.5 py-1 rounded-full text-[10px] font-bold ${
p.stock > 3 ? 'bg-emerald-50 text-emerald-700 border border-emerald-200' : 'bg-rose-50 text-rose-700 border border-rose-200'
}`}>
{p.stock} units
</span>
</td>
<td className="p-4 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => { setEditingProduct(p); setIsModalOpen(true); }}
className="p-1.5 bg-slate-100 hover:bg-slate-200 text-amber-700 rounded-lg"
>
<Edit3 className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteProduct(p._id)}
className="p-1.5 bg-slate-100 hover:bg-slate-200 text-rose-600 rounded-lg"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<ProductFormModal
product={editingProduct}
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSave={handleSaveProduct}
/>
</div>
);
};
export default AdminProducts;

View File

@@ -0,0 +1,152 @@
import React, { useState, useEffect } from 'react';
import { Settings } from 'lucide-react';
import { useDispatch } from 'react-redux';
import { showToast } from '../../store/slices/uiSlice';
import api from '../../api/axios';
const AdminSettings = () => {
const dispatch = useDispatch();
const [settings, setSettings] = useState({
businessName: 'Antigravity Guitars India',
siteName: 'Antigravity Guitars',
tagline: 'Premium Electric, Acoustic & Custom Guitars in India',
contactEmail: 'support@antigravityguitars.in',
phone: '+91 98765 43210',
address: '#102 MG Road, Indiranagar, Bengaluru, Karnataka 560038, India',
gstin: '29ABCDE1234F1Z5',
currencySymbol: '₹',
codEnabled: true,
maxCodAmount: 50000,
freeShippingThreshold: 5000
});
useEffect(() => {
api.get('/settings').then(res => setSettings(prev => ({ ...prev, ...res.data.data })));
}, []);
const handleSave = async (e) => {
e.preventDefault();
await api.put('/settings', settings);
dispatch(showToast({ message: 'Store settings updated successfully!', type: 'success' }));
};
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-slate-900 font-serif">Global Storefront & Indian Business Settings</h1>
<p className="text-xs text-slate-500">Configure Indian GST, business address, currency, COD limits, and store contact info.</p>
</div>
<form onSubmit={handleSave} className="p-6 bg-white border border-slate-200 shadow-sm rounded-3xl space-y-4 max-w-2xl text-xs">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-slate-600 font-semibold mb-1 block">Registered Business Name</label>
<input
type="text"
value={settings.businessName || ''}
onChange={(e) => setSettings({ ...settings, businessName: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">GSTIN Number</label>
<input
type="text"
value={settings.gstin || ''}
onChange={(e) => setSettings({ ...settings, gstin: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 font-mono uppercase focus:outline-none focus:border-amber-500"
/>
</div>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Store Front Name</label>
<input
type="text"
value={settings.siteName || ''}
onChange={(e) => setSettings({ ...settings, siteName: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Store Tagline</label>
<input
type="text"
value={settings.tagline || ''}
onChange={(e) => setSettings({ ...settings, tagline: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-slate-600 font-semibold mb-1 block">Support Email</label>
<input
type="email"
value={settings.contactEmail || ''}
onChange={(e) => setSettings({ ...settings, contactEmail: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Support Phone</label>
<input
type="text"
value={settings.phone || ''}
onChange={(e) => setSettings({ ...settings, phone: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Full Registered Business Address</label>
<input
type="text"
value={settings.address || ''}
onChange={(e) => setSettings({ ...settings, address: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div className="grid grid-cols-3 gap-4 pt-2 border-t border-slate-200">
<div>
<label className="text-slate-600 font-semibold mb-1 block">Currency Symbol</label>
<input
type="text"
value={settings.currencySymbol || '₹'}
onChange={(e) => setSettings({ ...settings, currencySymbol: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 font-bold focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Free Delivery Min (₹)</label>
<input
type="number"
value={settings.freeShippingThreshold || 5000}
onChange={(e) => setSettings({ ...settings, freeShippingThreshold: Number(e.target.value) })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Max COD Limit (₹)</label>
<input
type="number"
value={settings.maxCodAmount || 50000}
onChange={(e) => setSettings({ ...settings, maxCodAmount: Number(e.target.value) })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
</div>
<button type="submit" className="py-2.5 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl shadow-md shadow-amber-500/20">
Save Business & Store Settings
</button>
</form>
</div>
);
};
export default AdminSettings;

View File

@@ -0,0 +1,529 @@
import React, { useState, useEffect } from 'react';
import { X, Plus, Trash2, Upload, Check, Star } from 'lucide-react';
import api from '../../api/axios';
const defaultSpecs = {
guitarType: 'Electric',
bodyShape: 'Stratocaster',
bodyMaterial: 'Alder',
topMaterial: '',
backMaterial: '',
sideMaterial: '',
neckMaterial: 'Maple',
fingerboardMaterial: 'Rosewood',
scaleLength: '25.5"',
numberOfStrings: 6,
frets: 22,
pickupConfiguration: 'SSS',
pickupType: 'Alnico Single-Coil',
bridgeType: '2-Point Tremolo',
tuningMachines: 'Standard Cast/Sealed',
handOrientation: 'Right-Handed',
color: '3-Color Sunburst',
finish: 'Gloss Polyurethane',
includesCase: false
};
const ProductFormModal = ({ product, isOpen, onClose, onSave }) => {
const [categories, setCategories] = useState([]);
const [brands, setBrands] = useState([]);
const [uploading, setUploading] = useState(false);
// Requirement 13: Reset state cleanly when adding new product
const getInitialState = () => {
const randomSKU = `GTR-2026-${Math.floor(10000 + Math.random() * 90000)}`;
if (product) {
return {
...product,
brand: product.brand?._id || product.brand || '',
category: product.category?._id || product.category || '',
price: product.price || 0,
salePrice: product.salePrice || 0,
discountType: product.discountType || 'percentage',
discountValue: product.discountValue || 0,
stock: product.stock !== undefined ? product.stock : 10,
images: product.images || [],
primaryImage: product.primaryImage || (product.images?.[0] || ''),
specs: { ...defaultSpecs, ...(product.specs || {}) },
variants: product.variants || [],
isFeatured: Boolean(product.isFeatured),
isNewArrival: Boolean(product.isNewArrival),
isBestSeller: Boolean(product.isBestSeller)
};
}
return {
name: '',
slug: '',
sku: randomSKU, // Requirement 15: Auto-generated SKU
brand: '',
category: '',
price: 0,
salePrice: 0,
discountType: 'percentage',
discountValue: 0,
stock: 10,
description: '',
images: [],
primaryImage: '',
videoUrl: '',
specs: { ...defaultSpecs },
variants: [],
isFeatured: false,
isNewArrival: false,
isBestSeller: false
};
};
const [formData, setFormData] = useState(getInitialState);
useEffect(() => {
api.get('/categories').then(res => setCategories(res.data.data)).catch(() => {});
api.get('/brands').then(res => setBrands(res.data.data)).catch(() => {});
}, []);
useEffect(() => {
setFormData(getInitialState());
}, [product, isOpen]);
if (!isOpen) return null;
// Requirement 11: Auto calculate sale price from discount value & type
const handleDiscountChange = (type, val) => {
const regPrice = Number(formData.price) || 0;
let computedSalePrice = regPrice;
if (type === 'percentage') {
computedSalePrice = regPrice - (regPrice * (Number(val) || 0)) / 100;
} else if (type === 'fixed') {
computedSalePrice = regPrice - (Number(val) || 0);
}
computedSalePrice = Math.max(0, computedSalePrice);
setFormData({
...formData,
discountType: type,
discountValue: Number(val),
salePrice: Math.round(computedSalePrice)
});
};
const handlePriceChange = (regPrice) => {
const p = Number(regPrice) || 0;
let computedSalePrice = p;
if (formData.discountType === 'percentage') {
computedSalePrice = p - (p * (formData.discountValue || 0)) / 100;
} else {
computedSalePrice = p - (formData.discountValue || 0);
}
setFormData({
...formData,
price: p,
salePrice: Math.max(0, Math.round(computedSalePrice))
});
};
// Requirement 4: Complete Image Upload Handling
const handleImageUpload = async (e) => {
const files = e.target.files;
if (!files || files.length === 0) return;
const data = new FormData();
for (let i = 0; i < files.length; i++) {
data.append('images', files[i]);
}
try {
setUploading(true);
const res = await api.post('/upload/multiple', data, {
headers: { 'Content-Type': 'multipart/form-data' }
});
const uploadedUrls = res.data.data.urls || [];
const updatedImages = [...formData.images, ...uploadedUrls];
setFormData({
...formData,
images: updatedImages,
primaryImage: formData.primaryImage || updatedImages[0]
});
} catch (err) {
alert(err.response?.data?.message || 'Failed to upload images');
} finally {
setUploading(false);
}
};
const removeImage = (index) => {
const newImgs = [...formData.images];
const removed = newImgs.splice(index, 1)[0];
let newPrimary = formData.primaryImage;
if (newPrimary === removed) {
newPrimary = newImgs[0] || '';
}
setFormData({ ...formData, images: newImgs, primaryImage: newPrimary });
};
const handleSubmit = (e) => {
e.preventDefault();
if (formData.price < 0 || formData.salePrice > formData.price) {
alert('Invalid price configuration. Sale price cannot exceed regular price.');
return;
}
onSave(formData);
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 overflow-y-auto bg-slate-900/60 backdrop-blur-sm">
<div className="relative w-full max-w-4xl bg-white border border-slate-200 rounded-3xl p-6 md:p-8 shadow-2xl z-10 my-8 max-h-[90vh] overflow-y-auto text-slate-900">
<button onClick={onClose} className="absolute top-6 right-6 p-2 text-slate-400 hover:text-slate-900 rounded-full bg-slate-100">
<X className="w-5 h-5" />
</button>
<h2 className="text-xl font-bold text-slate-900 font-serif mb-6">
{product ? 'Edit Guitar Specifications' : 'Add New Instrument To Catalog'}
</h2>
<form onSubmit={handleSubmit} className="space-y-6 text-xs">
{/* Main Info */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-2">
<label className="text-slate-700 font-semibold mb-1 block">Guitar Product Title</label>
<input
type="text"
required
placeholder="Fender Player II Stratocaster"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value, slug: e.target.value.toLowerCase().replace(/\s+/g, '-') })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-slate-900"
/>
</div>
{/* Requirement 15: Auto-generated SKU */}
<div>
<label className="text-slate-700 font-semibold mb-1 block">Auto-Generated SKU</label>
<input
type="text"
required
readOnly
value={formData.sku}
className="w-full bg-slate-100 border border-slate-200 rounded-xl px-4 py-2.5 text-slate-700 font-mono font-bold"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="text-slate-700 font-semibold mb-1 block">Category</label>
<select
required
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-slate-900"
>
<option value="">Select Category</option>
{categories.map((c) => (
<option key={c._id} value={c._id}>{c.name}</option>
))}
</select>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Brand</label>
<select
required
value={formData.brand}
onChange={(e) => setFormData({ ...formData, brand: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-slate-900"
>
<option value="">Select Brand</option>
{brands.map((b) => (
<option key={b._id} value={b._id}>{b.name}</option>
))}
</select>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Stock Quantity</label>
<input
type="number"
required
min={0}
value={formData.stock}
onChange={(e) => setFormData({ ...formData, stock: Number(e.target.value) })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Video Demo URL</label>
<input
type="text"
placeholder="https://youtube.com/watch?v=..."
value={formData.videoUrl}
onChange={(e) => setFormData({ ...formData, videoUrl: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2.5 text-slate-900"
/>
</div>
</div>
{/* Requirement 11: Pricing & Discount Section */}
<div className="p-4 bg-slate-50 border border-slate-200 rounded-2xl space-y-3">
<div className="font-bold text-amber-700 uppercase tracking-wider text-[11px]">Pricing & Discount Configuration (₹ INR)</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="text-slate-700 font-semibold mb-1 block">Regular Price (₹)</label>
<input
type="number"
required
min={0}
value={formData.price}
onChange={(e) => handlePriceChange(e.target.value)}
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-slate-900 font-bold"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Discount Type</label>
<select
value={formData.discountType}
onChange={(e) => handleDiscountChange(e.target.value, formData.discountValue)}
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-slate-900"
>
<option value="percentage">Percentage (%)</option>
<option value="fixed">Fixed Amount (₹)</option>
</select>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Discount Value</label>
<input
type="number"
min={0}
value={formData.discountValue}
onChange={(e) => handleDiscountChange(formData.discountType, e.target.value)}
className="w-full bg-white border border-slate-200 rounded-xl p-2.5 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Calculated Sale Price (₹)</label>
<input
type="number"
readOnly
value={formData.salePrice}
className="w-full bg-slate-100 border border-slate-200 rounded-xl p-2.5 text-amber-700 font-extrabold"
/>
</div>
</div>
</div>
{/* Requirement 4: Product Image Upload Section */}
<div className="p-4 bg-slate-50 border border-slate-200 rounded-2xl space-y-3">
<div className="flex items-center justify-between">
<label className="font-bold text-slate-800 uppercase tracking-wider text-[11px]">Product Gallery Images</label>
<label className="py-2 px-4 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs flex items-center gap-1.5 cursor-pointer shadow-sm">
<Upload className="w-4 h-4" /> {uploading ? 'Uploading...' : 'Upload Images'}
<input type="file" multiple accept="image/*" onChange={handleImageUpload} className="hidden" />
</label>
</div>
{formData.images.length > 0 ? (
<div className="grid grid-cols-4 sm:grid-cols-6 gap-3">
{formData.images.map((img, idx) => (
<div key={idx} className="relative group aspect-square rounded-xl overflow-hidden border border-slate-200 bg-white">
<img src={img} alt="" className="w-full h-full object-cover" />
<button
type="button"
onClick={() => removeImage(idx)}
className="absolute top-1 right-1 p-1 bg-rose-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<X className="w-3 h-3" />
</button>
{formData.primaryImage === img && (
<span className="absolute bottom-1 left-1 bg-amber-500 text-white font-bold text-[9px] px-1.5 py-0.5 rounded">Primary</span>
)}
</div>
))}
</div>
) : (
<div className="text-center py-6 text-slate-400 text-xs border border-dashed border-slate-200 rounded-xl">
No images uploaded yet. Upload images above.
</div>
)}
</div>
{/* Requirement 10: Badges & Flags */}
<div className="p-4 bg-slate-50 border border-slate-200 rounded-2xl space-y-2">
<label className="font-bold text-slate-800 uppercase tracking-wider text-[11px]">Promotional Badges</label>
<div className="flex flex-wrap gap-6 text-xs font-semibold">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.isNewArrival}
onChange={(e) => setFormData({ ...formData, isNewArrival: e.target.checked })}
className="rounded text-amber-500"
/>
Mark as New Arrival (NEW)
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.isBestSeller}
onChange={(e) => setFormData({ ...formData, isBestSeller: e.target.checked })}
className="rounded text-amber-500"
/>
Mark as Best Seller
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={formData.isFeatured}
onChange={(e) => setFormData({ ...formData, isFeatured: e.target.checked })}
className="rounded text-amber-500"
/>
Mark as Featured Custom
</label>
</div>
</div>
{/* Requirement 9: Full Technical Specifications Fields */}
<div className="p-4 bg-slate-50 border border-slate-200 rounded-2xl space-y-4">
<div className="font-bold text-slate-800 uppercase tracking-wider text-[11px]">All Guitar Technical Specifications</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
<div>
<label className="text-slate-600">Instrument Type</label>
<input
type="text"
value={formData.specs.guitarType}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, guitarType: e.target.value } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
/>
</div>
<div>
<label className="text-slate-600">Body Shape</label>
<input
type="text"
value={formData.specs.bodyShape}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, bodyShape: e.target.value } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
/>
</div>
<div>
<label className="text-slate-600">Body Material</label>
<input
type="text"
value={formData.specs.bodyMaterial}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, bodyMaterial: e.target.value } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
/>
</div>
<div>
<label className="text-slate-600">Top Material</label>
<input
type="text"
value={formData.specs.topMaterial}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, topMaterial: e.target.value } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
/>
</div>
<div>
<label className="text-slate-600">Neck Material</label>
<input
type="text"
value={formData.specs.neckMaterial}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, neckMaterial: e.target.value } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
/>
</div>
<div>
<label className="text-slate-600">Fingerboard</label>
<input
type="text"
value={formData.specs.fingerboardMaterial}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, fingerboardMaterial: e.target.value } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
/>
</div>
<div>
<label className="text-slate-600">Scale Length</label>
<input
type="text"
value={formData.specs.scaleLength}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, scaleLength: e.target.value } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
/>
</div>
<div>
<label className="text-slate-600">Frets Count</label>
<input
type="number"
value={formData.specs.frets}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, frets: Number(e.target.value) } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
/>
</div>
<div>
<label className="text-slate-600">Pickup Configuration</label>
<input
type="text"
value={formData.specs.pickupConfiguration}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, pickupConfiguration: e.target.value } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
/>
</div>
<div>
<label className="text-slate-600">Bridge Type</label>
<input
type="text"
value={formData.specs.bridgeType}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, bridgeType: e.target.value } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
/>
</div>
<div>
<label className="text-slate-600">Hand Orientation</label>
<select
value={formData.specs.handOrientation}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, handOrientation: e.target.value } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
>
<option value="Right-Handed">Right-Handed</option>
<option value="Left-Handed">Left-Handed</option>
</select>
</div>
<div>
<label className="text-slate-600">Finish</label>
<input
type="text"
value={formData.specs.finish}
onChange={(e) => setFormData({ ...formData, specs: { ...formData.specs, finish: e.target.value } })}
className="w-full bg-white border border-slate-200 rounded-lg p-2 text-slate-900"
/>
</div>
</div>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Description & Notes</label>
<textarea
required
rows={3}
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-slate-900"
/>
</div>
<div className="flex justify-end gap-3 pt-4 border-t border-slate-200">
<button type="button" onClick={onClose} className="py-2.5 px-6 bg-slate-200 text-slate-800 font-bold rounded-xl">
Cancel
</button>
<button type="submit" className="py-2.5 px-8 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl shadow-sm">
Save Product
</button>
</div>
</form>
</div>
</div>
);
};
export default ProductFormModal;

View File

@@ -0,0 +1,24 @@
import React from 'react';
export const AboutUsPage = () => (
<div className="max-w-4xl mx-auto px-4 py-16 space-y-6">
<span className="text-xs font-bold text-amber-600 uppercase tracking-widest bg-amber-50 px-3 py-1 rounded-full border border-amber-200">Our Heritage</span>
<h1 className="text-3xl font-extrabold text-slate-900 font-serif">About Antigravity Guitars India</h1>
<p className="text-xs sm:text-sm text-slate-600 leading-relaxed">
Antigravity Guitars is India's premier destination for guitarists, tone purists, studio musicians, and live stage artists. We curate world-class acoustic, electric, and bass guitars alongside genuine accessories from top global manufacturers.
</p>
<p className="text-xs sm:text-sm text-slate-600 leading-relaxed">
Every instrument sold through our store undergoes a rigorous 55-point luthier setup inspection before dispatch across India. We partner directly with iconic guitar makers including Fender, Gibson, Martin, Taylor, PRS, Ibanez, and Yamaha.
</p>
</div>
);
export const PolicyPage = ({ title }) => (
<div className="max-w-4xl mx-auto px-4 py-16 space-y-6">
<h1 className="text-3xl font-extrabold text-slate-900 font-serif">{title}</h1>
<div className="p-6 bg-white border border-slate-200 shadow-sm rounded-3xl text-xs text-slate-600 space-y-4">
<p>All sales and transactions conducted through Antigravity Guitars India are protected under standard warranty terms and full buyer protection guaranteed.</p>
<p>Items may be returned within 30 days of arrival in original factory condition for a full refund or exchange.</p>
</div>
</div>
);

View File

@@ -0,0 +1,194 @@
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
import { Trash2, Plus, Minus, ArrowRight, ShieldCheck, ShoppingBag, Truck } from 'lucide-react';
import { fetchCart, updateCartItemAsync, removeCartItemAsync, applyCouponAsync } from '../../store/slices/cartSlice';
import { showToast } from '../../store/slices/uiSlice';
import { formatINR } from '../../utils/formatCurrency';
const CartPage = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { items, coupon } = useSelector((state) => state.cart);
const [couponCode, setCouponCode] = useState('');
useEffect(() => {
dispatch(fetchCart());
}, [dispatch]);
const subtotal = items.reduce((acc, item) => {
const itemPrice = item.product?.salePrice || item.product?.price || item.price;
return acc + itemPrice * item.quantity;
}, 0);
const discountAmount = coupon ? (coupon.discountType === 'percentage' ? (subtotal * coupon.discountValue) / 100 : coupon.discountValue) : 0;
const freeShippingThreshold = 5000; // ₹5,000 threshold for India
const shippingFee = subtotal >= freeShippingThreshold || subtotal === 0 ? 0 : 499;
const totalAmount = Math.max(0, subtotal - discountAmount + shippingFee);
const freeShippingProgress = Math.min(100, (subtotal / freeShippingThreshold) * 100);
const handleApplyCoupon = (e) => {
e.preventDefault();
if (couponCode.trim()) {
dispatch(applyCouponAsync(couponCode.trim()))
.unwrap()
.then(() => dispatch(showToast({ message: 'Coupon applied successfully!', type: 'success' })))
.catch((err) => dispatch(showToast({ message: err || 'Invalid coupon', type: 'error' })));
}
};
if (items.length === 0) {
return (
<div className="max-w-7xl mx-auto px-4 py-20 text-center space-y-4 bg-slate-50">
<div className="w-16 h-16 bg-white border border-slate-200 rounded-full flex items-center justify-center mx-auto text-slate-400 shadow-sm">
<ShoppingBag className="w-8 h-8" />
</div>
<h2 className="text-2xl font-bold text-slate-900 font-serif">Your Shopping Cart is Empty</h2>
<p className="text-xs text-slate-500">Discover hand-crafted custom shop electric, acoustic, and bass guitars.</p>
<Link to="/shop" className="inline-block py-3 px-8 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-2xl text-xs shadow-sm">
Explore Guitar Catalog
</Link>
</div>
);
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-8 bg-slate-50 text-slate-900">
<h1 className="text-3xl font-extrabold text-slate-900 font-serif">Your Shopping Cart ({items.length} Items)</h1>
{/* Free Shipping Progress Bar */}
<div className="p-4 bg-white border border-slate-200 rounded-2xl space-y-2 shadow-sm">
<div className="flex items-center justify-between text-xs font-semibold">
<span className="text-slate-700 flex items-center gap-2">
<Truck className="w-4 h-4 text-amber-600" />
{subtotal >= freeShippingThreshold
? '🎉 You qualify for FREE Express Delivery in India!'
: `Add ${formatINR(freeShippingThreshold - subtotal)} more to unlock FREE Delivery`}
</span>
<span className="text-amber-600 font-bold">{Math.round(freeShippingProgress)}%</span>
</div>
<div className="h-2 w-full bg-slate-100 rounded-full overflow-hidden">
<div className="h-full bg-amber-500 transition-all duration-500" style={{ width: `${freeShippingProgress}%` }} />
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Cart Items Table */}
<div className="lg:col-span-2 space-y-4">
{items.map((item) => {
const product = item.product || {};
const itemPrice = product.salePrice > 0 ? product.salePrice : (product.price || item.price);
return (
<div key={item._id} className="p-4 bg-white border border-slate-200 rounded-2xl flex flex-col sm:flex-row items-center justify-between gap-4 shadow-sm">
<div className="flex items-center gap-4 w-full sm:w-auto">
<img
src={product.images?.[0] || 'https://images.unsplash.com/photo-1564186763535-ebb21ef5277f?w=300'}
alt={product.name}
className="w-20 h-20 object-cover rounded-xl border border-slate-200"
/>
<div>
<span className="text-[10px] font-bold text-amber-600 uppercase">{product.brand?.name}</span>
<h3 className="text-sm font-bold text-slate-900 line-clamp-1">{product.name}</h3>
{item.variantColor && (
<div className="text-xs text-slate-500">Variant: {item.variantColor} ({item.variantFinish})</div>
)}
<div className="text-xs font-bold text-slate-900 mt-1">{formatINR(itemPrice)} each</div>
</div>
</div>
<div className="flex items-center justify-between sm:justify-end gap-6 w-full sm:w-auto">
{/* Quantity selector */}
<div className="flex items-center bg-slate-100 border border-slate-200 rounded-xl p-1">
<button
onClick={() => dispatch(updateCartItemAsync({ itemId: item._id, quantity: item.quantity - 1 }))}
className="p-1.5 text-slate-500 hover:text-slate-900"
>
<Minus className="w-3.5 h-3.5" />
</button>
<span className="w-8 text-center text-xs font-bold text-slate-900">{item.quantity}</span>
<button
onClick={() => dispatch(updateCartItemAsync({ itemId: item._id, quantity: item.quantity + 1 }))}
className="p-1.5 text-slate-500 hover:text-slate-900"
>
<Plus className="w-3.5 h-3.5" />
</button>
</div>
<div className="text-sm font-extrabold text-amber-700">{formatINR(itemPrice * item.quantity)}</div>
<button
onClick={() => dispatch(removeCartItemAsync(item._id))}
className="p-2 text-slate-400 hover:text-rose-600"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
);
})}
</div>
{/* Order Summary Column */}
<div className="space-y-6">
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-6 shadow-sm">
<h3 className="text-lg font-bold text-slate-900 font-serif">Order Summary</h3>
<div className="space-y-3 text-xs border-b border-slate-100 pb-4">
<div className="flex justify-between text-slate-600">
<span>Subtotal</span>
<span className="font-bold text-slate-900">{formatINR(subtotal)}</span>
</div>
{coupon && (
<div className="flex justify-between text-emerald-700">
<span>Discount ({coupon.code})</span>
<span className="font-bold">-{formatINR(discountAmount)}</span>
</div>
)}
<div className="flex justify-between text-slate-600">
<span>Estimated Shipping</span>
<span className="font-bold text-slate-900">{shippingFee === 0 ? 'FREE' : formatINR(shippingFee)}</span>
</div>
</div>
{/* Coupon Code Input */}
<form onSubmit={handleApplyCoupon} className="flex gap-2">
<input
type="text"
placeholder="Coupon (e.g. WELCOME10)"
value={couponCode}
onChange={(e) => setCouponCode(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-3 py-2 text-xs text-slate-900 uppercase focus:outline-none focus:border-amber-500"
/>
<button type="submit" className="py-2 px-4 bg-slate-900 hover:bg-slate-800 text-white font-bold rounded-xl text-xs">
Apply
</button>
</form>
<div className="flex justify-between items-baseline pt-2">
<span className="text-sm font-bold text-slate-900">Total Payable</span>
<span className="text-2xl font-extrabold text-amber-600">{formatINR(totalAmount)}</span>
</div>
<button
onClick={() => navigate('/checkout')}
className="w-full py-4 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-2xl text-xs sm:text-sm flex items-center justify-center gap-2 shadow-sm transition-all"
>
Proceed to Secure Checkout <ArrowRight className="w-4 h-4" />
</button>
<div className="flex items-center justify-center gap-2 text-xs text-slate-500 pt-2">
<ShieldCheck className="w-4 h-4 text-emerald-600" /> 256-Bit Encrypted Payment
</div>
</div>
</div>
</div>
</div>
);
};
export default CartPage;

View File

@@ -0,0 +1,306 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { ShieldCheck, CheckCircle2, CreditCard, Truck, MapPin, ArrowRight, Smartphone, Building } from 'lucide-react';
import { showToast } from '../../store/slices/uiSlice';
import { clearCart } from '../../store/slices/cartSlice';
import { formatINR } from '../../utils/formatCurrency';
import api from '../../api/axios';
const CheckoutPage = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const { items, coupon } = useSelector((state) => state.cart);
const { userInfo } = useSelector((state) => state.auth);
const [step, setStep] = useState(1);
const [shippingAddress, setShippingAddress] = useState({
fullName: userInfo?.name || 'Rahul Sharma',
mobileNumber: userInfo?.phone || '9812345678',
addressLine1: userInfo?.addresses?.[0]?.addressLine1 || 'Flat 402, Green Glen Layout',
addressLine2: userInfo?.addresses?.[0]?.addressLine2 || 'Outer Ring Road, Bellandur',
landmark: userInfo?.addresses?.[0]?.landmark || 'Near Sakra Hospital',
city: userInfo?.addresses?.[0]?.city || 'Bengaluru',
district: userInfo?.addresses?.[0]?.district || 'Bengaluru Urban',
state: userInfo?.addresses?.[0]?.state || 'Karnataka',
postalCode: userInfo?.addresses?.[0]?.postalCode || '560103',
country: 'India'
});
const [deliveryMethod, setDeliveryMethod] = useState('express');
const [paymentMethod, setPaymentMethod] = useState('Razorpay'); // Razorpay, UPI, Credit/Debit Card, Cash on Delivery
const [isSubmitting, setIsSubmitting] = useState(false);
const subtotal = items.reduce((acc, item) => {
const price = item.product?.salePrice || item.product?.price || item.price;
return acc + price * item.quantity;
}, 0);
const discountAmount = coupon ? (coupon.discountType === 'percentage' ? (subtotal * coupon.discountValue) / 100 : coupon.discountValue) : 0;
const shippingPrice = subtotal >= 5000 ? 0 : 499;
const gstRate = 0.18; // 18% GST for musical instruments in India
const taxPrice = Number((subtotal * gstRate).toFixed(2));
const totalAmount = Math.max(0, subtotal - discountAmount + shippingPrice + taxPrice);
const handlePlaceOrder = async () => {
if (!userInfo) {
dispatch(showToast({ message: 'Please sign in to place an order', type: 'error' }));
navigate('/login');
return;
}
try {
setIsSubmitting(true);
const orderItems = items.map((item) => ({
product: item.product?._id || item.product,
name: item.product?.name || 'Guitar Product',
image: item.product?.images?.[0] || '',
price: item.product?.salePrice || item.product?.price || item.price,
quantity: item.quantity,
variantColor: item.variantColor || '',
variantFinish: item.variantFinish || '',
sku: item.product?.sku || ''
}));
const res = await api.post('/orders', {
orderItems,
shippingAddress,
paymentMethod,
itemsPrice: subtotal,
gstPercentage: 18,
cgst: Number((taxPrice / 2).toFixed(2)),
sgst: Number((taxPrice / 2).toFixed(2)),
taxPrice,
shippingPrice,
discountAmount,
totalAmount
});
dispatch(clearCart());
dispatch(showToast({ message: 'Order placed successfully!', type: 'success' }));
navigate(`/order-confirmation/${res.data.data._id}`);
} catch (err) {
dispatch(showToast({ message: err.response?.data?.message || 'Failed to place order', type: 'error' }));
} finally {
setIsSubmitting(false);
}
};
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-8 bg-slate-50 text-slate-900">
{/* Step Tracker */}
<div className="flex items-center justify-between max-w-2xl mx-auto border-b border-slate-200 pb-6 text-xs font-bold">
<div className={`flex items-center gap-2 ${step >= 1 ? 'text-amber-600' : 'text-slate-400'}`}>
<MapPin className="w-4 h-4" /> 1. Shipping Address (India)
</div>
<div className={`flex items-center gap-2 ${step >= 2 ? 'text-amber-600' : 'text-slate-400'}`}>
<CreditCard className="w-4 h-4" /> 2. Payment Architecture
</div>
<div className={`flex items-center gap-2 ${step >= 3 ? 'text-amber-600' : 'text-slate-400'}`}>
<CheckCircle2 className="w-4 h-4" /> 3. Review & Order
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-6">
{/* Step 1: Indian Shipping Address Form */}
{step === 1 && (
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-4 shadow-sm">
<h3 className="text-lg font-bold text-slate-900 font-serif">Indian Delivery Address</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
<div>
<label className="text-slate-700 font-semibold mb-1 block">Full Name</label>
<input
type="text"
required
value={shippingAddress.fullName}
onChange={(e) => setShippingAddress({ ...shippingAddress, fullName: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">10-Digit Mobile Number (+91)</label>
<input
type="tel"
required
placeholder="9812345678"
value={shippingAddress.mobileNumber}
onChange={(e) => setShippingAddress({ ...shippingAddress, mobileNumber: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Flat, House No., Building</label>
<input
type="text"
required
value={shippingAddress.addressLine1}
onChange={(e) => setShippingAddress({ ...shippingAddress, addressLine1: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Area, Street, Sector</label>
<input
type="text"
value={shippingAddress.addressLine2}
onChange={(e) => setShippingAddress({ ...shippingAddress, addressLine2: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Landmark (Optional)</label>
<input
type="text"
value={shippingAddress.landmark}
onChange={(e) => setShippingAddress({ ...shippingAddress, landmark: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">City / Town</label>
<input
type="text"
required
value={shippingAddress.city}
onChange={(e) => setShippingAddress({ ...shippingAddress, city: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">State</label>
<input
type="text"
required
value={shippingAddress.state}
onChange={(e) => setShippingAddress({ ...shippingAddress, state: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">6-Digit PIN Code</label>
<input
type="text"
required
maxLength={6}
value={shippingAddress.postalCode}
onChange={(e) => setShippingAddress({ ...shippingAddress, postalCode: e.target.value })}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-slate-900 font-mono"
/>
</div>
</div>
<button
onClick={() => setStep(2)}
className="py-3.5 px-8 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs flex items-center gap-2 shadow-sm"
>
Continue to Payment Method <ArrowRight className="w-4 h-4" />
</button>
</div>
)}
{/* Step 2: Payment Method Selection */}
{step === 2 && (
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-6 shadow-sm">
<h3 className="text-lg font-bold text-slate-900 font-serif">Indian Payment Options</h3>
<div className="space-y-3">
<button
onClick={() => setPaymentMethod('Razorpay')}
className={`w-full p-4 rounded-2xl border text-left flex items-center justify-between transition-all ${
paymentMethod === 'Razorpay'
? 'border-amber-500 bg-amber-50 text-slate-900 font-bold'
: 'border-slate-200 bg-white text-slate-700'
}`}
>
<div className="flex items-center gap-3">
<Smartphone className="w-5 h-5 text-amber-600" />
<div>
<div className="text-sm font-bold">UPI / GPay / PhonePe / Paytm / Cards (Razorpay)</div>
<div className="text-xs text-slate-500">Instant verification & free courier priority</div>
</div>
</div>
</button>
<button
onClick={() => setPaymentMethod('Cash on Delivery')}
className={`w-full p-4 rounded-2xl border text-left flex items-center justify-between transition-all ${
paymentMethod === 'Cash on Delivery'
? 'border-amber-500 bg-amber-50 text-slate-900 font-bold'
: 'border-slate-200 bg-white text-slate-700'
}`}
>
<div className="flex items-center gap-3">
<Truck className="w-5 h-5 text-amber-600" />
<div>
<div className="text-sm font-bold">Cash on Delivery (COD)</div>
<div className="text-xs text-slate-500">Pay upon physical courier inspection in India</div>
</div>
</div>
</button>
</div>
<div className="flex gap-4 pt-4">
<button onClick={() => setStep(1)} className="py-3 px-6 bg-slate-200 text-slate-800 font-bold rounded-xl text-xs">
Back
</button>
<button
onClick={() => setStep(3)}
className="py-3.5 px-8 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs flex items-center gap-2"
>
Review Order <ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
)}
{/* Step 3: Review Order */}
{step === 3 && (
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-6 shadow-sm">
<h3 className="text-lg font-bold text-slate-900 font-serif">Review & Confirm Purchase</h3>
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-200 text-xs space-y-2">
<div className="font-bold text-amber-700">Delivery Destination:</div>
<div className="text-slate-800 font-semibold">{shippingAddress.fullName} (+91 {shippingAddress.mobileNumber})</div>
<div className="text-slate-600">{shippingAddress.addressLine1}, {shippingAddress.addressLine2}, {shippingAddress.city}, {shippingAddress.state} – {shippingAddress.postalCode}</div>
<div className="font-bold text-amber-700 pt-2">Selected Payment:</div>
<div className="text-slate-800 font-semibold">{paymentMethod}</div>
</div>
<button
onClick={handlePlaceOrder}
disabled={isSubmitting}
className="w-full py-4 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-2xl text-sm flex items-center justify-center gap-2 shadow-sm"
>
{isSubmitting ? 'Authorizing Purchase...' : 'Place Order & Pay'}
</button>
</div>
)}
</div>
{/* Order Summary Column */}
<div className="p-6 bg-white border border-slate-200 rounded-3xl space-y-4 h-fit shadow-sm">
<h3 className="text-base font-bold text-slate-900 font-serif">Price Breakdown (₹ INR)</h3>
<div className="space-y-3 text-xs border-b border-slate-100 pb-4">
<div className="flex justify-between text-slate-600"><span>Items ({items.length})</span><span>{formatINR(subtotal)}</span></div>
<div className="flex justify-between text-slate-600"><span>GST (18%)</span><span>{formatINR(taxPrice)}</span></div>
<div className="flex justify-between text-slate-600"><span>Shipping</span><span>{shippingPrice === 0 ? 'FREE' : formatINR(shippingPrice)}</span></div>
</div>
<div className="flex justify-between text-base font-extrabold text-slate-900">
<span>Total Payable</span>
<span className="text-amber-600">{formatINR(totalAmount)}</span>
</div>
</div>
</div>
</div>
);
};
export default CheckoutPage;

View File

@@ -0,0 +1,116 @@
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
import { X, ShoppingBag, ArrowLeft, Check, Minus } from 'lucide-react';
import { removeFromCompare, clearCompare } from '../../store/slices/compareSlice';
import { addToCartAsync } from '../../store/slices/cartSlice';
import { showToast } from '../../store/slices/uiSlice';
import { formatINR } from '../../utils/formatCurrency';
const ComparePage = () => {
const dispatch = useDispatch();
const { items } = useSelector((state) => state.compare);
if (items.length === 0) {
return (
<div className="max-w-7xl mx-auto px-4 py-20 text-center space-y-4 bg-slate-50">
<h2 className="text-2xl font-bold text-slate-900 font-serif">No Guitars Selected for Comparison</h2>
<p className="text-xs text-slate-500">Click the compare icon on any product card to add up to 4 instruments.</p>
<Link to="/shop" className="inline-block py-3 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs shadow-sm">
Browse Shop
</Link>
</div>
);
}
const specKeys = [
{ label: 'Brand', key: 'brand.name' },
{ label: 'Guitar Type', key: 'specs.guitarType' },
{ label: 'Body Shape', key: 'specs.bodyShape' },
{ label: 'Body Wood', key: 'specs.bodyMaterial' },
{ label: 'Top Wood', key: 'specs.topMaterial' },
{ label: 'Neck Wood', key: 'specs.neckMaterial' },
{ label: 'Fingerboard', key: 'specs.fingerboardMaterial' },
{ label: 'Scale Length', key: 'specs.scaleLength' },
{ label: 'Pickups', key: 'specs.pickupConfiguration' },
{ label: 'Bridge Type', key: 'specs.bridgeType' },
{ label: 'Handedness', key: 'specs.handOrientation' },
{ label: 'Frets', key: 'specs.frets' },
{ label: 'Case Included', key: 'specs.includesCase', isBool: true }
];
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-8 bg-slate-50 text-slate-900">
<div className="flex items-center justify-between border-b border-slate-200 pb-4">
<div>
<Link to="/shop" className="text-xs font-semibold text-amber-600 hover:underline flex items-center gap-1 mb-1">
<ArrowLeft className="w-3.5 h-3.5" /> Back to Shop
</Link>
<h1 className="text-3xl font-extrabold text-slate-900 font-serif">Guitar Specification Comparison Matrix</h1>
</div>
<button
onClick={() => dispatch(clearCompare())}
className="text-xs font-semibold text-rose-600 hover:underline"
>
Clear All
</button>
</div>
{/* Matrix Table */}
<div className="overflow-x-auto bg-white border border-slate-200 rounded-3xl shadow-sm">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr>
<th className="p-4 bg-slate-100 border-b border-r border-slate-200 text-slate-700 font-bold w-48">Specification</th>
{items.map((p) => (
<th key={p._id} className="p-4 bg-white border-b border-r border-slate-200 min-w-[220px]">
<div className="space-y-3 relative text-center">
<button
onClick={() => dispatch(removeFromCompare(p._id))}
className="absolute top-0 right-0 p-1 text-slate-400 hover:text-rose-600"
>
<X className="w-4 h-4" />
</button>
<img src={p.images?.[0]} alt="" className="w-28 h-24 object-cover rounded-xl mx-auto border border-slate-200" />
<h3 className="font-bold text-slate-900 text-xs line-clamp-2">{p.name}</h3>
<div className="text-amber-700 font-extrabold text-sm">{formatINR(p.salePrice || p.price)}</div>
<button
onClick={() => {
dispatch(addToCartAsync({ productId: p._id, quantity: 1 }));
dispatch(showToast({ message: `${p.name} added to cart`, type: 'success' }));
}}
className="w-full py-2 bg-slate-900 hover:bg-amber-600 text-white font-bold rounded-xl flex items-center justify-center gap-1 shadow-sm"
>
<ShoppingBag className="w-3.5 h-3.5" /> Add to Cart
</button>
</div>
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{specKeys.map((row) => (
<tr key={row.label} className="hover:bg-slate-50">
<td className="p-4 bg-slate-50 border-r border-slate-200 font-bold text-slate-700">{row.label}</td>
{items.map((p) => {
const val = row.key.split('.').reduce((o, i) => (o ? o[i] : undefined), p);
return (
<td key={p._id} className="p-4 border-r border-slate-200 text-slate-700 font-medium">
{row.isBool ? (
val ? <Check className="w-4 h-4 text-emerald-600" /> : <Minus className="w-4 h-4 text-slate-400" />
) : (
val || 'N/A'
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
export default ComparePage;

View File

@@ -0,0 +1,140 @@
import React, { useState } from 'react';
import { Mail, Phone, MapPin, Send, MessageSquare } from 'lucide-react';
import { useDispatch } from 'react-redux';
import { showToast } from '../../store/slices/uiSlice';
import api from '../../api/axios';
const ContactPage = () => {
const dispatch = useDispatch();
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
subject: 'General Inquiry',
message: ''
});
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
try {
setSubmitting(true);
await api.post('/contact', formData);
dispatch(showToast({ message: 'Thank you! Your message has been sent to our tone specialists.', type: 'success' }));
setFormData({ name: '', email: '', phone: '', subject: 'General Inquiry', message: '' });
} catch (err) {
dispatch(showToast({ message: 'Failed to submit contact form', type: 'error' }));
} finally {
setSubmitting(false);
}
};
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 space-y-12">
<div className="text-center max-w-2xl mx-auto space-y-3">
<span className="text-xs font-bold text-amber-600 uppercase tracking-widest bg-amber-50 px-3 py-1 rounded-full border border-amber-200">Connect With Antigravity</span>
<h1 className="text-3xl sm:text-4xl font-extrabold text-slate-900 font-serif">Speak With A Guitar Specialist</h1>
<p className="text-xs sm:text-sm text-slate-600">
Have questions about neck profile specs, pickup wiring, or custom luthier setups? Our guitar support team is ready to assist.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Info */}
<div className="p-8 bg-white border border-slate-200 shadow-sm rounded-3xl space-y-8">
<h3 className="text-xl font-bold text-slate-900 font-serif">India Experience Center & Store</h3>
<div className="space-y-6 text-xs text-slate-700">
<div className="flex items-start gap-4">
<div className="p-3 bg-amber-50 border border-amber-200 text-amber-600 rounded-2xl">
<MapPin className="w-5 h-5" />
</div>
<div>
<div className="font-bold text-slate-900 text-sm">Bengaluru Experience Store</div>
<div className="text-slate-600">#102 MG Road, Indiranagar</div>
<div className="text-slate-600">Bengaluru, Karnataka 560038, India</div>
</div>
</div>
<div className="flex items-start gap-4">
<div className="p-3 bg-amber-50 border border-amber-200 text-amber-600 rounded-2xl">
<Phone className="w-5 h-5" />
</div>
<div>
<div className="font-bold text-slate-900 text-sm">Customer Helpline (India)</div>
<div className="text-slate-600">+91 98765 43210 / 1800 123 4567 (Toll Free)</div>
<div className="text-slate-400">Mon-Sat 10:00 AM - 7:00 PM IST</div>
</div>
</div>
<div className="flex items-start gap-4">
<div className="p-3 bg-amber-50 border border-amber-200 text-amber-600 rounded-2xl">
<Mail className="w-5 h-5" />
</div>
<div>
<div className="font-bold text-slate-900 text-sm">Email Inquiries</div>
<div className="text-slate-600">support@antigravityguitars.in</div>
</div>
</div>
</div>
</div>
{/* Contact Form */}
<form onSubmit={handleSubmit} className="p-8 bg-white border border-slate-200 shadow-sm rounded-3xl space-y-4">
<h3 className="text-xl font-bold text-slate-900 font-serif mb-4">Send Us A Message</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs">
<div>
<label className="text-slate-600 font-semibold mb-1 block">Full Name</label>
<input
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Email Address</label>
<input
type="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
</div>
<div className="text-xs">
<label className="text-slate-600 font-semibold mb-1 block">Subject</label>
<input
type="text"
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div className="text-xs">
<label className="text-slate-600 font-semibold mb-1 block">Your Message</label>
<textarea
required
rows={4}
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl p-4 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<button
type="submit"
disabled={submitting}
className="py-3 px-8 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs flex items-center gap-2 shadow-md shadow-amber-500/20"
>
<Send className="w-4 h-4" /> {submitting ? 'Sending...' : 'Send Message'}
</button>
</form>
</div>
</div>
);
};
export default ContactPage;

View File

@@ -0,0 +1,250 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { Compass, Sparkles, ArrowRight, ChevronRight, Zap } from 'lucide-react';
import { openGuitarFinder } from '../../store/slices/uiSlice';
import ProductCard from '../../components/ProductCard';
import { ProductGridSkeleton } from '../../components/SkeletonLoaders';
import api from '../../api/axios';
const HomePage = () => {
const dispatch = useDispatch();
const [banners, setBanners] = useState([]);
const [categories, setCategories] = useState([]);
const [brands, setBrands] = useState([]);
const [featuredProducts, setFeaturedProducts] = useState([]);
const [newArrivals, setNewArrivals] = useState([]);
const [bestSellers, setBestSellers] = useState([]);
const [activeTab, setActiveTab] = useState('featured');
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadHomeData = async () => {
try {
setLoading(true);
const [bannerRes, catRes, brandRes, featRes, newRes, bestRes] = await Promise.all([
api.get('/banners'),
api.get('/categories'),
api.get('/brands'),
api.get('/products?isFeatured=true&limit=4'), // Requirement 7: Exactly 4 products
api.get('/products?isNewArrival=true&limit=4'),
api.get('/products?isBestSeller=true&limit=4')
]);
setBanners(bannerRes.data.data || []);
// Requirement 6: Display top 4 active featured categories
const allCats = catRes.data.data || [];
setCategories(allCats.slice(0, 4));
setBrands(brandRes.data.data || []);
setFeaturedProducts(featRes.data.data.products || []);
setNewArrivals(newRes.data.data.products || []);
setBestSellers(bestRes.data.data.products || []);
} catch (err) {
console.error('Failed to load home page data', err);
} finally {
setLoading(false);
}
};
loadHomeData();
}, []);
const displayedProducts =
activeTab === 'featured' ? featuredProducts :
activeTab === 'new' ? newArrivals : bestSellers;
return (
<div className="space-y-16 pb-20 bg-slate-50 text-slate-900">
{/* Requirement 5: Hero Banner Section with Left-Aligned Title, Subtitle, and Buttons */}
<section className="relative overflow-hidden bg-slate-900 border-b border-slate-200">
{banners.length > 0 ? (
<div className="relative min-h-[500px] md:min-h-[600px] flex items-center">
<img
src={banners[0].desktopImage}
alt=""
className="absolute inset-0 w-full h-full object-cover opacity-50"
/>
{/* Left-aligned gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-r from-slate-950 via-slate-900/90 to-transparent" />
<div className="relative max-w-7xl mx-auto px-6 sm:px-8 lg:px-12 w-full py-16">
{/* Left Aligned Content */}
<div className="max-w-2xl text-left space-y-5 animate-fadeIn">
<span className="inline-flex items-center gap-2 px-3 py-1 bg-amber-500/20 border border-amber-500/40 text-amber-400 text-xs font-bold rounded-full uppercase tracking-wider">
<Sparkles className="w-3.5 h-3.5" /> 2026 Guitar Heritage Collection
</span>
<h1 className="text-4xl sm:text-6xl font-extrabold text-white font-serif tracking-tight leading-tight text-left">
{banners[0].title}
</h1>
<p className="text-base sm:text-lg text-slate-300 font-normal leading-relaxed text-left max-w-xl">
{banners[0].subtitle}
</p>
<div className="pt-4 flex flex-wrap gap-4 justify-start">
<Link
to={banners[0].buttonLink || '/shop'}
className="py-3.5 px-7 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-sm shadow-md transition-all flex items-center gap-2"
>
{banners[0].buttonText || 'Explore Shop'} <ArrowRight className="w-4 h-4" />
</Link>
<button
onClick={() => dispatch(openGuitarFinder())}
className="py-3.5 px-7 bg-white/10 hover:bg-white/20 border border-white/30 text-white font-bold rounded-xl text-sm backdrop-blur-sm transition-all flex items-center gap-2"
>
<Compass className="w-4 h-4 text-amber-400" /> Interactive Tone Finder
</button>
</div>
</div>
</div>
</div>
) : (
<div className="py-24 text-center text-slate-400">Loading Banners...</div>
)}
</section>
{/* Requirement 6: Featured Categories Grid – Display ONLY 4 Cards */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-end justify-between mb-8">
<div>
<span className="text-xs font-bold text-amber-600 uppercase tracking-widest">Top Instrument Categories</span>
<h2 className="text-2xl sm:text-3xl font-extrabold text-slate-900 font-serif">Featured Collections</h2>
</div>
<Link to="/shop" className="text-xs font-bold text-amber-600 hover:underline flex items-center gap-1">
View All Categories <ChevronRight className="w-4 h-4" />
</Link>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{categories.map((cat) => (
<Link
key={cat._id}
to={`/shop?category=${cat.slug}`}
className="group relative h-56 rounded-2xl overflow-hidden border border-slate-200 bg-white p-6 flex flex-col justify-end transition-all hover:border-amber-500 hover:shadow-md"
>
<img
src={cat.homeImage || cat.image || cat.fallbackImage}
alt={cat.name}
className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-slate-900/40 to-transparent" />
<div className="relative z-10 text-left">
<h3 className="text-lg font-bold text-white group-hover:text-amber-400 transition-colors">
{cat.name}
</h3>
<p className="text-xs text-slate-300 line-clamp-1 mt-0.5">{cat.description}</p>
</div>
</Link>
))}
</div>
</section>
{/* Interactive Guitar Finder Banner */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="relative rounded-3xl bg-white border border-slate-200 p-8 sm:p-12 overflow-hidden shadow-sm flex flex-col md:flex-row items-center justify-between gap-8">
<div className="space-y-3 max-w-xl text-left">
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-amber-50 text-amber-700 text-xs font-bold rounded-full border border-amber-200">
<Zap className="w-3.5 h-3.5 text-amber-600" /> TONE RECOMMENDATION TOOL
</span>
<h3 className="text-2xl sm:text-3xl font-extrabold text-slate-900 font-serif">
Need Help Choosing The Perfect Guitar?
</h3>
<p className="text-xs sm:text-sm text-slate-600 leading-relaxed">
Use our interactive 30-second Tone Finder wizard. Select your playing style, neck preferences, and budget to get instant tailored recommendations.
</p>
</div>
<button
onClick={() => dispatch(openGuitarFinder())}
className="py-4 px-8 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-sm whitespace-nowrap shadow-sm"
>
Launch Guitar Finder
</button>
</div>
</section>
{/* Requirement 7: Home Page Product Showcase Sections – Display ONLY 4 Products Per Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
<div>
<span className="text-xs font-bold text-amber-600 uppercase tracking-widest">Hand-Selected Guitars</span>
<h2 className="text-2xl sm:text-3xl font-extrabold text-slate-900 font-serif">Curated Master Collections</h2>
</div>
<div className="flex items-center gap-4">
<div className="flex bg-slate-200/80 p-1 rounded-xl text-xs font-bold">
<button
onClick={() => setActiveTab('featured')}
className={`px-4 py-2 rounded-lg transition-all ${
activeTab === 'featured' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600 hover:text-slate-900'
}`}
>
Featured
</button>
<button
onClick={() => setActiveTab('new')}
className={`px-4 py-2 rounded-lg transition-all ${
activeTab === 'new' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600 hover:text-slate-900'
}`}
>
New Arrivals
</button>
<button
onClick={() => setActiveTab('bestsellers')}
className={`px-4 py-2 rounded-lg transition-all ${
activeTab === 'bestsellers' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600 hover:text-slate-900'
}`}
>
Best Sellers
</button>
</div>
<Link to="/shop" className="text-xs font-bold text-amber-600 hover:underline hidden sm:flex items-center gap-1">
View All <ChevronRight className="w-4 h-4" />
</Link>
</div>
</div>
{loading ? (
<ProductGridSkeleton count={4} />
) : (
<div className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{displayedProducts.slice(0, 4).map((product) => (
<ProductCard key={product._id} product={product} />
))}
</div>
<div className="text-center pt-4 sm:hidden">
<Link to="/shop" className="inline-block py-3 px-6 bg-slate-900 text-white font-bold rounded-xl text-xs">
View All Catalog Products
</Link>
</div>
</div>
)}
</section>
{/* Premier Brands */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-8">
<span className="text-xs font-bold text-amber-600 uppercase tracking-widest">Authorized Dealer</span>
<h2 className="text-2xl sm:text-3xl font-extrabold text-slate-900 font-serif">Premier Guitar Manufacturers</h2>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-8 gap-4">
{brands.map((brand) => (
<Link
key={brand._id}
to={`/shop?brand=${brand.slug}`}
className="p-5 bg-white border border-slate-200 hover:border-amber-500 rounded-2xl flex flex-col items-center justify-center text-center transition-all hover:shadow-sm"
>
<span className="font-bold text-xs text-slate-800 hover:text-amber-600">{brand.name}</span>
</Link>
))}
</div>
</section>
</div>
);
};
export default HomePage;

View File

@@ -0,0 +1,127 @@
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { login, register } from '../../store/slices/authSlice';
import { showToast } from '../../store/slices/uiSlice';
const LoginPage = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { loading } = useSelector((state) => state.auth);
const [isRegister, setIsRegister] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (isRegister) {
dispatch(register({ name, email, password, phone }))
.unwrap()
.then(() => {
dispatch(showToast({ message: 'Account created successfully!', type: 'success' }));
navigate('/');
})
.catch((err) => dispatch(showToast({ message: err || 'Registration failed', type: 'error' })));
} else {
dispatch(login({ email, password }))
.unwrap()
.then((user) => {
dispatch(showToast({ message: `Welcome back, ${user.name}!`, type: 'success' }));
navigate('/');
})
.catch((err) => dispatch(showToast({ message: err || 'Invalid email or password', type: 'error' })));
}
};
return (
<div className="max-w-md mx-auto px-4 py-20 bg-slate-50">
<div className="p-8 bg-white border border-slate-200 rounded-3xl space-y-6 shadow-sm">
<div className="text-center space-y-2">
<div className="w-12 h-12 bg-amber-500 rounded-2xl flex items-center justify-center font-bold text-white mx-auto text-xl shadow-sm">AG</div>
<h2 className="text-2xl font-bold text-slate-900 font-serif">{isRegister ? 'Create Customer Account' : 'Customer Sign In'}</h2>
<p className="text-xs text-slate-500">Access orders, saved guitars & tone recommendations</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4 text-xs">
{isRegister && (
<>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Full Name</label>
<input
type="text"
required
placeholder="Rahul Sharma"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Mobile Number (+91)</label>
<input
type="tel"
placeholder="9812345678"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-slate-900"
/>
</div>
</>
)}
<div>
<label className="text-slate-700 font-semibold mb-1 block">Email Address</label>
<input
type="email"
required
placeholder="john@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-slate-900"
/>
</div>
<div>
<label className="text-slate-700 font-semibold mb-1 block">Password</label>
<input
type="password"
required
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-3 text-slate-900"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3.5 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs shadow-sm transition-all"
>
{loading ? 'Authenticating...' : isRegister ? 'Register Account' : 'Sign In'}
</button>
</form>
<div className="p-4 bg-slate-50 border border-slate-200 rounded-2xl text-[11px] text-slate-600 space-y-1">
<div className="font-bold text-amber-700">Customer Demo Credentials:</div>
<div>Email: <code className="text-slate-900 font-bold">john@example.com</code></div>
<div>Password: <code className="text-slate-900 font-bold">Customer@123</code></div>
</div>
<div className="text-center text-xs">
<button
onClick={() => setIsRegister(!isRegister)}
className="text-amber-600 font-semibold hover:underline"
>
{isRegister ? 'Already registered? Sign In' : "New customer? Create an account"}
</button>
</div>
</div>
</div>
);
};
export default LoginPage;

View File

@@ -0,0 +1,77 @@
import React, { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { CheckCircle2, ArrowRight } from 'lucide-react';
import { formatINR } from '../../utils/formatCurrency';
import api from '../../api/axios';
const OrderConfirmationPage = () => {
const { id } = useParams();
const [order, setOrder] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.get(`/orders/${id}`)
.then((res) => setOrder(res.data.data))
.catch((err) => console.error(err))
.finally(() => setLoading(false));
}, [id]);
if (loading || !order) {
return <div className="max-w-7xl mx-auto px-4 py-20 text-center text-slate-500">Retrieving Order Receipt...</div>;
}
return (
<div className="max-w-3xl mx-auto px-4 py-16 text-center space-y-8 bg-slate-50 text-slate-900">
<div className="w-20 h-20 bg-emerald-50 text-emerald-600 rounded-full flex items-center justify-center mx-auto border border-emerald-200 shadow-sm">
<CheckCircle2 className="w-10 h-10" />
</div>
<div className="space-y-2">
<span className="text-xs font-bold text-amber-600 uppercase tracking-widest">Order Confirmed & Placed in India</span>
<h1 className="text-3xl font-extrabold text-slate-900 font-serif">Thank You for Your Order!</h1>
<p className="text-xs text-slate-500">Order Number: <strong className="text-slate-900 font-mono">{order.orderNumber}</strong></p>
</div>
{/* Summary Box */}
<div className="bg-white border border-slate-200 rounded-3xl p-6 text-left space-y-4 shadow-sm">
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wider border-b border-slate-100 pb-3">Purchased Items</h3>
<div className="space-y-3">
{order.orderItems.map((item, idx) => (
<div key={idx} className="flex items-center justify-between text-xs">
<div className="flex items-center gap-3">
<img src={item.image} alt="" className="w-12 h-12 object-cover rounded-lg border border-slate-200" />
<div>
<div className="font-bold text-slate-900">{item.name}</div>
<div className="text-slate-500">Qty: {item.quantity} {item.variantColor && `| ${item.variantColor}`}</div>
</div>
</div>
<div className="font-bold text-amber-700">{formatINR(item.price * item.quantity)}</div>
</div>
))}
</div>
<div className="border-t border-slate-100 pt-4 flex justify-between text-sm font-bold text-slate-900">
<span>Total Paid</span>
<span className="text-amber-700">{formatINR(order.totalAmount)}</span>
</div>
</div>
<div className="flex justify-center gap-4">
<Link
to={`/track-order/${order._id}`}
className="py-3.5 px-8 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-2xl text-xs flex items-center gap-2 shadow-sm"
>
Track Live Delivery Timeline <ArrowRight className="w-4 h-4" />
</Link>
<Link
to="/shop"
className="py-3.5 px-8 bg-white hover:bg-slate-50 text-slate-800 font-bold rounded-2xl text-xs border border-slate-200 shadow-sm"
>
Continue Shopping
</Link>
</div>
</div>
);
};
export default OrderConfirmationPage;

View File

@@ -0,0 +1,95 @@
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { Clock, CheckCircle2, Truck, PackageCheck, AlertCircle, ArrowLeft } from 'lucide-react';
import api from '../../api/axios';
import { formatINR } from '../../utils/formatCurrency';
const OrderTrackingPage = () => {
const { id } = useParams();
const [order, setOrder] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.get(`/orders/${id}`)
.then((res) => setOrder(res.data.data))
.catch((err) => console.error(err))
.finally(() => setLoading(false));
}, [id]);
if (loading || !order) {
return <div className="max-w-7xl mx-auto px-4 py-20 text-center text-slate-500">Loading Order Tracking...</div>;
}
const statuses = ['Pending', 'Payment Confirmed', 'Processing', 'Packed', 'Shipped', 'Out for Delivery', 'Delivered'];
const currentStep = statuses.indexOf(order.orderStatus);
return (
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-8">
<div>
<Link to="/dashboard" className="text-xs font-semibold text-amber-600 hover:text-amber-700 hover:underline flex items-center gap-1 mb-2">
<ArrowLeft className="w-3.5 h-3.5" /> Back to My Orders
</Link>
<h1 className="text-3xl font-extrabold text-slate-900 font-serif">Order Tracking & Status Timeline</h1>
<p className="text-xs text-slate-500 mt-1">Order ID: <strong className="text-slate-800 font-mono">{order.orderNumber}</strong></p>
</div>
{/* Visual Timeline Progress */}
<div className="p-8 bg-white border border-slate-200 shadow-sm rounded-3xl space-y-8">
<div className="flex flex-col sm:flex-row sm:items-center justify-between text-xs font-bold border-b border-slate-100 pb-4 gap-2">
<div>Status: <span className="text-amber-600 bg-amber-50 px-2.5 py-1 rounded-full border border-amber-200">{order.orderStatus}</span></div>
{order.trackingNumber && (
<div className="text-slate-600">Tracking #: <span className="text-slate-900 font-mono">{order.trackingNumber}</span> ({order.courierProvider || order.shippingProvider || 'BlueDart Express'})</div>
)}
<div className="text-slate-900">Total: {formatINR(order.totalAmount)}</div>
</div>
{/* Horizontal Timeline */}
<div className="relative flex items-center justify-between">
<div className="absolute left-0 right-0 top-1/2 -translate-y-1/2 h-1 bg-slate-100 z-0" />
<div
className="absolute left-0 top-1/2 -translate-y-1/2 h-1 bg-amber-500 transition-all duration-500 z-0"
style={{ width: `${(Math.max(0, currentStep) / (statuses.length - 1)) * 100}%` }}
/>
{statuses.map((st, idx) => {
const isCompleted = idx <= currentStep;
return (
<div key={st} className="relative z-10 flex flex-col items-center gap-2">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all ${
isCompleted
? 'bg-amber-500 text-white shadow-md shadow-amber-500/30'
: 'bg-white border border-slate-300 text-slate-400'
}`}
>
{isCompleted ? <CheckCircle2 className="w-4 h-4" /> : idx + 1}
</div>
<span className={`text-[10px] font-semibold hidden md:block ${isCompleted ? 'text-slate-900 font-bold' : 'text-slate-400'}`}>
{st}
</span>
</div>
);
})}
</div>
</div>
{/* Status History Logs */}
<div className="p-6 bg-white border border-slate-200 shadow-sm rounded-3xl space-y-4">
<h3 className="text-xs font-bold text-slate-800 uppercase tracking-wider">Order Status Log</h3>
<div className="space-y-3 text-xs">
{order.statusHistory?.map((hist, idx) => (
<div key={idx} className="p-3.5 bg-slate-50 border border-slate-200 rounded-xl flex items-center justify-between">
<div>
<span className="font-bold text-amber-700">{hist.status}</span>
<p className="text-slate-600 mt-0.5">{hist.note}</p>
</div>
<div className="text-[10px] text-slate-400 font-medium">{new Date(hist.updatedAt).toLocaleString()}</div>
</div>
))}
</div>
</div>
</div>
);
};
export default OrderTrackingPage;

View File

@@ -0,0 +1,402 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { Star, ShoppingBag, Heart, Sliders, Truck, ShieldCheck, RotateCcw, Check, Plus, Minus, Video, MessageSquare } from 'lucide-react';
import { addToCartAsync } from '../../store/slices/cartSlice';
import { toggleWishlistAsync } from '../../store/slices/wishlistSlice';
import { addToCompare } from '../../store/slices/compareSlice';
import { showToast } from '../../store/slices/uiSlice';
import ProductCard from '../../components/ProductCard';
import { formatINR } from '../../utils/formatCurrency';
import api from '../../api/axios';
const ProductDetailPage = () => {
const { slug } = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
const { userInfo } = useSelector((state) => state.auth);
const { products: wishlistProducts } = useSelector((state) => state.wishlist);
const [product, setProduct] = useState(null);
const [relatedProducts, setRelatedProducts] = useState([]);
const [selectedImage, setSelectedImage] = useState(0);
const [selectedVariant, setSelectedVariant] = useState(null);
const [quantity, setQuantity] = useState(1);
const [reviews, setReviews] = useState([]);
const [loading, setLoading] = useState(true);
// Review Form state
const [reviewRating, setReviewRating] = useState(5);
const [reviewTitle, setReviewTitle] = useState('');
const [reviewComment, setReviewComment] = useState('');
const [submittingReview, setSubmittingReview] = useState(false);
useEffect(() => {
const fetchProduct = async () => {
try {
setLoading(true);
const res = await api.get(`/products/${slug}`);
const p = res.data.data.product;
setProduct(p);
setRelatedProducts(res.data.data.relatedProducts || []);
if (p.variants && p.variants.length > 0) {
setSelectedVariant(p.variants[0]);
}
const revRes = await api.get(`/reviews/product/${p._id}`);
setReviews(revRes.data.data || []);
} catch (err) {
console.error('Failed to load product detail', err);
} finally {
setLoading(false);
}
};
fetchProduct();
}, [slug]);
if (loading || !product) {
return <div className="max-w-7xl mx-auto px-4 py-20 text-center text-slate-500">Loading Instrument Details...</div>;
}
const isWishlisted = wishlistProducts.some((p) => (p._id || p) === product._id);
const priceToDisplay = selectedVariant?.price || (product.salePrice > 0 ? product.salePrice : product.price);
const handleAddToCart = () => {
if (product.stock <= 0) return;
dispatch(
addToCartAsync({
productId: product._id,
quantity,
variantColor: selectedVariant?.color || '',
variantFinish: selectedVariant?.finish || ''
})
);
dispatch(showToast({ message: `${product.name} added to cart`, type: 'success' }));
};
const handleBuyNow = () => {
handleAddToCart();
navigate('/cart');
};
const handleReviewSubmit = async (e) => {
e.preventDefault();
if (!userInfo) {
dispatch(showToast({ message: 'Please sign in to write a review', type: 'error' }));
return;
}
try {
setSubmittingReview(true);
const res = await api.post('/reviews', {
productId: product._id,
rating: reviewRating,
title: reviewTitle,
comment: reviewComment
});
setReviews([res.data.data, ...reviews]);
setReviewTitle('');
setReviewComment('');
dispatch(showToast({ message: 'Review submitted successfully!', type: 'success' }));
} catch (err) {
dispatch(showToast({ message: err.response?.data?.message || 'Failed to submit review', type: 'error' }));
} finally {
setSubmittingReview(false);
}
};
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-16 bg-slate-50 text-slate-900">
{/* Product Detail Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Gallery Column */}
<div className="space-y-4">
<div className="aspect-[4/3] bg-white border border-slate-200 rounded-3xl overflow-hidden shadow-sm relative">
<img
src={product.images?.[selectedImage] || product.images?.[0]}
alt={product.name}
className="w-full h-full object-cover"
/>
{product.videoUrl && (
<a
href={product.videoUrl}
target="_blank"
rel="noreferrer"
className="absolute bottom-4 right-4 py-2 px-4 bg-white/90 border border-slate-200 backdrop-blur-md rounded-xl text-xs font-bold text-amber-700 flex items-center gap-2 shadow-sm"
>
<Video className="w-4 h-4 text-amber-600" /> Watch Audio Demo
</a>
)}
</div>
{/* Thumbnails */}
{product.images?.length > 1 && (
<div className="flex gap-3 overflow-x-auto pb-2">
{product.images.map((img, idx) => (
<button
key={idx}
onClick={() => setSelectedImage(idx)}
className={`w-20 h-20 rounded-2xl overflow-hidden border-2 flex-shrink-0 transition-all ${
selectedImage === idx ? 'border-amber-500 scale-105 shadow-sm' : 'border-slate-200 opacity-70 hover:opacity-100'
}`}
>
<img src={img} alt="" className="w-full h-full object-cover" />
</button>
))}
</div>
)}
</div>
{/* Info Column */}
<div className="space-y-6 text-left">
<div>
<span className="text-xs font-bold text-amber-600 uppercase tracking-widest">{product.brand?.name}</span>
<h1 className="text-2xl sm:text-3xl font-extrabold text-slate-900 font-serif mt-1">{product.name}</h1>
{/* Requirement 16: Hidden internal SKU from customer view */}
<div className="flex items-center gap-4 mt-3 text-xs">
<div className="flex items-center gap-1 text-amber-500 font-bold">
<Star className="w-4 h-4 fill-amber-400" /> {product.rating || '4.9'}
</div>
<span className="text-slate-300">|</span>
<span className="text-slate-600">{reviews.length} Verified Customer Reviews</span>
</div>
</div>
{/* Price & Stock */}
<div className="p-5 bg-white border border-slate-200 rounded-2xl flex items-center justify-between shadow-sm">
<div>
<div className="text-3xl font-extrabold text-slate-900">{formatINR(priceToDisplay)}</div>
{product.salePrice > 0 && product.salePrice < product.price && (
<div className="text-xs text-slate-400 line-through mt-0.5">{formatINR(product.price)}</div>
)}
</div>
<div className="text-right">
{product.stock > 0 ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-emerald-50 border border-emerald-200 text-emerald-700 text-xs font-bold rounded-full">
<Check className="w-3.5 h-3.5" /> In Stock ({product.stock} left)
</span>
) : (
<span className="px-3 py-1 bg-rose-50 border border-rose-200 text-rose-700 text-xs font-bold rounded-full">
Out of Stock
</span>
)}
</div>
</div>
<p className="text-xs sm:text-sm text-slate-600 leading-relaxed">{product.description}</p>
{/* Variants Selector */}
{product.variants && product.variants.length > 0 && (
<div className="space-y-3 pt-4 border-t border-slate-200">
<label className="text-xs font-bold text-slate-800">Select Finish & Color Variant:</label>
<div className="flex flex-wrap gap-3">
{product.variants.map((v, idx) => (
<button
key={idx}
onClick={() => setSelectedVariant(v)}
className={`py-2 px-4 rounded-xl text-xs font-bold border flex items-center gap-2 transition-all ${
selectedVariant?.color === v.color
? 'border-amber-500 bg-amber-50 text-amber-700 shadow-sm'
: 'border-slate-200 bg-white text-slate-700 hover:border-slate-300'
}`}
>
<span className="w-3.5 h-3.5 rounded-full border border-slate-300" style={{ backgroundColor: v.colorHex }} />
{v.color} ({v.finish})
</button>
))}
</div>
</div>
)}
{/* Quantity & Actions */}
<div className="space-y-4 pt-4 border-t border-slate-200">
<div className="flex items-center gap-4">
<div className="flex items-center bg-white border border-slate-200 rounded-xl p-1 shadow-sm">
<button
onClick={() => setQuantity(Math.max(1, quantity - 1))}
className="p-2 text-slate-500 hover:text-slate-900"
>
<Minus className="w-4 h-4" />
</button>
<span className="w-10 text-center text-sm font-bold text-slate-900">{quantity}</span>
<button
onClick={() => setQuantity(quantity + 1)}
className="p-2 text-slate-500 hover:text-slate-900"
>
<Plus className="w-4 h-4" />
</button>
</div>
<button
onClick={handleAddToCart}
disabled={product.stock <= 0}
className="flex-1 py-3.5 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-2xl text-xs sm:text-sm flex items-center justify-center gap-2 shadow-sm transition-all"
>
<ShoppingBag className="w-5 h-5" /> Add To Shopping Cart
</button>
<button
onClick={handleBuyNow}
disabled={product.stock <= 0}
className="py-3.5 px-6 bg-slate-900 hover:bg-slate-800 text-white font-bold rounded-2xl text-xs sm:text-sm border border-slate-900 whitespace-nowrap"
>
Buy Now
</button>
</div>
<div className="flex gap-4 pt-2">
<button
onClick={() => dispatch(toggleWishlistAsync(product._id))}
className="flex-1 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-semibold text-slate-700 hover:text-rose-600 flex items-center justify-center gap-2 shadow-sm"
>
<Heart className="w-4 h-4" fill={isWishlisted ? 'currentColor' : 'none'} /> {isWishlisted ? 'Wishlisted' : 'Save to Wishlist'}
</button>
<button
onClick={() => dispatch(addToCompare(product))}
className="flex-1 py-2.5 bg-white border border-slate-200 rounded-xl text-xs font-semibold text-slate-700 hover:text-amber-600 flex items-center justify-center gap-2 shadow-sm"
>
<Sliders className="w-4 h-4" /> Compare Specs
</button>
</div>
</div>
{/* Badges */}
<div className="grid grid-cols-3 gap-4 pt-4 border-t border-slate-200 text-center text-[11px] text-slate-500 font-medium">
<div className="flex flex-col items-center gap-1">
<Truck className="w-4 h-4 text-amber-600" /> Free India Shipping
</div>
<div className="flex flex-col items-center gap-1">
<ShieldCheck className="w-4 h-4 text-amber-600" /> 2-Year Warranty
</div>
<div className="flex flex-col items-center gap-1">
<RotateCcw className="w-4 h-4 text-amber-600" /> 10-Day Returns
</div>
</div>
</div>
</div>
{/* Requirement 9: Specifications Table (Displaying only non-empty specs) */}
{product.specs && (
<section className="bg-white border border-slate-200 rounded-3xl p-8 space-y-6 shadow-sm">
<h2 className="text-xl font-bold text-slate-900 font-serif">Guitar Technical Specifications</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 text-xs">
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-200 space-y-2">
<div className="text-slate-400 font-bold uppercase tracking-wider text-[11px]">Body & Construction</div>
{product.specs.guitarType && <div><span className="text-slate-500">Instrument Type:</span> <strong className="text-slate-900">{product.specs.guitarType}</strong></div>}
{product.specs.bodyShape && <div><span className="text-slate-500">Body Shape:</span> <strong className="text-slate-900">{product.specs.bodyShape}</strong></div>}
{product.specs.bodyMaterial && <div><span className="text-slate-500">Body Material:</span> <strong className="text-slate-900">{product.specs.bodyMaterial}</strong></div>}
{product.specs.topMaterial && <div><span className="text-slate-500">Top Wood:</span> <strong className="text-slate-900">{product.specs.topMaterial}</strong></div>}
</div>
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-200 space-y-2">
<div className="text-slate-400 font-bold uppercase tracking-wider text-[11px]">Neck & Fretboard</div>
{product.specs.neckMaterial && <div><span className="text-slate-500">Neck Material:</span> <strong className="text-slate-900">{product.specs.neckMaterial}</strong></div>}
{product.specs.fingerboardMaterial && <div><span className="text-slate-500">Fingerboard:</span> <strong className="text-slate-900">{product.specs.fingerboardMaterial}</strong></div>}
{product.specs.scaleLength && <div><span className="text-slate-500">Scale Length:</span> <strong className="text-slate-900">{product.specs.scaleLength}</strong></div>}
{product.specs.frets && <div><span className="text-slate-500">Frets Count:</span> <strong className="text-slate-900">{product.specs.frets} Frets</strong></div>}
</div>
<div className="p-4 bg-slate-50 rounded-2xl border border-slate-200 space-y-2">
<div className="text-slate-400 font-bold uppercase tracking-wider text-[11px]">Electronics & Hardware</div>
{product.specs.pickupConfiguration && <div><span className="text-slate-500">Pickups:</span> <strong className="text-slate-900">{product.specs.pickupConfiguration}</strong></div>}
{product.specs.bridgeType && <div><span className="text-slate-500">Bridge System:</span> <strong className="text-slate-900">{product.specs.bridgeType}</strong></div>}
{product.specs.handOrientation && <div><span className="text-slate-500">Handedness:</span> <strong className="text-slate-900">{product.specs.handOrientation}</strong></div>}
<div><span className="text-slate-500">Case Included:</span> <strong className="text-slate-900">{product.specs.includesCase ? 'Yes' : 'No'}</strong></div>
</div>
</div>
</section>
)}
{/* Customer Reviews Section */}
<section className="space-y-8">
<div className="flex items-center justify-between border-b border-slate-200 pb-4">
<h2 className="text-2xl font-bold text-slate-900 font-serif flex items-center gap-2">
<MessageSquare className="w-6 h-6 text-amber-600" /> Customer Reviews ({reviews.length})
</h2>
</div>
{/* Submit Review Form */}
<div className="bg-white border border-slate-200 rounded-3xl p-6 space-y-4 shadow-sm">
<h3 className="text-sm font-bold text-slate-900">Write a Review for {product.name}</h3>
<form onSubmit={handleReviewSubmit} className="space-y-4">
<div className="flex items-center gap-4">
<label className="text-xs text-slate-600 font-semibold">Rating:</label>
<select
value={reviewRating}
onChange={(e) => setReviewRating(Number(e.target.value))}
className="bg-slate-50 border border-slate-200 rounded-xl px-3 py-1.5 text-xs text-slate-900 font-semibold"
>
<option value={5}>5 Stars - Outstanding Tone</option>
<option value={4}>4 Stars - Great Quality</option>
<option value={3}>3 Stars - Average</option>
</select>
</div>
<input
type="text"
required
placeholder="Review Title"
value={reviewTitle}
onChange={(e) => setReviewTitle(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl px-4 py-2.5 text-xs text-slate-900"
/>
<textarea
required
rows={3}
placeholder="Share your detailed studio or live stage review..."
value={reviewComment}
onChange={(e) => setReviewComment(e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-4 text-xs text-slate-900"
/>
<button
type="submit"
disabled={submittingReview}
className="py-2.5 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs"
>
Submit Review
</button>
</form>
</div>
{/* Reviews List */}
<div className="space-y-4">
{reviews.map((rev) => (
<div key={rev._id} className="p-6 bg-white border border-slate-200 rounded-2xl space-y-2 shadow-sm">
<div className="flex items-center justify-between text-xs">
<div className="font-bold text-slate-900">{rev.user?.name || 'Verified Player'}</div>
<div className="flex text-amber-400">
{[...Array(rev.rating)].map((_, i) => (
<Star key={i} className="w-3.5 h-3.5 fill-amber-400" />
))}
</div>
</div>
<h4 className="text-sm font-bold text-amber-700">{rev.title}</h4>
<p className="text-xs text-slate-600 leading-relaxed">{rev.comment}</p>
</div>
))}
</div>
</section>
{/* Related Products */}
{relatedProducts.length > 0 && (
<section className="space-y-6">
<h2 className="text-2xl font-bold text-slate-900 font-serif">Similar Guitars You Might Like</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
{relatedProducts.map((p) => (
<ProductCard key={p._id} product={p} />
))}
</div>
</section>
)}
</div>
);
};
export default ProductDetailPage;

View File

@@ -0,0 +1,323 @@
import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { SlidersHorizontal, X, ChevronLeft, ChevronRight, RotateCcw } from 'lucide-react';
import ProductCard from '../../components/ProductCard';
import { ProductGridSkeleton } from '../../components/SkeletonLoaders';
import { formatINR } from '../../utils/formatCurrency';
import api from '../../api/axios';
const ShopPage = () => {
const [searchParams, setSearchParams] = useSearchParams();
const [products, setProducts] = useState([]);
const [categories, setCategories] = useState([]);
const [brands, setBrands] = useState([]);
const [pagination, setPagination] = useState({ page: 1, pages: 1, total: 0 });
const [loading, setLoading] = useState(true);
const [showMobileFilters, setShowMobileFilters] = useState(false);
// Filters state from URL query parameters
const selectedCategory = searchParams.get('category') || '';
const selectedBrand = searchParams.get('brand') || '';
const selectedSearch = searchParams.get('search') || '';
const selectedGuitarType = searchParams.get('guitarType') || '';
const selectedSort = searchParams.get('sort') || 'newest';
const currentPage = parseInt(searchParams.get('page')) || 1;
const minPrice = searchParams.get('minPrice') || '';
const maxPrice = searchParams.get('maxPrice') || '';
// Local price input state
const [priceRange, setPriceRange] = useState({
min: minPrice,
max: maxPrice
});
useEffect(() => {
api.get('/categories').then((res) => setCategories(res.data.data)).catch(() => {});
api.get('/brands').then((res) => setBrands(res.data.data)).catch(() => {});
}, []);
useEffect(() => {
const fetchProducts = async () => {
try {
setLoading(true);
const queryParams = new URLSearchParams(searchParams);
if (!queryParams.get('page')) queryParams.set('page', '1');
const res = await api.get(`/products?${queryParams.toString()}`);
setProducts(res.data.data.products || []);
setPagination(res.data.data.pagination || { page: 1, pages: 1, total: 0 });
} catch (err) {
console.error('Error fetching shop products', err);
} finally {
setLoading(false);
}
};
fetchProducts();
}, [searchParams]);
const updateFilter = (key, value) => {
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set(key, value);
} else {
newParams.delete(key);
}
newParams.set('page', '1');
setSearchParams(newParams);
};
const handlePriceApply = (e) => {
e.preventDefault();
const newParams = new URLSearchParams(searchParams);
if (priceRange.min) newParams.set('minPrice', priceRange.min);
else newParams.delete('minPrice');
if (priceRange.max) newParams.set('maxPrice', priceRange.max);
else newParams.delete('maxPrice');
newParams.set('page', '1');
setSearchParams(newParams);
};
const clearAllFilters = () => {
setPriceRange({ min: '', max: '' });
setSearchParams({});
};
const hasActiveFilters = Boolean(
selectedCategory || selectedBrand || selectedSearch || selectedGuitarType || minPrice || maxPrice
);
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-8 bg-slate-50 text-slate-900">
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-6 border-b border-slate-200">
<div>
<h1 className="text-3xl font-extrabold text-slate-900 font-serif">Guitar & Gear Catalog</h1>
<p className="text-xs text-slate-500 mt-1">Showing {pagination.total} high-grade instruments</p>
</div>
<div className="flex items-center gap-4">
<button
onClick={() => setShowMobileFilters(!showMobileFilters)}
className="md:hidden py-2 px-4 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-800 flex items-center gap-2 shadow-sm"
>
<SlidersHorizontal className="w-4 h-4 text-amber-600" /> Filters
</button>
{/* Sort Dropdown */}
<div className="flex items-center gap-2 text-xs">
<span className="text-slate-500 font-semibold hidden sm:inline">Sort By:</span>
<select
value={selectedSort}
onChange={(e) => updateFilter('sort', e.target.value)}
className="bg-white border border-slate-200 rounded-xl py-2 px-3 text-xs text-slate-800 focus:outline-none focus:border-amber-500 font-semibold shadow-sm"
>
<option value="newest">Newest Arrivals</option>
<option value="price_asc">Price: Low to High</option>
<option value="price_desc">Price: High to Low</option>
<option value="rating">Highest Rated</option>
<option value="bestselling">Best Selling</option>
</select>
</div>
</div>
</div>
{/* Applied Filters Feedback Bar */}
{hasActiveFilters && (
<div className="flex flex-wrap items-center gap-2 p-3 bg-amber-50 border border-amber-200 rounded-2xl text-xs">
<span className="font-bold text-amber-800">Active Filters:</span>
{selectedCategory && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-white border border-amber-300 rounded-full text-slate-800 font-semibold">
Category: {selectedCategory}
<X className="w-3 h-3 cursor-pointer text-slate-400 hover:text-slate-900" onClick={() => updateFilter('category', '')} />
</span>
)}
{selectedBrand && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-white border border-amber-300 rounded-full text-slate-800 font-semibold">
Brand: {selectedBrand}
<X className="w-3 h-3 cursor-pointer text-slate-400 hover:text-slate-900" onClick={() => updateFilter('brand', '')} />
</span>
)}
{minPrice && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-white border border-amber-300 rounded-full text-slate-800 font-semibold">
Min: {formatINR(minPrice)}
<X className="w-3 h-3 cursor-pointer text-slate-400 hover:text-slate-900" onClick={() => updateFilter('minPrice', '')} />
</span>
)}
{maxPrice && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-white border border-amber-300 rounded-full text-slate-800 font-semibold">
Max: {formatINR(maxPrice)}
<X className="w-3 h-3 cursor-pointer text-slate-400 hover:text-slate-900" onClick={() => updateFilter('maxPrice', '')} />
</span>
)}
<button onClick={clearAllFilters} className="text-amber-700 font-bold hover:underline ml-auto flex items-center gap-1">
<RotateCcw className="w-3 h-3" /> Clear All
</button>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Sidebar Filters */}
<aside className={`md:block space-y-6 ${showMobileFilters ? 'block' : 'hidden md:block'}`}>
<div className="p-6 bg-white border border-slate-200 rounded-3xl shadow-sm space-y-6">
<div className="flex items-center justify-between pb-4 border-b border-slate-100">
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wider flex items-center gap-2">
<SlidersHorizontal className="w-4 h-4 text-amber-600" /> Filter Options
</h3>
<button onClick={clearAllFilters} className="text-xs text-amber-600 hover:underline">
Reset
</button>
</div>
{/* Requirement 8: Category Filter Select Dropdown */}
<div className="space-y-2">
<label className="text-xs font-bold text-slate-700">Filter By Category</label>
<select
value={selectedCategory}
onChange={(e) => updateFilter('category', e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs text-slate-800 font-semibold"
>
<option value="">All Categories</option>
{categories.map((c) => (
<option key={c._id} value={c.slug}>{c.name}</option>
))}
</select>
</div>
{/* Requirement 8: Brand Filter Select Dropdown */}
<div className="space-y-2 pt-4 border-t border-slate-100">
<label className="text-xs font-bold text-slate-700">Filter By Brand</label>
<select
value={selectedBrand}
onChange={(e) => updateFilter('brand', e.target.value)}
className="w-full bg-slate-50 border border-slate-200 rounded-xl p-2.5 text-xs text-slate-800 font-semibold"
>
<option value="">All Brands</option>
{brands.map((b) => (
<option key={b._id} value={b.slug}>{b.name}</option>
))}
</select>
</div>
{/* Requirement 8: Price Filter (Min & Max Price) */}
<div className="space-y-3 pt-4 border-t border-slate-100">
<label className="text-xs font-bold text-slate-700">Price Range (₹ INR)</label>
<form onSubmit={handlePriceApply} className="space-y-3">
<div className="grid grid-cols-2 gap-2">
<input
type="number"
placeholder="Min ₹"
value={priceRange.min}
onChange={(e) => setPriceRange({ ...priceRange, min: e.target.value })}
className="bg-slate-50 border border-slate-200 rounded-xl p-2 text-xs text-slate-800"
/>
<input
type="number"
placeholder="Max ₹"
value={priceRange.max}
onChange={(e) => setPriceRange({ ...priceRange, max: e.target.value })}
className="bg-slate-50 border border-slate-200 rounded-xl p-2 text-xs text-slate-800"
/>
</div>
<button
type="submit"
className="w-full py-2 bg-slate-900 hover:bg-amber-600 text-white font-bold rounded-xl text-xs transition-colors"
>
Apply Price Filter
</button>
</form>
</div>
{/* Guitar Type */}
<div className="space-y-2 pt-4 border-t border-slate-100">
<label className="text-xs font-bold text-slate-700">Instrument Type</label>
<div className="grid grid-cols-2 gap-2">
{['Electric', 'Acoustic', 'Bass', 'Classical'].map((type) => (
<button
key={type}
onClick={() => updateFilter('guitarType', selectedGuitarType === type ? '' : type)}
className={`py-2 px-3 rounded-xl text-xs font-semibold border text-center transition-all ${
selectedGuitarType === type
? 'border-amber-500 bg-amber-50 text-amber-700 font-bold'
: 'border-slate-200 bg-slate-50 text-slate-600 hover:border-slate-300'
}`}
>
{type}
</button>
))}
</div>
</div>
</div>
</aside>
{/* Product Grid */}
<main className="md:col-span-3 space-y-8">
{loading ? (
<ProductGridSkeleton count={9} />
) : products.length === 0 ? (
<div className="p-12 text-center bg-white border border-slate-200 rounded-3xl space-y-4 shadow-sm">
<h3 className="text-xl font-bold text-slate-900">No guitars matched your search</h3>
<p className="text-xs text-slate-500">Try adjusting your filters or price parameters.</p>
<button
onClick={clearAllFilters}
className="py-2.5 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs"
>
Clear All Filters
</button>
</div>
) : (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map((product) => (
<ProductCard key={product._id} product={product} />
))}
</div>
{/* Pagination */}
{pagination.pages > 1 && (
<div className="flex items-center justify-center gap-2 pt-6">
<button
onClick={() => updateFilter('page', currentPage - 1)}
disabled={currentPage === 1}
className="p-2 bg-white border border-slate-200 rounded-xl text-slate-600 disabled:opacity-40 shadow-sm"
>
<ChevronLeft className="w-5 h-5" />
</button>
{[...Array(pagination.pages)].map((_, i) => (
<button
key={i + 1}
onClick={() => updateFilter('page', i + 1)}
className={`w-10 h-10 rounded-xl text-xs font-bold transition-all ${
currentPage === i + 1
? 'bg-amber-500 text-white shadow-sm'
: 'bg-white border border-slate-200 text-slate-700 hover:border-amber-500'
}`}
>
{i + 1}
</button>
))}
<button
onClick={() => updateFilter('page', currentPage + 1)}
disabled={currentPage === pagination.pages}
className="p-2 bg-white border border-slate-200 rounded-xl text-slate-600 disabled:opacity-40 shadow-sm"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
)}
</>
)}
</main>
</div>
</div>
);
};
export default ShopPage;

View File

@@ -0,0 +1,252 @@
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
import { User, Package, MapPin, Key, Shield, Eye, LogOut } from 'lucide-react';
import { updateProfile, logout } from '../../store/slices/authSlice';
import { showToast } from '../../store/slices/uiSlice';
import api from '../../api/axios';
import { formatINR } from '../../utils/formatCurrency';
const UserDashboardPage = () => {
const dispatch = useDispatch();
const { userInfo } = useSelector((state) => state.auth);
const [activeTab, setActiveTab] = useState('orders'); // orders, profile
const [orders, setOrders] = useState([]);
const [loadingOrders, setLoadingOrders] = useState(true);
// Indian Form states
const [name, setName] = useState(userInfo?.name || '');
const [phone, setPhone] = useState(userInfo?.phone || '');
const [addressLine1, setAddressLine1] = useState(userInfo?.addresses?.[0]?.addressLine1 || userInfo?.addresses?.[0]?.street || '');
const [addressLine2, setAddressLine2] = useState(userInfo?.addresses?.[0]?.addressLine2 || '');
const [city, setCity] = useState(userInfo?.addresses?.[0]?.city || '');
const [district, setDistrict] = useState(userInfo?.addresses?.[0]?.district || '');
const [state, setState] = useState(userInfo?.addresses?.[0]?.state || '');
const [postalCode, setPostalCode] = useState(userInfo?.addresses?.[0]?.postalCode || '');
useEffect(() => {
if (userInfo) {
api.get('/orders/my-orders')
.then((res) => setOrders(res.data.data || []))
.catch(() => {})
.finally(() => setLoadingOrders(false));
}
}, [userInfo]);
const handleProfileSubmit = (e) => {
e.preventDefault();
dispatch(
updateProfile({
name,
phone,
addresses: [{
addressLine1,
addressLine2,
street: addressLine1,
city,
district,
state,
postalCode,
country: 'India',
isDefault: true
}]
})
)
.unwrap()
.then(() => dispatch(showToast({ message: 'Profile saved successfully!', type: 'success' })))
.catch((err) => dispatch(showToast({ message: err || 'Update failed', type: 'error' })));
};
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-8">
{/* User Header */}
<div className="p-6 bg-white border border-slate-200 shadow-sm rounded-3xl flex flex-col md:flex-row items-center justify-between gap-6">
<div className="flex items-center gap-4">
<img src={userInfo?.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150'} alt="" className="w-16 h-16 rounded-full object-cover border-2 border-amber-500" />
<div>
<h1 className="text-xl font-bold text-slate-900 font-serif">{userInfo?.name}</h1>
<div className="text-xs text-slate-500">{userInfo?.email} | Role: <span className="text-amber-700 font-bold">{userInfo?.role}</span></div>
</div>
</div>
<div className="flex gap-3">
{userInfo?.role === 'Admin' && (
<Link to="/admin" className="py-2.5 px-4 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs flex items-center gap-1.5 shadow-sm">
<Shield className="w-4 h-4" /> Admin Portal
</Link>
)}
<button
onClick={() => dispatch(logout())}
className="py-2.5 px-4 bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold rounded-xl text-xs flex items-center gap-1.5 border border-slate-200 transition-colors"
>
<LogOut className="w-4 h-4 text-rose-500" /> Logout
</button>
</div>
</div>
{/* Tabs */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<aside className="space-y-2">
<button
onClick={() => setActiveTab('orders')}
className={`w-full p-3.5 rounded-2xl text-xs font-bold flex items-center gap-3 text-left transition-all ${
activeTab === 'orders' ? 'bg-amber-500 text-white shadow-md shadow-amber-500/20' : 'bg-white text-slate-700 hover:bg-slate-100 border border-slate-200'
}`}
>
<Package className="w-4 h-4" /> Order History ({orders.length})
</button>
<button
onClick={() => setActiveTab('profile')}
className={`w-full p-3.5 rounded-2xl text-xs font-bold flex items-center gap-3 text-left transition-all ${
activeTab === 'profile' ? 'bg-amber-500 text-white shadow-md shadow-amber-500/20' : 'bg-white text-slate-700 hover:bg-slate-100 border border-slate-200'
}`}
>
<User className="w-4 h-4" /> Personal Profile & Address
</button>
</aside>
<main className="md:col-span-3">
{/* Order History */}
{activeTab === 'orders' && (
<div className="space-y-4">
<h2 className="text-xl font-bold text-slate-900 font-serif">Order History</h2>
{loadingOrders ? (
<div className="text-xs text-slate-500">Loading orders...</div>
) : orders.length === 0 ? (
<div className="p-8 text-center bg-white border border-slate-200 rounded-2xl text-xs text-slate-500">
No orders placed yet.
</div>
) : (
<div className="space-y-4">
{orders.map((ord) => (
<div key={ord._id} className="p-5 bg-white border border-slate-200 shadow-sm rounded-2xl flex flex-col sm:flex-row items-center justify-between gap-4">
<div>
<div className="text-xs font-bold text-amber-700 font-mono">{ord.orderNumber}</div>
<div className="text-xs text-slate-500 mt-0.5">Date: {new Date(ord.createdAt).toLocaleDateString()} | Total: <strong className="text-slate-900 font-bold">{formatINR(ord.totalAmount)}</strong></div>
<div className="text-xs font-bold text-emerald-600 mt-1">Status: {ord.orderStatus}</div>
</div>
<Link
to={`/track-order/${ord._id}`}
className="py-2 px-4 bg-slate-900 hover:bg-slate-800 text-white font-bold rounded-xl text-xs flex items-center gap-1.5 shadow-sm"
>
<Eye className="w-4 h-4" /> Track Order
</Link>
</div>
))}
</div>
)}
</div>
)}
{/* Profile Form */}
{activeTab === 'profile' && (
<form onSubmit={handleProfileSubmit} className="p-6 bg-white border border-slate-200 shadow-sm rounded-3xl space-y-4 max-w-xl">
<h2 className="text-xl font-bold text-slate-900 font-serif mb-4">Edit Profile & Delivery Address (India)</h2>
<div className="space-y-3 text-xs">
<div>
<label className="text-slate-600 font-semibold mb-1 block">Full Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Mobile Number (+91)</label>
<input
type="text"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="10-digit Mobile Number"
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div className="pt-3 border-t border-slate-200 font-bold text-slate-900">Default Shipping Address (India)</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Flat / House No. / Building / Street</label>
<input
type="text"
value={addressLine1}
onChange={(e) => setAddressLine1(e.target.value)}
placeholder="Address Line 1"
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">Area / Colony / Sector / Landmark</label>
<input
type="text"
value={addressLine2}
onChange={(e) => setAddressLine2(e.target.value)}
placeholder="Address Line 2"
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-4 py-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-slate-600 font-semibold mb-1 block">City / Town</label>
<input
type="text"
placeholder="City"
value={city}
onChange={(e) => setCity(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3 py-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">District</label>
<input
type="text"
placeholder="District"
value={district}
onChange={(e) => setDistrict(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3 py-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="text-slate-600 font-semibold mb-1 block">State</label>
<input
type="text"
placeholder="State"
value={state}
onChange={(e) => setState(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3 py-2.5 text-slate-900 focus:outline-none focus:border-amber-500"
/>
</div>
<div>
<label className="text-slate-600 font-semibold mb-1 block">PIN Code (6 digits)</label>
<input
type="text"
placeholder="6-digit PIN Code"
value={postalCode}
onChange={(e) => setPostalCode(e.target.value)}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3 py-2.5 text-slate-900 focus:outline-none focus:border-amber-500 font-mono"
/>
</div>
</div>
</div>
<button
type="submit"
className="py-3 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs shadow-md shadow-amber-500/20"
>
Save Profile & Address
</button>
</form>
)}
</main>
</div>
</div>
);
};
export default UserDashboardPage;

View File

@@ -0,0 +1,41 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { Heart } from 'lucide-react';
import { fetchWishlist } from '../../store/slices/wishlistSlice';
import ProductCard from '../../components/ProductCard';
const WishlistPage = () => {
const dispatch = useDispatch();
const { products } = useSelector((state) => state.wishlist);
useEffect(() => {
dispatch(fetchWishlist());
}, [dispatch]);
if (products.length === 0) {
return (
<div className="max-w-7xl mx-auto px-4 py-20 text-center space-y-4 bg-slate-50 text-slate-900">
<Heart className="w-12 h-12 text-slate-300 mx-auto" />
<h2 className="text-2xl font-bold text-slate-900 font-serif">Your Wishlist is Empty</h2>
<p className="text-xs text-slate-500">Save your favorite guitars and custom shop models here.</p>
<Link to="/shop" className="inline-block py-3 px-6 bg-amber-500 hover:bg-amber-600 text-white font-bold rounded-xl text-xs shadow-sm">
Browse Shop
</Link>
</div>
);
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-8 bg-slate-50 text-slate-900">
<h1 className="text-3xl font-extrabold text-slate-900 font-serif">Your Saved Wishlist ({products.length})</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{products.map((p) => (
<ProductCard key={p._id || p} product={p} />
))}
</div>
</div>
);
};
export default WishlistPage;

17
client/src/store/index.js Normal file
View File

@@ -0,0 +1,17 @@
import { configureStore } from '@reduxjs/toolkit';
import authReducer from './slices/authSlice';
import cartReducer from './slices/cartSlice';
import wishlistReducer from './slices/wishlistSlice';
import compareReducer from './slices/compareSlice';
import uiReducer from './slices/uiSlice';
export const store = configureStore({
reducer: {
auth: authReducer,
cart: cartReducer,
wishlist: wishlistReducer,
compare: compareReducer,
ui: uiReducer
}
});

View File

@@ -0,0 +1,73 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import api from '../../api/axios';
const userInfoFromStorage = localStorage.getItem('userInfo')
? JSON.parse(localStorage.getItem('userInfo'))
: null;
export const login = createAsyncThunk('auth/login', async ({ email, password }, { rejectWithValue }) => {
try {
const res = await api.post('/auth/login', { email, password });
localStorage.setItem('userInfo', JSON.stringify(res.data.data));
return res.data.data;
} catch (err) {
return rejectWithValue(err.response?.data?.message || 'Login failed');
}
});
export const register = createAsyncThunk('auth/register', async (userData, { rejectWithValue }) => {
try {
const res = await api.post('/auth/register', userData);
localStorage.setItem('userInfo', JSON.stringify(res.data.data));
return res.data.data;
} catch (err) {
return rejectWithValue(err.response?.data?.message || 'Registration failed');
}
});
export const updateProfile = createAsyncThunk('auth/updateProfile', async (userData, { rejectWithValue }) => {
try {
const res = await api.put('/auth/profile', userData);
localStorage.setItem('userInfo', JSON.stringify(res.data.data));
return res.data.data;
} catch (err) {
return rejectWithValue(err.response?.data?.message || 'Profile update failed');
}
});
const authSlice = createSlice({
name: 'auth',
initialState: {
userInfo: userInfoFromStorage,
loading: false,
error: null
},
reducers: {
logout: (state) => {
localStorage.removeItem('userInfo');
state.userInfo = null;
state.error = null;
},
clearError: (state) => {
state.error = null;
}
},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => { state.loading = true; state.error = null; })
.addCase(login.fulfilled, (state, action) => { state.loading = false; state.userInfo = action.payload; })
.addCase(login.rejected, (state, action) => { state.loading = false; state.error = action.payload; })
.addCase(register.pending, (state) => { state.loading = true; state.error = null; })
.addCase(register.fulfilled, (state, action) => { state.loading = false; state.userInfo = action.payload; })
.addCase(register.rejected, (state, action) => { state.loading = false; state.error = action.payload; })
.addCase(updateProfile.pending, (state) => { state.loading = true; })
.addCase(updateProfile.fulfilled, (state, action) => { state.loading = false; state.userInfo = action.payload; })
.addCase(updateProfile.rejected, (state, action) => { state.loading = false; state.error = action.payload; });
}
});
export const { logout, clearError } = authSlice.actions;
export default authSlice.reducer;

View File

@@ -0,0 +1,85 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import api from '../../api/axios';
export const fetchCart = createAsyncThunk('cart/fetchCart', async (_, { rejectWithValue }) => {
try {
const res = await api.get('/cart');
return res.data.data;
} catch (err) {
return rejectWithValue(err.response?.data?.message || 'Failed to fetch cart');
}
});
export const addToCartAsync = createAsyncThunk('cart/addToCartAsync', async (itemData, { rejectWithValue }) => {
try {
const res = await api.post('/cart/add', itemData);
return res.data.data;
} catch (err) {
return rejectWithValue(err.response?.data?.message || 'Failed to add item');
}
});
export const updateCartItemAsync = createAsyncThunk('cart/updateCartItemAsync', async ({ itemId, quantity }, { rejectWithValue }) => {
try {
const res = await api.put('/cart/update', { itemId, quantity });
return res.data.data;
} catch (err) {
return rejectWithValue(err.response?.data?.message || 'Failed to update item');
}
});
export const removeCartItemAsync = createAsyncThunk('cart/removeCartItemAsync', async (itemId, { rejectWithValue }) => {
try {
const res = await api.delete(`/cart/item/${itemId}`);
return res.data.data;
} catch (err) {
return rejectWithValue(err.response?.data?.message || 'Failed to remove item');
}
});
export const applyCouponAsync = createAsyncThunk('cart/applyCouponAsync', async (code, { rejectWithValue }) => {
try {
const res = await api.post('/cart/coupon', { code });
return res.data.data;
} catch (err) {
return rejectWithValue(err.response?.data?.message || 'Invalid coupon');
}
});
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
coupon: null,
loading: false,
error: null
},
reducers: {
clearCart: (state) => {
state.items = [];
state.coupon = null;
}
},
extraReducers: (builder) => {
builder
.addCase(fetchCart.fulfilled, (state, action) => {
state.items = action.payload.items || [];
})
.addCase(addToCartAsync.fulfilled, (state, action) => {
state.items = action.payload.items || [];
})
.addCase(updateCartItemAsync.fulfilled, (state, action) => {
state.items = action.payload.items || [];
})
.addCase(removeCartItemAsync.fulfilled, (state, action) => {
state.items = action.payload.items || [];
})
.addCase(applyCouponAsync.fulfilled, (state, action) => {
state.coupon = action.payload;
});
}
});
export const { clearCart } = cartSlice.actions;
export default cartSlice.reducer;

View File

@@ -0,0 +1,33 @@
import { createSlice } from '@reduxjs/toolkit';
const initialItems = localStorage.getItem('compareProducts')
? JSON.parse(localStorage.getItem('compareProducts'))
: [];
const compareSlice = createSlice({
name: 'compare',
initialState: {
items: initialItems
},
reducers: {
addToCompare: (state, action) => {
const exists = state.items.some(item => item._id === action.payload._id);
if (!exists && state.items.length < 4) {
state.items.push(action.payload);
localStorage.setItem('compareProducts', JSON.stringify(state.items));
}
},
removeFromCompare: (state, action) => {
state.items = state.items.filter(item => item._id !== action.payload);
localStorage.setItem('compareProducts', JSON.stringify(state.items));
},
clearCompare: (state) => {
state.items = [];
localStorage.removeItem('compareProducts');
}
}
});
export const { addToCompare, removeFromCompare, clearCompare } = compareSlice.actions;
export default compareSlice.reducer;

View File

@@ -0,0 +1,30 @@
import { createSlice } from '@reduxjs/toolkit';
const uiSlice = createSlice({
name: 'ui',
initialState: {
guitarFinderOpen: false,
quickViewProduct: null,
toast: null // { message, type: 'success' | 'error' | 'info' }
},
reducers: {
openGuitarFinder: (state) => { state.guitarFinderOpen = true; },
closeGuitarFinder: (state) => { state.guitarFinderOpen = false; },
openQuickView: (state, action) => { state.quickViewProduct = action.payload; },
closeQuickView: (state) => { state.quickViewProduct = null; },
showToast: (state, action) => { state.toast = action.payload; },
hideToast: (state) => { state.toast = null; }
}
});
export const {
openGuitarFinder,
closeGuitarFinder,
openQuickView,
closeQuickView,
showToast,
hideToast
} = uiSlice.actions;
export default uiSlice.reducer;

View File

@@ -0,0 +1,41 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import api from '../../api/axios';
export const fetchWishlist = createAsyncThunk('wishlist/fetchWishlist', async (_, { rejectWithValue }) => {
try {
const res = await api.get('/wishlist');
return res.data.data;
} catch (err) {
return rejectWithValue(err.response?.data?.message || 'Failed to fetch wishlist');
}
});
export const toggleWishlistAsync = createAsyncThunk('wishlist/toggleWishlistAsync', async (productId, { rejectWithValue }) => {
try {
const res = await api.post('/wishlist/toggle', { productId });
return res.data.data;
} catch (err) {
return rejectWithValue(err.response?.data?.message || 'Failed to update wishlist');
}
});
const wishlistSlice = createSlice({
name: 'wishlist',
initialState: {
products: [],
loading: false
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchWishlist.fulfilled, (state, action) => {
state.products = action.payload.products || [];
})
.addCase(toggleWishlistAsync.fulfilled, (state, action) => {
state.products = action.payload.products || [];
});
}
});
export default wishlistSlice.reducer;

View File

@@ -0,0 +1,15 @@
/**
* Indian Rupee (₹ INR) Currency Formatter Helper
* Formats numbers in Indian format (e.g., 49999 -> ₹49,999)
*/
export const formatINR = (amount = 0) => {
const num = Number(amount) || 0;
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
maximumFractionDigits: 0
}).format(num);
};
export default formatINR;

25
client/tailwind.config.js Normal file
View File

@@ -0,0 +1,25 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
amber: {
50: '#fffbeb',
100: '#fef3c7',
500: '#d97706', // Primary amber/gold accent
600: '#b45309',
700: '#92400e'
}
},
fontFamily: {
sans: ['Plus Jakarta Sans', 'Inter', 'sans-serif'],
serif: ['Playfair Display', 'serif']
}
},
},
plugins: [],
}

20
client/vite.config.js Normal file
View File

@@ -0,0 +1,20 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
},
'/uploads': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
});