create-ecom-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +101 -0
- package/package.json +25 -0
- package/templates/ecommerce/app/about/page.tsx +152 -0
- package/templates/ecommerce/app/cart/page.tsx +103 -0
- package/templates/ecommerce/app/checkout/page.tsx +126 -0
- package/templates/ecommerce/app/contact/page.tsx +124 -0
- package/templates/ecommerce/app/globals.css +137 -0
- package/templates/ecommerce/app/layout.tsx +31 -0
- package/templates/ecommerce/app/login/page.tsx +224 -0
- package/templates/ecommerce/app/not-found.tsx +14 -0
- package/templates/ecommerce/app/orders/[id]/page.tsx +138 -0
- package/templates/ecommerce/app/page.tsx +215 -0
- package/templates/ecommerce/app/product/[id]/page.tsx +417 -0
- package/templates/ecommerce/app/products/page.tsx +244 -0
- package/templates/ecommerce/app/profile/page.tsx +273 -0
- package/templates/ecommerce/app/register/page.tsx +221 -0
- package/templates/ecommerce/app/wishlist/page.tsx +89 -0
- package/templates/ecommerce/components/CartItem.tsx +74 -0
- package/templates/ecommerce/components/CheckoutForm.tsx +109 -0
- package/templates/ecommerce/components/Filters.tsx +142 -0
- package/templates/ecommerce/components/Footer.tsx +100 -0
- package/templates/ecommerce/components/Header.tsx +194 -0
- package/templates/ecommerce/components/ProductCard.tsx +158 -0
- package/templates/ecommerce/components/SearchBar.tsx +41 -0
- package/templates/ecommerce/data/products.json +427 -0
- package/templates/ecommerce/lib/localStorage.ts +27 -0
- package/templates/ecommerce/lib/utils.ts +25 -0
- package/templates/ecommerce/next-env.d.ts +5 -0
- package/templates/ecommerce/next.config.js +13 -0
- package/templates/ecommerce/next.config.ts +14 -0
- package/templates/ecommerce/package.json +30 -0
- package/templates/ecommerce/postcss.config.js +7 -0
- package/templates/ecommerce/public/images/about-2.jpg +0 -0
- package/templates/ecommerce/public/images/contact-bg.jpg +0 -0
- package/templates/ecommerce/public/images/hero-bg.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-1.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-10.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-11.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-12.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-13.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-14.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-15.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-16.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-17.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-18.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-19.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-2.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-20.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-21.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-22.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-23.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-24.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-25.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-3.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-4.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-5.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-6.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-7.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-8.jpg +0 -0
- package/templates/ecommerce/public/images/products/product-9.jpg +0 -0
- package/templates/ecommerce/public/service-worker.js +1 -0
- package/templates/ecommerce/store/useAuthStore.ts +56 -0
- package/templates/ecommerce/store/useCartStore.ts +63 -0
- package/templates/ecommerce/store/useOrderStore.ts +43 -0
- package/templates/ecommerce/store/useReviewStore.ts +60 -0
- package/templates/ecommerce/store/useWishlistStore.ts +43 -0
- package/templates/ecommerce/tailwind.config.ts +44 -0
- package/templates/ecommerce/tsconfig.json +22 -0
- package/templates/ecommerce/types/index.ts +69 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
6
|
+
import { SlidersHorizontal, X } from 'lucide-react';
|
|
7
|
+
import ProductCard from '@/components/ProductCard';
|
|
8
|
+
import SearchBar from '@/components/SearchBar';
|
|
9
|
+
import Filters from '@/components/Filters';
|
|
10
|
+
import productsData from '@/data/products.json';
|
|
11
|
+
import type { Product } from '@/types';
|
|
12
|
+
|
|
13
|
+
const products = productsData as Product[];
|
|
14
|
+
const MIN_PRICE = 0;
|
|
15
|
+
const MAX_PRICE = 20000;
|
|
16
|
+
|
|
17
|
+
const containerVariants = {
|
|
18
|
+
hidden: {},
|
|
19
|
+
visible: { transition: { staggerChildren: 0.05 } },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const itemVariants = {
|
|
23
|
+
hidden: { opacity: 0, y: 20 },
|
|
24
|
+
visible: { opacity: 1, y: 0, transition: { duration: 0.35 } },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ActiveChip = { key: string; label: string };
|
|
28
|
+
|
|
29
|
+
export default function ProductsPage() {
|
|
30
|
+
const searchParams = useSearchParams();
|
|
31
|
+
const initialCat = searchParams.get('category') || 'All';
|
|
32
|
+
|
|
33
|
+
const [search, setSearch] = useState('');
|
|
34
|
+
const [category, setCategory] = useState(initialCat);
|
|
35
|
+
const [priceRange, setPriceRange] = useState<[number, number]>([MIN_PRICE, MAX_PRICE]);
|
|
36
|
+
const [minRating, setMinRating] = useState(0);
|
|
37
|
+
const [sortBy, setSortBy] = useState('default');
|
|
38
|
+
const [showFilters, setShowFilters] = useState(false);
|
|
39
|
+
|
|
40
|
+
const activeChips: ActiveChip[] = useMemo(() => {
|
|
41
|
+
const chips: ActiveChip[] = [];
|
|
42
|
+
if (search) chips.push({ key: 'search', label: `"${search}"` });
|
|
43
|
+
if (category !== 'All') chips.push({ key: 'category', label: category });
|
|
44
|
+
if (priceRange[0] > MIN_PRICE || priceRange[1] < MAX_PRICE)
|
|
45
|
+
chips.push({ key: 'price', label: `₹${priceRange[0].toLocaleString('en-IN')}–₹${priceRange[1].toLocaleString('en-IN')}` });
|
|
46
|
+
if (minRating > 0) chips.push({ key: 'rating', label: `${minRating}+ stars` });
|
|
47
|
+
return chips;
|
|
48
|
+
}, [search, category, priceRange, minRating]);
|
|
49
|
+
|
|
50
|
+
const removeChip = (key: string) => {
|
|
51
|
+
if (key === 'search') setSearch('');
|
|
52
|
+
if (key === 'category') setCategory('All');
|
|
53
|
+
if (key === 'price') setPriceRange([MIN_PRICE, MAX_PRICE]);
|
|
54
|
+
if (key === 'rating') setMinRating(0);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const filtered = useMemo(() => {
|
|
58
|
+
let result = products.filter((p) => {
|
|
59
|
+
const matchSearch =
|
|
60
|
+
p.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
61
|
+
p.description.toLowerCase().includes(search.toLowerCase()) ||
|
|
62
|
+
p.category.toLowerCase().includes(search.toLowerCase());
|
|
63
|
+
const matchCat = category === 'All' || p.category === category;
|
|
64
|
+
const matchPrice = p.price >= priceRange[0] && p.price <= priceRange[1];
|
|
65
|
+
const matchRating = p.rating >= minRating;
|
|
66
|
+
return matchSearch && matchCat && matchPrice && matchRating;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (sortBy === 'price-asc') result = [...result].sort((a, b) => a.price - b.price);
|
|
70
|
+
if (sortBy === 'price-desc') result = [...result].sort((a, b) => b.price - a.price);
|
|
71
|
+
if (sortBy === 'rating') result = [...result].sort((a, b) => b.rating - a.rating);
|
|
72
|
+
if (sortBy === 'popular') result = [...result].sort((a, b) => b.reviewCount - a.reviewCount);
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
}, [search, category, priceRange, minRating, sortBy]);
|
|
76
|
+
|
|
77
|
+
const handleReset = () => {
|
|
78
|
+
setSearch('');
|
|
79
|
+
setCategory('All');
|
|
80
|
+
setPriceRange([MIN_PRICE, MAX_PRICE]);
|
|
81
|
+
setMinRating(0);
|
|
82
|
+
setSortBy('default');
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<motion.div
|
|
87
|
+
className="container-main py-10"
|
|
88
|
+
initial={{ opacity: 0, y: 16 }}
|
|
89
|
+
animate={{ opacity: 1, y: 0 }}
|
|
90
|
+
transition={{ duration: 0.4 }}
|
|
91
|
+
>
|
|
92
|
+
<div className="mb-6">
|
|
93
|
+
<h1 className="page-title">All Products</h1>
|
|
94
|
+
<p className="text-gray-500">{filtered.length} products found</p>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Search + Sort */}
|
|
98
|
+
<div className="flex flex-col sm:flex-row gap-3 mb-4">
|
|
99
|
+
<div className="flex-1">
|
|
100
|
+
<SearchBar value={search} onChange={setSearch} />
|
|
101
|
+
</div>
|
|
102
|
+
<select
|
|
103
|
+
value={sortBy}
|
|
104
|
+
onChange={(e) => setSortBy(e.target.value)}
|
|
105
|
+
className="input w-full sm:w-48"
|
|
106
|
+
id="sort-select"
|
|
107
|
+
aria-label="Sort products"
|
|
108
|
+
>
|
|
109
|
+
<option value="default">Sort: Default</option>
|
|
110
|
+
<option value="price-asc">Price: Low → High</option>
|
|
111
|
+
<option value="price-desc">Price: High → Low</option>
|
|
112
|
+
<option value="rating">Top Rated</option>
|
|
113
|
+
<option value="popular">Most Popular</option>
|
|
114
|
+
</select>
|
|
115
|
+
<button
|
|
116
|
+
className="sm:hidden btn-secondary flex items-center justify-center gap-2"
|
|
117
|
+
onClick={() => setShowFilters(true)}
|
|
118
|
+
id="toggle-filters"
|
|
119
|
+
>
|
|
120
|
+
<SlidersHorizontal className="w-4 h-4" />
|
|
121
|
+
Filters
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* Active Filter Chips */}
|
|
126
|
+
<AnimatePresence>
|
|
127
|
+
{activeChips.length > 0 && (
|
|
128
|
+
<motion.div
|
|
129
|
+
initial={{ opacity: 0, height: 0 }}
|
|
130
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
131
|
+
exit={{ opacity: 0, height: 0 }}
|
|
132
|
+
className="flex flex-wrap gap-2 mb-5"
|
|
133
|
+
>
|
|
134
|
+
{activeChips.map((chip) => (
|
|
135
|
+
<motion.button
|
|
136
|
+
key={chip.key}
|
|
137
|
+
initial={{ scale: 0.8, opacity: 0 }}
|
|
138
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
139
|
+
exit={{ scale: 0.8, opacity: 0 }}
|
|
140
|
+
onClick={() => removeChip(chip.key)}
|
|
141
|
+
className="flex items-center gap-1.5 bg-gray-900 text-white text-xs font-medium px-3 py-1.5 rounded-full hover:bg-gray-700 transition-colors"
|
|
142
|
+
>
|
|
143
|
+
{chip.label}
|
|
144
|
+
<X className="w-3 h-3" />
|
|
145
|
+
</motion.button>
|
|
146
|
+
))}
|
|
147
|
+
<button
|
|
148
|
+
onClick={handleReset}
|
|
149
|
+
className="text-xs font-medium text-gray-400 hover:text-gray-700 px-2 transition-colors"
|
|
150
|
+
>
|
|
151
|
+
Clear all
|
|
152
|
+
</button>
|
|
153
|
+
</motion.div>
|
|
154
|
+
)}
|
|
155
|
+
</AnimatePresence>
|
|
156
|
+
|
|
157
|
+
<div className="flex gap-8">
|
|
158
|
+
{/* Sidebar Filters — Desktop */}
|
|
159
|
+
<aside className="hidden sm:block w-64 flex-shrink-0">
|
|
160
|
+
<Filters
|
|
161
|
+
selectedCategory={category}
|
|
162
|
+
onCategoryChange={setCategory}
|
|
163
|
+
minPrice={MIN_PRICE}
|
|
164
|
+
maxPrice={MAX_PRICE}
|
|
165
|
+
priceRange={priceRange}
|
|
166
|
+
onPriceChange={setPriceRange}
|
|
167
|
+
minRating={minRating}
|
|
168
|
+
onRatingChange={setMinRating}
|
|
169
|
+
onReset={handleReset}
|
|
170
|
+
/>
|
|
171
|
+
</aside>
|
|
172
|
+
|
|
173
|
+
{/* Mobile Filter Drawer */}
|
|
174
|
+
<AnimatePresence>
|
|
175
|
+
{showFilters && (
|
|
176
|
+
<>
|
|
177
|
+
<motion.div
|
|
178
|
+
key="overlay"
|
|
179
|
+
initial={{ opacity: 0 }}
|
|
180
|
+
animate={{ opacity: 1 }}
|
|
181
|
+
exit={{ opacity: 0 }}
|
|
182
|
+
className="fixed inset-0 z-50 bg-black/40 sm:hidden"
|
|
183
|
+
onClick={() => setShowFilters(false)}
|
|
184
|
+
/>
|
|
185
|
+
<motion.div
|
|
186
|
+
key="drawer"
|
|
187
|
+
initial={{ x: '100%' }}
|
|
188
|
+
animate={{ x: 0 }}
|
|
189
|
+
exit={{ x: '100%' }}
|
|
190
|
+
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
|
191
|
+
className="fixed right-0 top-0 h-full w-72 bg-white z-50 shadow-2xl overflow-y-auto sm:hidden"
|
|
192
|
+
>
|
|
193
|
+
<div className="flex items-center justify-between p-5 border-b border-gray-100">
|
|
194
|
+
<h3 className="font-bold">Filters</h3>
|
|
195
|
+
<button onClick={() => setShowFilters(false)} className="text-gray-400 hover:text-gray-700 p-1">
|
|
196
|
+
<X className="w-5 h-5" />
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
<div className="p-5">
|
|
200
|
+
<Filters
|
|
201
|
+
selectedCategory={category}
|
|
202
|
+
onCategoryChange={(c) => { setCategory(c); setShowFilters(false); }}
|
|
203
|
+
minPrice={MIN_PRICE}
|
|
204
|
+
maxPrice={MAX_PRICE}
|
|
205
|
+
priceRange={priceRange}
|
|
206
|
+
onPriceChange={setPriceRange}
|
|
207
|
+
minRating={minRating}
|
|
208
|
+
onRatingChange={setMinRating}
|
|
209
|
+
onReset={() => { handleReset(); setShowFilters(false); }}
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
</motion.div>
|
|
213
|
+
</>
|
|
214
|
+
)}
|
|
215
|
+
</AnimatePresence>
|
|
216
|
+
|
|
217
|
+
{/* Products Grid */}
|
|
218
|
+
<div className="flex-1">
|
|
219
|
+
{filtered.length === 0 ? (
|
|
220
|
+
<div className="text-center py-24 text-gray-400">
|
|
221
|
+
<div className="text-6xl mb-4">🔍</div>
|
|
222
|
+
<p className="text-lg font-semibold text-gray-700">No products found</p>
|
|
223
|
+
<p className="text-sm mt-1 mb-4">Try adjusting your filters or search terms</p>
|
|
224
|
+
<button onClick={handleReset} className="btn-primary">Reset Filters</button>
|
|
225
|
+
</div>
|
|
226
|
+
) : (
|
|
227
|
+
<motion.div
|
|
228
|
+
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"
|
|
229
|
+
variants={containerVariants}
|
|
230
|
+
initial="hidden"
|
|
231
|
+
animate="visible"
|
|
232
|
+
>
|
|
233
|
+
{filtered.map((product) => (
|
|
234
|
+
<motion.div key={product.id} variants={itemVariants}>
|
|
235
|
+
<ProductCard product={product} />
|
|
236
|
+
</motion.div>
|
|
237
|
+
))}
|
|
238
|
+
</motion.div>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</motion.div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
7
|
+
import { User, Package, Star, Save, CheckCircle } from 'lucide-react';
|
|
8
|
+
import { useAuthStore } from '@/store/useAuthStore';
|
|
9
|
+
import { useOrderStore } from '@/store/useOrderStore';
|
|
10
|
+
import { useReviewStore } from '@/store/useReviewStore';
|
|
11
|
+
import { formatPrice } from '@/lib/utils';
|
|
12
|
+
import type { OrderStatus } from '@/types';
|
|
13
|
+
|
|
14
|
+
const STATUS_COLORS: Record<OrderStatus, string> = {
|
|
15
|
+
confirmed: 'bg-blue-100 text-blue-700',
|
|
16
|
+
processing: 'bg-amber-100 text-amber-700',
|
|
17
|
+
shipped: 'bg-violet-100 text-violet-700',
|
|
18
|
+
delivered: 'bg-green-100 text-green-700',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type Tab = 'profile' | 'orders' | 'reviews';
|
|
22
|
+
|
|
23
|
+
export default function ProfilePage() {
|
|
24
|
+
const router = useRouter();
|
|
25
|
+
const searchParams = useSearchParams();
|
|
26
|
+
const initialTab = (searchParams.get('tab') as Tab) || 'profile';
|
|
27
|
+
const [activeTab, setActiveTab] = useState<Tab>(initialTab);
|
|
28
|
+
const { user, isAuthenticated, logout } = useAuthStore();
|
|
29
|
+
const { getUserOrders } = useOrderStore();
|
|
30
|
+
const { getUserReviews } = useReviewStore();
|
|
31
|
+
const [name, setName] = useState('');
|
|
32
|
+
const [saved, setSaved] = useState(false);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!isAuthenticated) router.replace('/login');
|
|
36
|
+
if (user) setName(user.name);
|
|
37
|
+
}, [isAuthenticated, user, router]);
|
|
38
|
+
|
|
39
|
+
if (!user) return null;
|
|
40
|
+
|
|
41
|
+
const orders = getUserOrders(user.email);
|
|
42
|
+
const reviews = getUserReviews(user.id);
|
|
43
|
+
|
|
44
|
+
const handleSaveProfile = (e: React.FormEvent) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
setSaved(true);
|
|
47
|
+
setTimeout(() => setSaved(false), 2000);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const TABS = [
|
|
51
|
+
{ key: 'profile' as Tab, label: 'Profile', icon: User },
|
|
52
|
+
{ key: 'orders' as Tab, label: `Orders (${orders.length})`, icon: Package },
|
|
53
|
+
{ key: 'reviews' as Tab, label: `Reviews (${reviews.length})`, icon: Star },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<motion.div
|
|
58
|
+
className="container-main py-10"
|
|
59
|
+
initial={{ opacity: 0, y: 16 }}
|
|
60
|
+
animate={{ opacity: 1, y: 0 }}
|
|
61
|
+
transition={{ duration: 0.4 }}
|
|
62
|
+
>
|
|
63
|
+
<div className="mb-8">
|
|
64
|
+
<h1 className="page-title">My Account</h1>
|
|
65
|
+
<p className="text-gray-500">Manage your profile, orders, and reviews</p>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="flex flex-col lg:flex-row gap-8">
|
|
69
|
+
{/* Sidebar */}
|
|
70
|
+
<aside className="w-full lg:w-64 flex-shrink-0">
|
|
71
|
+
<div className="bg-white rounded-2xl shadow-sm p-5">
|
|
72
|
+
{/* Avatar */}
|
|
73
|
+
<div className="flex items-center gap-4 mb-6 pb-5 border-b border-gray-100">
|
|
74
|
+
<div className="w-14 h-14 bg-gray-900 rounded-2xl flex items-center justify-center flex-shrink-0">
|
|
75
|
+
<span className="text-white text-xl font-bold">{user.name.charAt(0).toUpperCase()}</span>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="min-w-0">
|
|
78
|
+
<p className="font-bold text-gray-900 truncate">{user.name}</p>
|
|
79
|
+
<p className="text-sm text-gray-400 truncate">{user.email}</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<nav className="flex flex-col gap-1">
|
|
84
|
+
{TABS.map(({ key, label, icon: Icon }) => (
|
|
85
|
+
<button
|
|
86
|
+
key={key}
|
|
87
|
+
onClick={() => setActiveTab(key)}
|
|
88
|
+
className={`flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-medium transition-all text-left
|
|
89
|
+
${activeTab === key ? 'bg-gray-900 text-white' : 'text-gray-600 hover:bg-gray-50'}`}
|
|
90
|
+
>
|
|
91
|
+
<Icon className="w-4 h-4" />
|
|
92
|
+
{label}
|
|
93
|
+
</button>
|
|
94
|
+
))}
|
|
95
|
+
</nav>
|
|
96
|
+
|
|
97
|
+
<div className="border-t border-gray-100 mt-5 pt-4">
|
|
98
|
+
<button
|
|
99
|
+
onClick={() => { logout(); router.push('/'); }}
|
|
100
|
+
className="w-full text-left px-4 py-2.5 text-sm text-red-500 hover:bg-red-50 rounded-xl transition-colors font-medium"
|
|
101
|
+
>
|
|
102
|
+
Logout
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</aside>
|
|
107
|
+
|
|
108
|
+
{/* Main Content */}
|
|
109
|
+
<div className="flex-1 min-w-0">
|
|
110
|
+
<AnimatePresence mode="wait">
|
|
111
|
+
{/* ── Profile Tab ── */}
|
|
112
|
+
{activeTab === 'profile' && (
|
|
113
|
+
<motion.div
|
|
114
|
+
key="profile"
|
|
115
|
+
initial={{ opacity: 0, x: 10 }}
|
|
116
|
+
animate={{ opacity: 1, x: 0 }}
|
|
117
|
+
exit={{ opacity: 0, x: -10 }}
|
|
118
|
+
transition={{ duration: 0.2 }}
|
|
119
|
+
>
|
|
120
|
+
<div className="bg-white rounded-2xl shadow-sm p-6">
|
|
121
|
+
<h2 className="font-bold text-gray-900 mb-5 text-lg">Edit Profile</h2>
|
|
122
|
+
<form onSubmit={handleSaveProfile} className="space-y-4 max-w-md">
|
|
123
|
+
<div>
|
|
124
|
+
<label htmlFor="profile-name" className="block text-sm font-medium text-gray-700 mb-1.5">
|
|
125
|
+
Full Name
|
|
126
|
+
</label>
|
|
127
|
+
<input
|
|
128
|
+
id="profile-name"
|
|
129
|
+
type="text"
|
|
130
|
+
value={name}
|
|
131
|
+
onChange={(e) => setName(e.target.value)}
|
|
132
|
+
className="input"
|
|
133
|
+
placeholder="Your name"
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
<div>
|
|
137
|
+
<label htmlFor="profile-email" className="block text-sm font-medium text-gray-700 mb-1.5">
|
|
138
|
+
Email
|
|
139
|
+
</label>
|
|
140
|
+
<input
|
|
141
|
+
id="profile-email"
|
|
142
|
+
type="email"
|
|
143
|
+
value={user.email}
|
|
144
|
+
className="input bg-gray-50 cursor-not-allowed"
|
|
145
|
+
disabled
|
|
146
|
+
/>
|
|
147
|
+
<p className="text-xs text-gray-400 mt-1">Email cannot be changed.</p>
|
|
148
|
+
</div>
|
|
149
|
+
<button
|
|
150
|
+
type="submit"
|
|
151
|
+
className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-semibold transition-all
|
|
152
|
+
${saved ? 'bg-green-500 text-white' : 'bg-gray-900 text-white hover:bg-gray-700'}`}
|
|
153
|
+
id="save-profile-btn"
|
|
154
|
+
>
|
|
155
|
+
{saved ? <><CheckCircle className="w-4 h-4" /> Saved!</> : <><Save className="w-4 h-4" /> Save Changes</>}
|
|
156
|
+
</button>
|
|
157
|
+
</form>
|
|
158
|
+
</div>
|
|
159
|
+
</motion.div>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{/* ── Orders Tab ── */}
|
|
163
|
+
{activeTab === 'orders' && (
|
|
164
|
+
<motion.div
|
|
165
|
+
key="orders"
|
|
166
|
+
initial={{ opacity: 0, x: 10 }}
|
|
167
|
+
animate={{ opacity: 1, x: 0 }}
|
|
168
|
+
exit={{ opacity: 0, x: -10 }}
|
|
169
|
+
transition={{ duration: 0.2 }}
|
|
170
|
+
>
|
|
171
|
+
<div className="space-y-4">
|
|
172
|
+
{orders.length === 0 ? (
|
|
173
|
+
<div className="bg-white rounded-2xl shadow-sm p-12 text-center">
|
|
174
|
+
<Package className="w-16 h-16 text-gray-200 mx-auto mb-4" />
|
|
175
|
+
<h3 className="font-bold text-gray-900 mb-2">No orders yet</h3>
|
|
176
|
+
<p className="text-gray-500 text-sm mb-5">Start shopping to see your orders here.</p>
|
|
177
|
+
<Link href="/products" className="btn-primary inline-block">Browse Products</Link>
|
|
178
|
+
</div>
|
|
179
|
+
) : (
|
|
180
|
+
orders.map((order) => (
|
|
181
|
+
<div key={order.id} className="bg-white rounded-2xl shadow-sm p-5">
|
|
182
|
+
<div className="flex items-center justify-between mb-4">
|
|
183
|
+
<div>
|
|
184
|
+
<p className="text-xs text-gray-400">Order</p>
|
|
185
|
+
<p className="font-mono font-semibold text-gray-900 text-sm">{order.id}</p>
|
|
186
|
+
</div>
|
|
187
|
+
<span className={`text-xs font-semibold px-3 py-1.5 rounded-full capitalize ${STATUS_COLORS[order.status]}`}>
|
|
188
|
+
{order.status}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Items preview */}
|
|
193
|
+
<div className="space-y-2 mb-4">
|
|
194
|
+
{order.items.slice(0, 2).map((item) => (
|
|
195
|
+
<div key={item.product.id} className="flex justify-between text-sm">
|
|
196
|
+
<span className="text-gray-600 truncate mr-2">{item.product.name} × {item.quantity}</span>
|
|
197
|
+
<span className="font-medium text-gray-900 flex-shrink-0">{formatPrice(item.product.price * item.quantity)}</span>
|
|
198
|
+
</div>
|
|
199
|
+
))}
|
|
200
|
+
{order.items.length > 2 && (
|
|
201
|
+
<p className="text-xs text-gray-400">+{order.items.length - 2} more items</p>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div className="flex items-center justify-between border-t border-gray-100 pt-3">
|
|
206
|
+
<div>
|
|
207
|
+
<span className="text-xs text-gray-400">{new Date(order.createdAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' })}</span>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="flex items-center gap-4">
|
|
210
|
+
<span className="font-bold text-gray-900">{formatPrice(order.total)}</span>
|
|
211
|
+
<Link href={`/orders/${order.id}`} className="text-sm font-semibold text-gray-700 hover:text-gray-900 underline">
|
|
212
|
+
View Details →
|
|
213
|
+
</Link>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
))
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
</motion.div>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
{/* ── Reviews Tab ── */}
|
|
224
|
+
{activeTab === 'reviews' && (
|
|
225
|
+
<motion.div
|
|
226
|
+
key="reviews"
|
|
227
|
+
initial={{ opacity: 0, x: 10 }}
|
|
228
|
+
animate={{ opacity: 1, x: 0 }}
|
|
229
|
+
exit={{ opacity: 0, x: -10 }}
|
|
230
|
+
transition={{ duration: 0.2 }}
|
|
231
|
+
>
|
|
232
|
+
<div className="space-y-4">
|
|
233
|
+
{reviews.length === 0 ? (
|
|
234
|
+
<div className="bg-white rounded-2xl shadow-sm p-12 text-center">
|
|
235
|
+
<Star className="w-16 h-16 text-gray-200 mx-auto mb-4" />
|
|
236
|
+
<h3 className="font-bold text-gray-900 mb-2">No reviews yet</h3>
|
|
237
|
+
<p className="text-gray-500 text-sm mb-5">Buy products and share your experience!</p>
|
|
238
|
+
<Link href="/products" className="btn-primary inline-block">Shop Now</Link>
|
|
239
|
+
</div>
|
|
240
|
+
) : (
|
|
241
|
+
reviews.map((review) => (
|
|
242
|
+
<div key={review.id} className="bg-white rounded-2xl shadow-sm p-5">
|
|
243
|
+
<div className="flex items-start justify-between">
|
|
244
|
+
<div>
|
|
245
|
+
<p className="font-semibold text-gray-900">{review.title}</p>
|
|
246
|
+
<div className="flex gap-0.5 mt-1 mb-2">
|
|
247
|
+
{[1,2,3,4,5].map((s) => (
|
|
248
|
+
<Star key={s} className={`w-3.5 h-3.5 ${s <= review.rating ? 'text-amber-400 fill-amber-400' : 'text-gray-200 fill-gray-200'}`} />
|
|
249
|
+
))}
|
|
250
|
+
</div>
|
|
251
|
+
<p className="text-sm text-gray-600">{review.body}</p>
|
|
252
|
+
</div>
|
|
253
|
+
<span className="text-xs text-gray-400 ml-4 flex-shrink-0">
|
|
254
|
+
{new Date(review.date).toLocaleDateString('en-IN', { day: 'numeric', month: 'short' })}
|
|
255
|
+
</span>
|
|
256
|
+
</div>
|
|
257
|
+
<div className="mt-3">
|
|
258
|
+
<Link href={`/product/${review.productId}`} className="text-xs text-gray-400 hover:text-gray-700 underline transition-colors">
|
|
259
|
+
View Product →
|
|
260
|
+
</Link>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
))
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
</motion.div>
|
|
267
|
+
)}
|
|
268
|
+
</AnimatePresence>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</motion.div>
|
|
272
|
+
);
|
|
273
|
+
}
|