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

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;