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.
Files changed (69) hide show
  1. package/index.js +101 -0
  2. package/package.json +25 -0
  3. package/templates/ecommerce/app/about/page.tsx +152 -0
  4. package/templates/ecommerce/app/cart/page.tsx +103 -0
  5. package/templates/ecommerce/app/checkout/page.tsx +126 -0
  6. package/templates/ecommerce/app/contact/page.tsx +124 -0
  7. package/templates/ecommerce/app/globals.css +137 -0
  8. package/templates/ecommerce/app/layout.tsx +31 -0
  9. package/templates/ecommerce/app/login/page.tsx +224 -0
  10. package/templates/ecommerce/app/not-found.tsx +14 -0
  11. package/templates/ecommerce/app/orders/[id]/page.tsx +138 -0
  12. package/templates/ecommerce/app/page.tsx +215 -0
  13. package/templates/ecommerce/app/product/[id]/page.tsx +417 -0
  14. package/templates/ecommerce/app/products/page.tsx +244 -0
  15. package/templates/ecommerce/app/profile/page.tsx +273 -0
  16. package/templates/ecommerce/app/register/page.tsx +221 -0
  17. package/templates/ecommerce/app/wishlist/page.tsx +89 -0
  18. package/templates/ecommerce/components/CartItem.tsx +74 -0
  19. package/templates/ecommerce/components/CheckoutForm.tsx +109 -0
  20. package/templates/ecommerce/components/Filters.tsx +142 -0
  21. package/templates/ecommerce/components/Footer.tsx +100 -0
  22. package/templates/ecommerce/components/Header.tsx +194 -0
  23. package/templates/ecommerce/components/ProductCard.tsx +158 -0
  24. package/templates/ecommerce/components/SearchBar.tsx +41 -0
  25. package/templates/ecommerce/data/products.json +427 -0
  26. package/templates/ecommerce/lib/localStorage.ts +27 -0
  27. package/templates/ecommerce/lib/utils.ts +25 -0
  28. package/templates/ecommerce/next-env.d.ts +5 -0
  29. package/templates/ecommerce/next.config.js +13 -0
  30. package/templates/ecommerce/next.config.ts +14 -0
  31. package/templates/ecommerce/package.json +30 -0
  32. package/templates/ecommerce/postcss.config.js +7 -0
  33. package/templates/ecommerce/public/images/about-2.jpg +0 -0
  34. package/templates/ecommerce/public/images/contact-bg.jpg +0 -0
  35. package/templates/ecommerce/public/images/hero-bg.jpg +0 -0
  36. package/templates/ecommerce/public/images/products/product-1.jpg +0 -0
  37. package/templates/ecommerce/public/images/products/product-10.jpg +0 -0
  38. package/templates/ecommerce/public/images/products/product-11.jpg +0 -0
  39. package/templates/ecommerce/public/images/products/product-12.jpg +0 -0
  40. package/templates/ecommerce/public/images/products/product-13.jpg +0 -0
  41. package/templates/ecommerce/public/images/products/product-14.jpg +0 -0
  42. package/templates/ecommerce/public/images/products/product-15.jpg +0 -0
  43. package/templates/ecommerce/public/images/products/product-16.jpg +0 -0
  44. package/templates/ecommerce/public/images/products/product-17.jpg +0 -0
  45. package/templates/ecommerce/public/images/products/product-18.jpg +0 -0
  46. package/templates/ecommerce/public/images/products/product-19.jpg +0 -0
  47. package/templates/ecommerce/public/images/products/product-2.jpg +0 -0
  48. package/templates/ecommerce/public/images/products/product-20.jpg +0 -0
  49. package/templates/ecommerce/public/images/products/product-21.jpg +0 -0
  50. package/templates/ecommerce/public/images/products/product-22.jpg +0 -0
  51. package/templates/ecommerce/public/images/products/product-23.jpg +0 -0
  52. package/templates/ecommerce/public/images/products/product-24.jpg +0 -0
  53. package/templates/ecommerce/public/images/products/product-25.jpg +0 -0
  54. package/templates/ecommerce/public/images/products/product-3.jpg +0 -0
  55. package/templates/ecommerce/public/images/products/product-4.jpg +0 -0
  56. package/templates/ecommerce/public/images/products/product-5.jpg +0 -0
  57. package/templates/ecommerce/public/images/products/product-6.jpg +0 -0
  58. package/templates/ecommerce/public/images/products/product-7.jpg +0 -0
  59. package/templates/ecommerce/public/images/products/product-8.jpg +0 -0
  60. package/templates/ecommerce/public/images/products/product-9.jpg +0 -0
  61. package/templates/ecommerce/public/service-worker.js +1 -0
  62. package/templates/ecommerce/store/useAuthStore.ts +56 -0
  63. package/templates/ecommerce/store/useCartStore.ts +63 -0
  64. package/templates/ecommerce/store/useOrderStore.ts +43 -0
  65. package/templates/ecommerce/store/useReviewStore.ts +60 -0
  66. package/templates/ecommerce/store/useWishlistStore.ts +43 -0
  67. package/templates/ecommerce/tailwind.config.ts +44 -0
  68. package/templates/ecommerce/tsconfig.json +22 -0
  69. package/templates/ecommerce/types/index.ts +69 -0
@@ -0,0 +1,100 @@
1
+ import Link from 'next/link';
2
+ import { Zap, Instagram, Twitter, Youtube, Github } from 'lucide-react';
3
+
4
+ const SOCIAL = [
5
+ { icon: Instagram, href: '#', label: 'Instagram' },
6
+ { icon: Twitter, href: '#', label: 'Twitter' },
7
+ { icon: Youtube, href: '#', label: 'YouTube' },
8
+ ];
9
+
10
+ export default function Footer() {
11
+ return (
12
+ <footer className="bg-gray-900 text-gray-300 mt-20">
13
+ <div className="container-main py-14">
14
+ <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-10">
15
+ {/* Brand */}
16
+ <div className="col-span-1 sm:col-span-2 md:col-span-1">
17
+ <div className="flex items-center gap-2 mb-3">
18
+ <div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center">
19
+ <Zap className="w-4 h-4 text-gray-900" />
20
+ </div>
21
+ <span className="text-xl font-black text-white tracking-tight">GenZ Shop</span>
22
+ </div>
23
+ <p className="text-sm text-gray-400 leading-relaxed mb-5">
24
+ Shop Different. Shop Smarter. Curated fashion and lifestyle for the next generation.
25
+ </p>
26
+ <div className="flex gap-3">
27
+ {SOCIAL.map(({ icon: Icon, href, label }) => (
28
+ <a
29
+ key={label}
30
+ href={href}
31
+ aria-label={label}
32
+ className="w-9 h-9 bg-gray-800 rounded-xl flex items-center justify-center text-gray-400 hover:bg-gray-700 hover:text-white transition-all"
33
+ >
34
+ <Icon className="w-4 h-4" />
35
+ </a>
36
+ ))}
37
+ </div>
38
+ </div>
39
+
40
+ {/* Shop */}
41
+ <div>
42
+ <h4 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">Shop</h4>
43
+ <ul className="space-y-2.5">
44
+ {[
45
+ { label: 'All Products', href: '/products' },
46
+ { label: 'New Arrivals', href: '/products' },
47
+ { label: 'Best Sellers', href: '/products' },
48
+ { label: 'Sale', href: '/products' },
49
+ ].map(({ label, href }) => (
50
+ <li key={label}>
51
+ <Link href={href} className="text-sm text-gray-400 hover:text-white transition-colors">{label}</Link>
52
+ </li>
53
+ ))}
54
+ </ul>
55
+ </div>
56
+
57
+ {/* Account */}
58
+ <div>
59
+ <h4 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">Account</h4>
60
+ <ul className="space-y-2.5">
61
+ {[
62
+ { label: 'Login', href: '/login' },
63
+ { label: 'Register', href: '/register' },
64
+ { label: 'My Profile', href: '/profile' },
65
+ { label: 'My Orders', href: '/profile?tab=orders' },
66
+ { label: 'Wishlist', href: '/wishlist' },
67
+ ].map(({ label, href }) => (
68
+ <li key={label}>
69
+ <Link href={href} className="text-sm text-gray-400 hover:text-white transition-colors">{label}</Link>
70
+ </li>
71
+ ))}
72
+ </ul>
73
+ </div>
74
+
75
+ {/* Company */}
76
+ <div>
77
+ <h4 className="text-white font-semibold mb-4 text-sm uppercase tracking-wider">Company</h4>
78
+ <ul className="space-y-2.5">
79
+ {[
80
+ { label: 'About Us', href: '/about' },
81
+ { label: 'Contact', href: '/contact' },
82
+ { label: 'Privacy Policy', href: '#' },
83
+ { label: 'Terms of Service', href: '#' },
84
+ ].map(({ label, href }) => (
85
+ <li key={label}>
86
+ <Link href={href} className="text-sm text-gray-400 hover:text-white transition-colors">{label}</Link>
87
+ </li>
88
+ ))}
89
+ </ul>
90
+ </div>
91
+ </div>
92
+
93
+ <div className="border-t border-gray-800 mt-12 pt-6 flex flex-col sm:flex-row items-center justify-between gap-4">
94
+ <p className="text-sm text-gray-500">© 2024 GenZ Shop. All rights reserved.</p>
95
+ <p className="text-sm text-gray-600">Built with Next.js + Tailwind CSS</p>
96
+ </div>
97
+ </div>
98
+ </footer>
99
+ );
100
+ }
@@ -0,0 +1,194 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useState } from 'react';
5
+ import { motion, AnimatePresence } from 'framer-motion';
6
+ import { ShoppingCart, Heart, User, Menu, X, LogOut, ChevronDown, Zap } from 'lucide-react';
7
+ import { useCartStore } from '@/store/useCartStore';
8
+ import { useWishlistStore } from '@/store/useWishlistStore';
9
+ import { useAuthStore } from '@/store/useAuthStore';
10
+ import { useRouter, usePathname } from 'next/navigation';
11
+
12
+ const NAV_LINKS = [
13
+ { href: '/', label: 'Home' },
14
+ { href: '/products', label: 'Products' },
15
+ { href: '/about', label: 'About' },
16
+ { href: '/contact', label: 'Contact' },
17
+ ];
18
+
19
+ export default function Header() {
20
+ const [drawerOpen, setDrawerOpen] = useState(false);
21
+ const [userMenuOpen, setUserMenuOpen] = useState(false);
22
+ const cartCount = useCartStore((s) => s.itemCount)();
23
+ const wishlistCount = useWishlistStore((s) => s.itemCount)();
24
+ const { user, isAuthenticated, logout } = useAuthStore();
25
+ const router = useRouter();
26
+ const pathname = usePathname();
27
+
28
+ const handleLogout = () => {
29
+ logout();
30
+ setUserMenuOpen(false);
31
+ router.push('/');
32
+ };
33
+
34
+ return (
35
+ <>
36
+ <header className="sticky top-0 z-50 bg-white/90 backdrop-blur-md border-b border-gray-100 shadow-sm">
37
+ <div className="container-main">
38
+ <div className="flex items-center justify-between h-16">
39
+ {/* Logo */}
40
+ <Link href="/" className="flex items-center gap-2 group">
41
+ <div className="w-8 h-8 bg-gray-900 rounded-lg flex items-center justify-center group-hover:bg-gray-700 transition-colors">
42
+ <Zap className="w-4 h-4 text-white" />
43
+ </div>
44
+ <span className="text-xl font-black text-gray-900 tracking-tight">GenZ Shop</span>
45
+ </Link>
46
+
47
+ {/* Desktop Nav */}
48
+ <nav className="hidden md:flex items-center gap-1">
49
+ {NAV_LINKS.map(({ href, label }) => {
50
+ const isActive = pathname === href || (href !== '/' && pathname.startsWith(href));
51
+ return (
52
+ <Link
53
+ key={href}
54
+ href={href}
55
+ className="relative px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
56
+ >
57
+ {label}
58
+ {isActive && (
59
+ <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-gray-900 rounded-full" />
60
+ )}
61
+ </Link>
62
+ );
63
+ })}
64
+ </nav>
65
+
66
+ {/* Right Icons */}
67
+ <div className="flex items-center gap-1">
68
+ <Link href="/wishlist" className="relative p-2.5 text-gray-500 hover:text-gray-900 hover:bg-gray-50 rounded-xl transition-all" aria-label="Wishlist">
69
+ <Heart className="w-5 h-5" />
70
+ <AnimatePresence>
71
+ {wishlistCount > 0 && (
72
+ <motion.span key="wb" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} className="absolute -top-0.5 -right-0.5 badge">
73
+ {wishlistCount}
74
+ </motion.span>
75
+ )}
76
+ </AnimatePresence>
77
+ </Link>
78
+
79
+ <Link href="/cart" className="relative p-2.5 text-gray-500 hover:text-gray-900 hover:bg-gray-50 rounded-xl transition-all" aria-label="Cart">
80
+ <ShoppingCart className="w-5 h-5" />
81
+ <AnimatePresence>
82
+ {cartCount > 0 && (
83
+ <motion.span key="cb" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} className="absolute -top-0.5 -right-0.5 badge">
84
+ {cartCount}
85
+ </motion.span>
86
+ )}
87
+ </AnimatePresence>
88
+ </Link>
89
+
90
+ {isAuthenticated && user ? (
91
+ <div className="relative">
92
+ <button onClick={() => setUserMenuOpen(!userMenuOpen)} className="flex items-center gap-2 px-3 py-2 text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-xl transition-all">
93
+ <div className="w-7 h-7 bg-gray-900 rounded-full flex items-center justify-center">
94
+ <span className="text-white text-xs font-bold">{user.name.charAt(0).toUpperCase()}</span>
95
+ </div>
96
+ <span className="hidden sm:block text-sm font-medium">{user.name.split(' ')[0]}</span>
97
+ <ChevronDown className={`w-3.5 h-3.5 transition-transform ${userMenuOpen ? 'rotate-180' : ''}`} />
98
+ </button>
99
+ <AnimatePresence>
100
+ {userMenuOpen && (
101
+ <motion.div
102
+ initial={{ opacity: 0, y: -8, scale: 0.95 }}
103
+ animate={{ opacity: 1, y: 0, scale: 1 }}
104
+ exit={{ opacity: 0, y: -8, scale: 0.95 }}
105
+ transition={{ duration: 0.15 }}
106
+ className="absolute right-0 mt-1 w-48 bg-white border border-gray-100 rounded-2xl shadow-xl overflow-hidden z-50"
107
+ >
108
+ <div className="px-4 py-3 border-b border-gray-100">
109
+ <p className="text-sm font-semibold text-gray-900">{user.name}</p>
110
+ <p className="text-xs text-gray-400 truncate">{user.email}</p>
111
+ </div>
112
+ <Link href="/profile" onClick={() => setUserMenuOpen(false)} className="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-600 hover:bg-gray-50 transition-colors">
113
+ <User className="w-4 h-4" /> My Profile
114
+ </Link>
115
+ <button onClick={handleLogout} className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-red-500 hover:bg-red-50 transition-colors">
116
+ <LogOut className="w-4 h-4" /> Logout
117
+ </button>
118
+ </motion.div>
119
+ )}
120
+ </AnimatePresence>
121
+ </div>
122
+ ) : (
123
+ <Link href="/login" className="hidden sm:block btn-primary text-sm py-2">Login</Link>
124
+ )}
125
+
126
+ <button className="md:hidden p-2.5 text-gray-500 hover:bg-gray-50 rounded-xl" onClick={() => setDrawerOpen(true)} aria-label="Open menu">
127
+ <Menu className="w-5 h-5" />
128
+ </button>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </header>
133
+
134
+ {/* Mobile Drawer */}
135
+ <AnimatePresence>
136
+ {drawerOpen && (
137
+ <>
138
+ <motion.div key="overlay" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 bg-black/40 z-50 md:hidden" onClick={() => setDrawerOpen(false)} />
139
+ <motion.div
140
+ key="drawer"
141
+ initial={{ x: '100%' }} animate={{ x: 0 }} exit={{ x: '100%' }}
142
+ transition={{ type: 'spring', damping: 25, stiffness: 200 }}
143
+ className="fixed right-0 top-0 h-full w-72 bg-white z-50 shadow-2xl flex flex-col md:hidden"
144
+ >
145
+ <div className="flex items-center justify-between p-5 border-b border-gray-100">
146
+ <div className="flex items-center gap-2">
147
+ <div className="w-7 h-7 bg-gray-900 rounded-lg flex items-center justify-center">
148
+ <Zap className="w-3.5 h-3.5 text-white" />
149
+ </div>
150
+ <span className="font-black text-gray-900">GenZ Shop</span>
151
+ </div>
152
+ <button onClick={() => setDrawerOpen(false)} className="p-2 text-gray-400 hover:text-gray-900 hover:bg-gray-100 rounded-lg">
153
+ <X className="w-5 h-5" />
154
+ </button>
155
+ </div>
156
+ <nav className="flex flex-col p-5 gap-1 flex-1">
157
+ {NAV_LINKS.map(({ href, label }, i) => (
158
+ <motion.div key={href} initial={{ x: 20, opacity: 0 }} animate={{ x: 0, opacity: 1 }} transition={{ delay: i * 0.05 }}>
159
+ <Link href={href} onClick={() => setDrawerOpen(false)} className={`block px-4 py-3 rounded-xl text-sm font-medium transition-colors ${pathname === href ? 'bg-gray-900 text-white' : 'text-gray-700 hover:bg-gray-50'}`}>
160
+ {label}
161
+ </Link>
162
+ </motion.div>
163
+ ))}
164
+ </nav>
165
+ <div className="p-5 border-t border-gray-100">
166
+ {isAuthenticated && user ? (
167
+ <div className="space-y-2">
168
+ <div className="flex items-center gap-3 px-4 py-3 bg-gray-50 rounded-xl">
169
+ <div className="w-9 h-9 bg-gray-900 rounded-full flex items-center justify-center">
170
+ <span className="text-white text-sm font-bold">{user.name.charAt(0).toUpperCase()}</span>
171
+ </div>
172
+ <div>
173
+ <p className="text-sm font-semibold text-gray-900">{user.name}</p>
174
+ <p className="text-xs text-gray-400">{user.email}</p>
175
+ </div>
176
+ </div>
177
+ <Link href="/profile" onClick={() => setDrawerOpen(false)} className="flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm text-gray-700 hover:bg-gray-50 transition-colors">
178
+ <User className="w-4 h-4" /> My Profile
179
+ </Link>
180
+ <button onClick={handleLogout} className="w-full flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm text-red-500 hover:bg-red-50 transition-colors">
181
+ <LogOut className="w-4 h-4" /> Logout
182
+ </button>
183
+ </div>
184
+ ) : (
185
+ <Link href="/login" onClick={() => setDrawerOpen(false)} className="btn-primary w-full text-center block">Login</Link>
186
+ )}
187
+ </div>
188
+ </motion.div>
189
+ </>
190
+ )}
191
+ </AnimatePresence>
192
+ </>
193
+ );
194
+ }
@@ -0,0 +1,158 @@
1
+ 'use client';
2
+
3
+ import Image from 'next/image';
4
+ import Link from 'next/link';
5
+ import { motion } from 'framer-motion';
6
+ import { ShoppingCart, Heart, Star } from 'lucide-react';
7
+ import { useCartStore } from '@/store/useCartStore';
8
+ import { useWishlistStore } from '@/store/useWishlistStore';
9
+ import type { Product } from '@/types';
10
+ import { formatPrice, getDiscount } from '@/lib/utils';
11
+ import { useState } from 'react';
12
+
13
+ interface ProductCardProps {
14
+ product: Product;
15
+ }
16
+
17
+ export default function ProductCard({ product }: ProductCardProps) {
18
+ const addItem = useCartStore((s) => s.addItem);
19
+ const { toggle, isWishlisted } = useWishlistStore();
20
+ const wishlisted = isWishlisted(product.id);
21
+ const [added, setAdded] = useState(false);
22
+
23
+ const handleAddToCart = (e: React.MouseEvent) => {
24
+ e.preventDefault();
25
+ if (!product.inStock) return;
26
+ addItem(product);
27
+ setAdded(true);
28
+ setTimeout(() => setAdded(false), 1500);
29
+ };
30
+
31
+ const handleWishlist = (e: React.MouseEvent) => {
32
+ e.preventDefault();
33
+ toggle(product);
34
+ };
35
+
36
+ const discount = product.originalPrice
37
+ ? getDiscount(product.originalPrice, product.price)
38
+ : null;
39
+
40
+ return (
41
+ <motion.div
42
+ whileHover={{ y: -4 }}
43
+ transition={{ type: 'spring', stiffness: 300, damping: 20 }}
44
+ >
45
+ <Link href={`/product/${product.id}`} className="group block">
46
+ <div className="card overflow-hidden">
47
+ {/* Image Container */}
48
+ <div className="relative aspect-square overflow-hidden bg-gray-100">
49
+ <Image
50
+ src={product.image}
51
+ alt={product.name}
52
+ fill
53
+ className="object-cover group-hover:scale-105 transition-transform duration-500"
54
+ sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
55
+ />
56
+
57
+ {/* Hover overlay with quick-add */}
58
+ <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300 flex items-end justify-center pb-4">
59
+ <motion.button
60
+ onClick={handleAddToCart}
61
+ disabled={!product.inStock}
62
+ initial={{ opacity: 0, y: 10 }}
63
+ whileHover={{ scale: 1.02 }}
64
+ className={`opacity-0 group-hover:opacity-100 transition-all duration-300 px-5 py-2 rounded-full text-sm font-semibold shadow-lg flex items-center gap-2
65
+ ${added
66
+ ? 'bg-green-500 text-white'
67
+ : product.inStock
68
+ ? 'bg-white text-gray-900 hover:bg-gray-100'
69
+ : 'bg-gray-200 text-gray-500 cursor-not-allowed'
70
+ }`}
71
+ >
72
+ <ShoppingCart className="w-4 h-4" />
73
+ {added ? 'Added!' : 'Quick Add'}
74
+ </motion.button>
75
+ </div>
76
+
77
+ {/* Discount badge */}
78
+ {discount && (
79
+ <div className="absolute top-3 left-3 bg-red-500 text-white text-xs font-bold px-2 py-1 rounded-full">
80
+ -{discount}%
81
+ </div>
82
+ )}
83
+
84
+ {/* Out of stock */}
85
+ {!product.inStock && (
86
+ <div className="absolute inset-0 bg-white/60 flex items-center justify-center">
87
+ <span className="bg-gray-800 text-white text-xs font-semibold px-3 py-1.5 rounded-full">
88
+ Out of Stock
89
+ </span>
90
+ </div>
91
+ )}
92
+
93
+ {/* Wishlist button */}
94
+ <motion.button
95
+ onClick={handleWishlist}
96
+ whileHover={{ scale: 1.15 }}
97
+ whileTap={{ scale: 0.9 }}
98
+ className="absolute top-3 right-3 w-9 h-9 bg-white rounded-full shadow-md flex items-center justify-center"
99
+ aria-label={wishlisted ? 'Remove from wishlist' : 'Add to wishlist'}
100
+ >
101
+ <Heart
102
+ className={`w-4 h-4 transition-colors ${wishlisted ? 'text-red-500 fill-red-500' : 'text-gray-400'}`}
103
+ />
104
+ </motion.button>
105
+ </div>
106
+
107
+ {/* Content */}
108
+ <div className="p-4">
109
+ <p className="text-xs text-gray-400 uppercase tracking-wider mb-1">{product.category}</p>
110
+ <h3 className="font-semibold text-gray-900 text-sm mb-2 line-clamp-2 group-hover:text-gray-700 transition-colors">
111
+ {product.name}
112
+ </h3>
113
+
114
+ {/* Rating */}
115
+ <div className="flex items-center gap-1 mb-3">
116
+ <div className="flex">
117
+ {[1, 2, 3, 4, 5].map((star) => (
118
+ <Star
119
+ key={star}
120
+ className={`w-3 h-3 ${star <= Math.floor(product.rating) ? 'text-amber-400 fill-amber-400' : 'text-gray-200 fill-gray-200'}`}
121
+ />
122
+ ))}
123
+ </div>
124
+ <span className="text-xs text-gray-400">({product.reviewCount})</span>
125
+ </div>
126
+
127
+ {/* Price + Cart Button */}
128
+ <div className="flex items-center justify-between gap-2">
129
+ <div>
130
+ <span className="font-bold text-gray-900">{formatPrice(product.price)}</span>
131
+ {product.originalPrice && (
132
+ <span className="text-xs text-gray-400 line-through ml-1.5">
133
+ {formatPrice(product.originalPrice)}
134
+ </span>
135
+ )}
136
+ </div>
137
+ <motion.button
138
+ onClick={handleAddToCart}
139
+ disabled={!product.inStock}
140
+ whileTap={{ scale: 0.92 }}
141
+ className={`flex-shrink-0 p-2 rounded-lg transition-colors duration-200
142
+ ${added
143
+ ? 'bg-green-500 text-white'
144
+ : product.inStock
145
+ ? 'bg-gray-900 text-white hover:bg-gray-700'
146
+ : 'bg-gray-100 text-gray-400 cursor-not-allowed'
147
+ }`}
148
+ aria-label="Add to cart"
149
+ >
150
+ <ShoppingCart className="w-4 h-4" />
151
+ </motion.button>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </Link>
156
+ </motion.div>
157
+ );
158
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { Search, X } from 'lucide-react';
4
+
5
+ interface SearchBarProps {
6
+ value: string;
7
+ onChange: (value: string) => void;
8
+ placeholder?: string;
9
+ }
10
+
11
+ export default function SearchBar({
12
+ value,
13
+ onChange,
14
+ placeholder = 'Search products...',
15
+ }: SearchBarProps) {
16
+ return (
17
+ <div className="relative">
18
+ <div className="absolute inset-y-0 left-3.5 flex items-center pointer-events-none">
19
+ <Search className="w-4 h-4 text-gray-400" />
20
+ </div>
21
+ <input
22
+ type="text"
23
+ value={value}
24
+ onChange={(e) => onChange(e.target.value)}
25
+ placeholder={placeholder}
26
+ className="input pl-10 pr-10"
27
+ id="product-search"
28
+ aria-label="Search products"
29
+ />
30
+ {value && (
31
+ <button
32
+ onClick={() => onChange('')}
33
+ className="absolute inset-y-0 right-3 flex items-center text-gray-400 hover:text-gray-700 transition-colors"
34
+ aria-label="Clear search"
35
+ >
36
+ <X className="w-4 h-4" />
37
+ </button>
38
+ )}
39
+ </div>
40
+ );
41
+ }