reroute-js 0.2.1 → 0.2.3

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 (188) hide show
  1. package/_/README.md +59 -0
  2. package/_/basic/package.json +23 -0
  3. package/_/basic/src/client/App.tsx +10 -0
  4. package/_/basic/src/client/components/Counter.tsx +15 -0
  5. package/_/basic/src/client/index.html +12 -0
  6. package/_/basic/src/client/index.tsx +5 -0
  7. package/_/basic/src/client/routes/[404].tsx +18 -0
  8. package/_/basic/src/client/routes/about.tsx +25 -0
  9. package/_/basic/src/client/routes/index.tsx +57 -0
  10. package/_/basic/src/index.ts +20 -0
  11. package/_/basic/tsconfig.json +26 -0
  12. package/_/blog/package.json +23 -0
  13. package/_/blog/src/client/App.tsx +10 -0
  14. package/_/blog/src/client/components/Counter.tsx +14 -0
  15. package/_/blog/src/client/components/RecentPosts.tsx +90 -0
  16. package/_/blog/src/client/index.html +13 -0
  17. package/_/blog/src/client/index.tsx +5 -0
  18. package/_/blog/src/client/routes/[404].tsx +21 -0
  19. package/_/blog/src/client/routes/about.tsx +31 -0
  20. package/_/blog/src/client/routes/blog/[404].tsx +21 -0
  21. package/_/blog/src/client/routes/blog/[layout].tsx +84 -0
  22. package/_/blog/src/client/routes/blog/[slug].tsx +11 -0
  23. package/_/blog/src/client/routes/blog/content/1-hello-world.tsx +27 -0
  24. package/_/blog/src/client/routes/blog/content/2-what-is-reroute.tsx +31 -0
  25. package/_/blog/src/client/routes/blog/index.tsx +70 -0
  26. package/_/blog/src/client/routes/index.tsx +63 -0
  27. package/_/blog/src/index.ts +20 -0
  28. package/_/blog/tsconfig.json +26 -0
  29. package/_/store/package.json +25 -0
  30. package/_/store/src/client/App.tsx +17 -0
  31. package/_/store/src/client/components/Header.tsx +40 -0
  32. package/_/store/src/client/components/ProductCard.tsx +51 -0
  33. package/_/store/src/client/index.html +17 -0
  34. package/_/store/src/client/index.tsx +7 -0
  35. package/_/store/src/client/lib/api.ts +153 -0
  36. package/_/store/src/client/routes/[404].tsx +63 -0
  37. package/_/store/src/client/routes/categories/[category].tsx +223 -0
  38. package/_/store/src/client/routes/categories/index.tsx +187 -0
  39. package/_/store/src/client/routes/index.tsx +126 -0
  40. package/_/store/src/client/routes/products/[id].tsx +233 -0
  41. package/_/store/src/client/routes/products/index.tsx +261 -0
  42. package/_/store/src/client/theme.css +306 -0
  43. package/_/store/src/index.ts +19 -0
  44. package/_/store/tsconfig.json +26 -0
  45. package/{packages/cli/bin.ts → cli/bin.d.ts} +1 -1
  46. package/cli/bin.d.ts.map +1 -0
  47. package/cli/bin.js +878 -0
  48. package/cli/bin.js.map +15 -0
  49. package/cli/index.d.ts +2 -0
  50. package/cli/index.d.ts.map +1 -0
  51. package/cli/index.js +147 -0
  52. package/cli/index.js.map +10 -0
  53. package/cli/src/cli.d.ts +8 -0
  54. package/cli/src/cli.d.ts.map +1 -0
  55. package/cli/src/commands/build.d.ts +8 -0
  56. package/cli/src/commands/build.d.ts.map +1 -0
  57. package/cli/src/commands/dev.d.ts +8 -0
  58. package/cli/src/commands/dev.d.ts.map +1 -0
  59. package/cli/src/commands/gen.d.ts +3 -0
  60. package/cli/src/commands/gen.d.ts.map +1 -0
  61. package/cli/src/commands/init.d.ts +8 -0
  62. package/cli/src/commands/init.d.ts.map +1 -0
  63. package/cli/src/libs/index.d.ts +2 -0
  64. package/cli/src/libs/index.d.ts.map +1 -0
  65. package/cli/src/libs/tailwind.d.ts +45 -0
  66. package/cli/src/libs/tailwind.d.ts.map +1 -0
  67. package/core/index.d.ts +2 -0
  68. package/core/index.d.ts.map +1 -0
  69. package/core/index.js +1117 -0
  70. package/core/index.js.map +25 -0
  71. package/core/src/bundler/hash.d.ts +2 -0
  72. package/core/src/bundler/hash.d.ts.map +1 -0
  73. package/core/src/bundler/index.d.ts +3 -0
  74. package/core/src/bundler/index.d.ts.map +1 -0
  75. package/core/src/bundler/transpile.d.ts +4 -0
  76. package/core/src/bundler/transpile.d.ts.map +1 -0
  77. package/core/src/content/discovery.d.ts +5 -0
  78. package/core/src/content/discovery.d.ts.map +1 -0
  79. package/core/src/content/index.d.ts +4 -0
  80. package/core/src/content/index.d.ts.map +1 -0
  81. package/core/src/content/metadata.d.ts +9 -0
  82. package/core/src/content/metadata.d.ts.map +1 -0
  83. package/core/src/content/registry.d.ts +2 -0
  84. package/core/src/content/registry.d.ts.map +1 -0
  85. package/core/src/index.d.ts +7 -0
  86. package/core/src/index.d.ts.map +1 -0
  87. package/core/src/ssr/data.d.ts +9 -0
  88. package/core/src/ssr/data.d.ts.map +1 -0
  89. package/core/src/ssr/index.d.ts +4 -0
  90. package/core/src/ssr/index.d.ts.map +1 -0
  91. package/core/src/ssr/modules.d.ts +8 -0
  92. package/core/src/ssr/modules.d.ts.map +1 -0
  93. package/core/src/ssr/render.d.ts +20 -0
  94. package/core/src/ssr/render.d.ts.map +1 -0
  95. package/core/src/ssr/seed.d.ts +2 -0
  96. package/core/src/ssr/seed.d.ts.map +1 -0
  97. package/core/src/template/html.d.ts +4 -0
  98. package/core/src/template/html.d.ts.map +1 -0
  99. package/core/src/template/index.d.ts +2 -0
  100. package/core/src/template/index.d.ts.map +1 -0
  101. package/core/src/types.d.ts +50 -0
  102. package/core/src/types.d.ts.map +1 -0
  103. package/core/src/utils/cache.d.ts +12 -0
  104. package/core/src/utils/cache.d.ts.map +1 -0
  105. package/core/src/utils/compression.d.ts +5 -0
  106. package/core/src/utils/compression.d.ts.map +1 -0
  107. package/core/src/utils/index.d.ts +5 -0
  108. package/core/src/utils/index.d.ts.map +1 -0
  109. package/core/src/utils/mime.d.ts +3 -0
  110. package/core/src/utils/mime.d.ts.map +1 -0
  111. package/core/src/utils/path.d.ts +6 -0
  112. package/core/src/utils/path.d.ts.map +1 -0
  113. package/elysia/index.d.ts +2 -0
  114. package/elysia/index.d.ts.map +1 -0
  115. package/elysia/index.js +1780 -0
  116. package/elysia/index.js.map +32 -0
  117. package/elysia/src/index.d.ts +3 -0
  118. package/elysia/src/index.d.ts.map +1 -0
  119. package/elysia/src/plugin.d.ts +32 -0
  120. package/elysia/src/plugin.d.ts.map +1 -0
  121. package/elysia/src/routes/artifacts.d.ts +3 -0
  122. package/elysia/src/routes/artifacts.d.ts.map +1 -0
  123. package/elysia/src/routes/content.d.ts +3 -0
  124. package/elysia/src/routes/content.d.ts.map +1 -0
  125. package/elysia/src/routes/dev.d.ts +7 -0
  126. package/elysia/src/routes/dev.d.ts.map +1 -0
  127. package/elysia/src/routes/ssr.d.ts +21 -0
  128. package/elysia/src/routes/ssr.d.ts.map +1 -0
  129. package/elysia/src/routes/static.d.ts +19 -0
  130. package/elysia/src/routes/static.d.ts.map +1 -0
  131. package/elysia/src/types.d.ts +31 -0
  132. package/elysia/src/types.d.ts.map +1 -0
  133. package/elysia/src/utils/http.d.ts +5 -0
  134. package/elysia/src/utils/http.d.ts.map +1 -0
  135. package/package.json +3 -3
  136. package/react/index.d.ts +2 -0
  137. package/react/index.d.ts.map +1 -0
  138. package/react/index.js +1152 -0
  139. package/react/index.js.map +23 -0
  140. package/react/src/components/ContentRoute.d.ts +13 -0
  141. package/react/src/components/ContentRoute.d.ts.map +1 -0
  142. package/react/src/components/Link.d.ts +8 -0
  143. package/react/src/components/Link.d.ts.map +1 -0
  144. package/react/src/components/Outlet.d.ts +7 -0
  145. package/react/src/components/Outlet.d.ts.map +1 -0
  146. package/react/src/components/index.d.ts +4 -0
  147. package/react/src/components/index.d.ts.map +1 -0
  148. package/react/src/hooks/index.d.ts +7 -0
  149. package/react/src/hooks/index.d.ts.map +1 -0
  150. package/react/src/hooks/useContent.d.ts +26 -0
  151. package/react/src/hooks/useContent.d.ts.map +1 -0
  152. package/react/src/hooks/useData.d.ts +10 -0
  153. package/react/src/hooks/useData.d.ts.map +1 -0
  154. package/react/src/hooks/useNavigate.d.ts +6 -0
  155. package/react/src/hooks/useNavigate.d.ts.map +1 -0
  156. package/react/src/hooks/useParams.d.ts +6 -0
  157. package/react/src/hooks/useParams.d.ts.map +1 -0
  158. package/react/src/hooks/useRouter.d.ts +7 -0
  159. package/react/src/hooks/useRouter.d.ts.map +1 -0
  160. package/react/src/hooks/useSearchParams.d.ts +6 -0
  161. package/react/src/hooks/useSearchParams.d.ts.map +1 -0
  162. package/react/src/index.d.ts +6 -0
  163. package/react/src/index.d.ts.map +1 -0
  164. package/react/src/providers/ContentProvider.d.ts +35 -0
  165. package/react/src/providers/ContentProvider.d.ts.map +1 -0
  166. package/react/src/providers/RerouteProvider.d.ts +25 -0
  167. package/react/src/providers/RerouteProvider.d.ts.map +1 -0
  168. package/react/src/providers/RouterProvider.d.ts +23 -0
  169. package/react/src/providers/RouterProvider.d.ts.map +1 -0
  170. package/react/src/providers/index.d.ts +4 -0
  171. package/react/src/providers/index.d.ts.map +1 -0
  172. package/react/src/types/any.d.ts +3 -0
  173. package/react/src/types/any.d.ts.map +1 -0
  174. package/react/src/types/index.d.ts +3 -0
  175. package/react/src/types/index.d.ts.map +1 -0
  176. package/react/src/types/router.d.ts +32 -0
  177. package/react/src/types/router.d.ts.map +1 -0
  178. package/react/src/utils/content.d.ts +8 -0
  179. package/react/src/utils/content.d.ts.map +1 -0
  180. package/react/src/utils/head.d.ts +6 -0
  181. package/react/src/utils/head.d.ts.map +1 -0
  182. package/react/src/utils/index.d.ts +3 -0
  183. package/react/src/utils/index.d.ts.map +1 -0
  184. package/CHANGELOG.md +0 -23
  185. package/packages/cli/README.md +0 -264
  186. package/packages/core/README.md +0 -90
  187. package/packages/elysia/README.md +0 -250
  188. package/packages/react/README.md +0 -3
@@ -0,0 +1,233 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Link, useData, useParams } from 'reroute-js/react';
3
+ import Header from '../../components/Header';
4
+ import ProductCard from '../../components/ProductCard';
5
+ import { getProduct, getProductsByCategory, type Product } from '../../lib/api';
6
+
7
+ // Enable SSR data fetching for this route
8
+ const ssr = {
9
+ async data({ params }: { params: { id?: string } }) {
10
+ try {
11
+ const idStr = params?.id;
12
+ const id = idStr ? Number.parseInt(idStr, 10) : NaN;
13
+ if (!Number.isFinite(id)) {
14
+ return {
15
+ product: null,
16
+ relatedProducts: [],
17
+ error: 'Invalid product ID',
18
+ };
19
+ }
20
+ const product = await getProduct(id);
21
+ const allInCategory = await getProductsByCategory(product.category);
22
+ const relatedProducts = allInCategory
23
+ .filter((p) => p.id !== id)
24
+ .slice(0, 4);
25
+ return { product, relatedProducts };
26
+ } catch (e) {
27
+ console.error('ssr.data error for product detail:', e);
28
+ return {
29
+ product: null,
30
+ relatedProducts: [],
31
+ error: 'Failed to load product details. Please try again later.',
32
+ };
33
+ }
34
+ },
35
+ };
36
+
37
+ function ProductDetailPage() {
38
+ const params = useParams<{ id: string }>();
39
+ const productId = params.id ? Number.parseInt(params.id, 10) : null;
40
+
41
+ // Read SSR-seeded or prefetched data for this route
42
+ const routeData = useData<{
43
+ product: Product | null;
44
+ relatedProducts: Product[];
45
+ error?: string;
46
+ }>();
47
+ const [clientData, setClientData] = useState<typeof routeData | null>(null);
48
+ const data = routeData ?? clientData ?? undefined;
49
+ const isLoading = data === undefined;
50
+ const product = data?.product ?? null;
51
+ const relatedProducts = data?.relatedProducts ?? [];
52
+ const error = data?.error ?? (productId ? null : 'Invalid product ID');
53
+
54
+ // Fallback on client nav if hover prefetch didn't run
55
+ useEffect(() => {
56
+ if (typeof window === 'undefined') return;
57
+ if (routeData !== undefined) return;
58
+ if (!productId) return;
59
+ const path = `/products/${productId}`;
60
+ fetch(`/__reroute_data?path=${encodeURIComponent(path)}`, {
61
+ headers: { Accept: 'application/json' },
62
+ credentials: 'same-origin',
63
+ })
64
+ .then((r) => (r.ok ? r.json() : null))
65
+ .then((j) => {
66
+ if (!j) return;
67
+ setClientData(j.data ?? j);
68
+ })
69
+ .catch(() => {});
70
+ }, [productId, routeData]);
71
+
72
+ const [addingToCart, setAddingToCart] = useState(false);
73
+
74
+ const handleAddToCart = () => {
75
+ setAddingToCart(true);
76
+ // Simulate adding to cart
77
+ setTimeout(() => {
78
+ setAddingToCart(false);
79
+ alert(`Added ${product?.title} to cart!`);
80
+ }, 500);
81
+ };
82
+
83
+ const handleBuyNow = () => {
84
+ alert(`Proceeding to checkout with ${product?.title}`);
85
+ };
86
+
87
+ if (isLoading) {
88
+ return (
89
+ <>
90
+ <Header />
91
+ <main className='container-custom py-8'>
92
+ <div className='text-center py-16 text-gray-600 text-lg'>
93
+ <div className='animate-pulse'>Loading product details...</div>
94
+ </div>
95
+ </main>
96
+ </>
97
+ );
98
+ }
99
+
100
+ if (error || !product) {
101
+ return (
102
+ <>
103
+ <Header />
104
+ <main className='container-custom py-8'>
105
+ <div className='text-center py-16 bg-red-50 text-red-600 text-lg rounded-xl'>
106
+ {error || 'Product not found'}
107
+ <div className='mt-4'>
108
+ <Link to='/products' className='link font-semibold'>
109
+ ← Back to Products
110
+ </Link>
111
+ </div>
112
+ </div>
113
+ </main>
114
+ </>
115
+ );
116
+ }
117
+
118
+ return (
119
+ <>
120
+ <Header />
121
+
122
+ <main className='container-custom py-8'>
123
+ {/* Breadcrumbs */}
124
+ <nav className='flex items-center gap-2 text-sm text-gray-600 mb-8'>
125
+ <Link to='/' className='link'>
126
+ Home
127
+ </Link>
128
+ <span>/</span>
129
+ <Link to='/products' className='link'>
130
+ Products
131
+ </Link>
132
+ <span>/</span>
133
+ <Link to={`/categories/${product.category}`} className='link'>
134
+ {product.category}
135
+ </Link>
136
+ <span>/</span>
137
+ <span className='text-gray-900'>{product.title}</span>
138
+ </nav>
139
+
140
+ {/* Product Details */}
141
+ <div className='grid grid-cols-1 lg:grid-cols-2 gap-12 mb-16'>
142
+ {/* Product Image */}
143
+ <div className='card flex items-center justify-center p-8 min-h-[500px]'>
144
+ <img
145
+ src={product.image}
146
+ alt={product.title}
147
+ className='max-w-full max-h-[500px] object-contain'
148
+ style={{ viewTransitionName: `product-image-${product.id}` }}
149
+ />
150
+ </div>
151
+
152
+ {/* Product Info */}
153
+ <div className='flex flex-col gap-6'>
154
+ <div className='badge-primary'>{product.category}</div>
155
+
156
+ <h1 className='text-4xl font-bold text-gray-900 leading-tight'>
157
+ {product.title}
158
+ </h1>
159
+
160
+ {product.rating && (
161
+ <div className='flex items-center gap-4'>
162
+ <div className='flex items-center gap-2 text-base text-gray-600'>
163
+ <span className='text-2xl'>⭐</span>
164
+ <span className='font-semibold text-xl'>
165
+ {product.rating.rate}
166
+ </span>
167
+ <span>({product.rating.count} reviews)</span>
168
+ </div>
169
+ </div>
170
+ )}
171
+
172
+ <div className='text-4xl font-bold text-primary-600'>
173
+ ${product.price.toFixed(2)}
174
+ </div>
175
+
176
+ <p className='text-lg text-gray-700 leading-relaxed'>
177
+ {product.description}
178
+ </p>
179
+
180
+ <div className='flex gap-4 mt-4'>
181
+ <button
182
+ type='button'
183
+ className='btn-primary flex-1 disabled:opacity-50'
184
+ onClick={handleAddToCart}
185
+ disabled={addingToCart}
186
+ >
187
+ {addingToCart ? 'Adding...' : '🛒 Add to Cart'}
188
+ </button>
189
+
190
+ <button
191
+ type='button'
192
+ className='btn-secondary'
193
+ onClick={handleBuyNow}
194
+ >
195
+ Buy Now
196
+ </button>
197
+ </div>
198
+
199
+ <div className='p-4 bg-gray-50 rounded-xl text-sm text-gray-600 space-y-2'>
200
+ <div className='flex items-center gap-2'>
201
+ <span className='text-green-600 font-bold'>✓</span>
202
+ <span>Free shipping on orders over $50</span>
203
+ </div>
204
+ <div className='flex items-center gap-2'>
205
+ <span className='text-green-600 font-bold'>✓</span>
206
+ <span>30-day return policy</span>
207
+ </div>
208
+ <div className='flex items-center gap-2'>
209
+ <span className='text-green-600 font-bold'>✓</span>
210
+ <span>Secure checkout</span>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+
216
+ {/* Related Products */}
217
+ {relatedProducts.length > 0 && (
218
+ <section className='animate-in'>
219
+ <h2 className='section-heading'>Related Products</h2>
220
+ <div className='product-grid'>
221
+ {relatedProducts.map((relatedProduct) => (
222
+ <ProductCard key={relatedProduct.id} product={relatedProduct} />
223
+ ))}
224
+ </div>
225
+ </section>
226
+ )}
227
+ </main>
228
+ </>
229
+ );
230
+ }
231
+
232
+ export default ProductDetailPage;
233
+ export { ssr };
@@ -0,0 +1,261 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useData } from 'reroute-js/react';
3
+ import Header from '../../components/Header';
4
+ import ProductCard from '../../components/ProductCard';
5
+ import {
6
+ getAllProducts,
7
+ getCategories,
8
+ getProductsByCategory,
9
+ type Product,
10
+ } from '../../lib/api';
11
+
12
+ type SortOption = 'default' | 'price-asc' | 'price-desc' | 'name-asc';
13
+
14
+ const ssr = {
15
+ async data() {
16
+ try {
17
+ const [categories, products] = await Promise.all([
18
+ getCategories(),
19
+ getAllProducts(),
20
+ ]);
21
+ return { categories, products } as {
22
+ categories: string[];
23
+ products: Product[];
24
+ };
25
+ } catch (e) {
26
+ console.error('ssr.data error for products list:', e);
27
+ return {
28
+ categories: [],
29
+ products: [],
30
+ error: 'Failed to load products.',
31
+ };
32
+ }
33
+ },
34
+ };
35
+
36
+ function ProductsPage() {
37
+ const routeData = useData<{
38
+ products: Product[];
39
+ categories: string[];
40
+ error?: string;
41
+ }>();
42
+ const [products, setProducts] = useState<Product[]>(
43
+ routeData?.products || [],
44
+ );
45
+ const [filteredProducts, setFilteredProducts] = useState<Product[]>(
46
+ routeData?.products || [],
47
+ );
48
+ const [categories, setCategories] = useState<string[]>(
49
+ routeData?.categories || [],
50
+ );
51
+ const [selectedCategory, setSelectedCategory] = useState<string>('all');
52
+ const [searchQuery, setSearchQuery] = useState<string>('');
53
+ const [sortBy, setSortBy] = useState<SortOption>('default');
54
+ const [loading, setLoading] = useState(!routeData);
55
+ const [error, setError] = useState<string | null>(routeData?.error || null);
56
+
57
+ // Load categories on mount (if not provided by SSR)
58
+ useEffect(() => {
59
+ if (categories.length > 0) return;
60
+ getCategories()
61
+ .then(setCategories)
62
+ .catch((err) => {
63
+ console.error('Failed to load categories:', err);
64
+ });
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ }, [categories.length]);
67
+
68
+ // Load products based on category (skip initial when SSR provided for 'all')
69
+ useEffect(() => {
70
+ // When SSR provided initial products just stop loading
71
+ if (products.length > 0) {
72
+ setFilteredProducts(products);
73
+ setLoading(false);
74
+ return;
75
+ }
76
+ setLoading(true);
77
+ setError(null);
78
+
79
+ const loadProducts =
80
+ selectedCategory === 'all'
81
+ ? getAllProducts()
82
+ : getProductsByCategory(selectedCategory);
83
+
84
+ loadProducts
85
+ .then((data) => {
86
+ setProducts(data);
87
+ setFilteredProducts(data);
88
+ })
89
+ .catch((err) => {
90
+ console.error('Failed to load products:', err);
91
+ setError('Failed to load products. Please try again later.');
92
+ })
93
+ .finally(() => {
94
+ setLoading(false);
95
+ });
96
+ // eslint-disable-next-line react-hooks/exhaustive-deps
97
+ }, [selectedCategory, products]);
98
+
99
+ // Filter and sort products
100
+ useEffect(() => {
101
+ let result = [...products];
102
+
103
+ // Apply search filter
104
+ if (searchQuery.trim()) {
105
+ const query = searchQuery.toLowerCase();
106
+ result = result.filter(
107
+ (product) =>
108
+ product.title.toLowerCase().includes(query) ||
109
+ product.description.toLowerCase().includes(query) ||
110
+ product.category.toLowerCase().includes(query),
111
+ );
112
+ }
113
+
114
+ // Apply sorting
115
+ switch (sortBy) {
116
+ case 'price-asc':
117
+ result.sort((a, b) => a.price - b.price);
118
+ break;
119
+ case 'price-desc':
120
+ result.sort((a, b) => b.price - a.price);
121
+ break;
122
+ case 'name-asc':
123
+ result.sort((a, b) => a.title.localeCompare(b.title));
124
+ break;
125
+ default:
126
+ // Keep default order
127
+ break;
128
+ }
129
+
130
+ setFilteredProducts(result);
131
+ }, [products, searchQuery, sortBy]);
132
+
133
+ return (
134
+ <>
135
+ <Header />
136
+
137
+ <main className='container-custom py-8'>
138
+ <div className='mb-8'>
139
+ <h1 className='section-heading'>All Products</h1>
140
+ <p className='section-subheading'>
141
+ Browse our complete collection of quality products
142
+ </p>
143
+ </div>
144
+
145
+ {/* Filters */}
146
+ <div className='flex flex-wrap items-center gap-4 mb-8 p-6 bg-white rounded-xl shadow-card'>
147
+ <div className='flex items-center gap-2'>
148
+ <label
149
+ className='font-semibold text-gray-700'
150
+ htmlFor='category-filter'
151
+ >
152
+ Category:
153
+ </label>
154
+ <select
155
+ id='category-filter'
156
+ className='select min-w-[200px]'
157
+ value={selectedCategory}
158
+ onChange={(e) => setSelectedCategory(e.target.value)}
159
+ >
160
+ <option value='all'>All Categories</option>
161
+ {categories.map((category) => (
162
+ <option key={category} value={category}>
163
+ {category.charAt(0).toUpperCase() + category.slice(1)}
164
+ </option>
165
+ ))}
166
+ </select>
167
+ </div>
168
+
169
+ <input
170
+ type='search'
171
+ placeholder='Search products...'
172
+ className='input flex-1 min-w-[250px]'
173
+ value={searchQuery}
174
+ onChange={(e) => setSearchQuery(e.target.value)}
175
+ />
176
+
177
+ <div className='flex items-center gap-2'>
178
+ <span className='font-semibold text-gray-700'>Sort:</span>
179
+ <div className='flex gap-2'>
180
+ <button
181
+ type='button'
182
+ className={`btn-sm transition-all ${
183
+ sortBy === 'default'
184
+ ? 'bg-primary-600 text-white border-primary-600'
185
+ : 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'
186
+ }`}
187
+ onClick={() => setSortBy('default')}
188
+ >
189
+ Default
190
+ </button>
191
+ <button
192
+ type='button'
193
+ className={`btn-sm transition-all ${
194
+ sortBy === 'price-asc'
195
+ ? 'bg-primary-600 text-white border-primary-600'
196
+ : 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'
197
+ }`}
198
+ onClick={() => setSortBy('price-asc')}
199
+ >
200
+ Price: Low to High
201
+ </button>
202
+ <button
203
+ type='button'
204
+ className={`btn-sm transition-all ${
205
+ sortBy === 'price-desc'
206
+ ? 'bg-primary-600 text-white border-primary-600'
207
+ : 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'
208
+ }`}
209
+ onClick={() => setSortBy('price-desc')}
210
+ >
211
+ Price: High to Low
212
+ </button>
213
+ <button
214
+ type='button'
215
+ className={`btn-sm transition-all ${
216
+ sortBy === 'name-asc'
217
+ ? 'bg-primary-600 text-white border-primary-600'
218
+ : 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'
219
+ }`}
220
+ onClick={() => setSortBy('name-asc')}
221
+ >
222
+ Name: A-Z
223
+ </button>
224
+ </div>
225
+ </div>
226
+
227
+ {!loading && (
228
+ <div className='ml-auto text-sm text-gray-600'>
229
+ Showing {filteredProducts.length} of {products.length} products
230
+ </div>
231
+ )}
232
+ </div>
233
+
234
+ {/* Products Grid */}
235
+ {loading ? (
236
+ <div className='text-center py-16 text-gray-600 text-lg'>
237
+ <div className='animate-pulse'>Loading products...</div>
238
+ </div>
239
+ ) : error ? (
240
+ <div className='text-center py-16 bg-red-50 text-red-600 text-lg rounded-xl'>
241
+ {error}
242
+ </div>
243
+ ) : filteredProducts.length === 0 ? (
244
+ <div className='text-center py-16 text-gray-600 text-lg'>
245
+ <div className='text-6xl mb-4'>🔍</div>
246
+ <div>No products found matching your criteria</div>
247
+ </div>
248
+ ) : (
249
+ <div className='product-grid animate-in'>
250
+ {filteredProducts.map((product) => (
251
+ <ProductCard key={product.id} product={product} />
252
+ ))}
253
+ </div>
254
+ )}
255
+ </main>
256
+ </>
257
+ );
258
+ }
259
+
260
+ export default ProductsPage;
261
+ export { ssr };