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.
- package/_/README.md +59 -0
- package/_/basic/package.json +23 -0
- package/_/basic/src/client/App.tsx +10 -0
- package/_/basic/src/client/components/Counter.tsx +15 -0
- package/_/basic/src/client/index.html +12 -0
- package/_/basic/src/client/index.tsx +5 -0
- package/_/basic/src/client/routes/[404].tsx +18 -0
- package/_/basic/src/client/routes/about.tsx +25 -0
- package/_/basic/src/client/routes/index.tsx +57 -0
- package/_/basic/src/index.ts +20 -0
- package/_/basic/tsconfig.json +26 -0
- package/_/blog/package.json +23 -0
- package/_/blog/src/client/App.tsx +10 -0
- package/_/blog/src/client/components/Counter.tsx +14 -0
- package/_/blog/src/client/components/RecentPosts.tsx +90 -0
- package/_/blog/src/client/index.html +13 -0
- package/_/blog/src/client/index.tsx +5 -0
- package/_/blog/src/client/routes/[404].tsx +21 -0
- package/_/blog/src/client/routes/about.tsx +31 -0
- package/_/blog/src/client/routes/blog/[404].tsx +21 -0
- package/_/blog/src/client/routes/blog/[layout].tsx +84 -0
- package/_/blog/src/client/routes/blog/[slug].tsx +11 -0
- package/_/blog/src/client/routes/blog/content/1-hello-world.tsx +27 -0
- package/_/blog/src/client/routes/blog/content/2-what-is-reroute.tsx +31 -0
- package/_/blog/src/client/routes/blog/index.tsx +70 -0
- package/_/blog/src/client/routes/index.tsx +63 -0
- package/_/blog/src/index.ts +20 -0
- package/_/blog/tsconfig.json +26 -0
- package/_/store/package.json +25 -0
- package/_/store/src/client/App.tsx +17 -0
- package/_/store/src/client/components/Header.tsx +40 -0
- package/_/store/src/client/components/ProductCard.tsx +51 -0
- package/_/store/src/client/index.html +17 -0
- package/_/store/src/client/index.tsx +7 -0
- package/_/store/src/client/lib/api.ts +153 -0
- package/_/store/src/client/routes/[404].tsx +63 -0
- package/_/store/src/client/routes/categories/[category].tsx +223 -0
- package/_/store/src/client/routes/categories/index.tsx +187 -0
- package/_/store/src/client/routes/index.tsx +126 -0
- package/_/store/src/client/routes/products/[id].tsx +233 -0
- package/_/store/src/client/routes/products/index.tsx +261 -0
- package/_/store/src/client/theme.css +306 -0
- package/_/store/src/index.ts +19 -0
- package/_/store/tsconfig.json +26 -0
- package/{packages/cli/bin.ts → cli/bin.d.ts} +1 -1
- package/cli/bin.d.ts.map +1 -0
- package/cli/bin.js +878 -0
- package/cli/bin.js.map +15 -0
- package/cli/index.d.ts +2 -0
- package/cli/index.d.ts.map +1 -0
- package/cli/index.js +147 -0
- package/cli/index.js.map +10 -0
- package/cli/src/cli.d.ts +8 -0
- package/cli/src/cli.d.ts.map +1 -0
- package/cli/src/commands/build.d.ts +8 -0
- package/cli/src/commands/build.d.ts.map +1 -0
- package/cli/src/commands/dev.d.ts +8 -0
- package/cli/src/commands/dev.d.ts.map +1 -0
- package/cli/src/commands/gen.d.ts +3 -0
- package/cli/src/commands/gen.d.ts.map +1 -0
- package/cli/src/commands/init.d.ts +8 -0
- package/cli/src/commands/init.d.ts.map +1 -0
- package/cli/src/libs/index.d.ts +2 -0
- package/cli/src/libs/index.d.ts.map +1 -0
- package/cli/src/libs/tailwind.d.ts +45 -0
- package/cli/src/libs/tailwind.d.ts.map +1 -0
- package/core/index.d.ts +2 -0
- package/core/index.d.ts.map +1 -0
- package/core/index.js +1117 -0
- package/core/index.js.map +25 -0
- package/core/src/bundler/hash.d.ts +2 -0
- package/core/src/bundler/hash.d.ts.map +1 -0
- package/core/src/bundler/index.d.ts +3 -0
- package/core/src/bundler/index.d.ts.map +1 -0
- package/core/src/bundler/transpile.d.ts +4 -0
- package/core/src/bundler/transpile.d.ts.map +1 -0
- package/core/src/content/discovery.d.ts +5 -0
- package/core/src/content/discovery.d.ts.map +1 -0
- package/core/src/content/index.d.ts +4 -0
- package/core/src/content/index.d.ts.map +1 -0
- package/core/src/content/metadata.d.ts +9 -0
- package/core/src/content/metadata.d.ts.map +1 -0
- package/core/src/content/registry.d.ts +2 -0
- package/core/src/content/registry.d.ts.map +1 -0
- package/core/src/index.d.ts +7 -0
- package/core/src/index.d.ts.map +1 -0
- package/core/src/ssr/data.d.ts +9 -0
- package/core/src/ssr/data.d.ts.map +1 -0
- package/core/src/ssr/index.d.ts +4 -0
- package/core/src/ssr/index.d.ts.map +1 -0
- package/core/src/ssr/modules.d.ts +8 -0
- package/core/src/ssr/modules.d.ts.map +1 -0
- package/core/src/ssr/render.d.ts +20 -0
- package/core/src/ssr/render.d.ts.map +1 -0
- package/core/src/ssr/seed.d.ts +2 -0
- package/core/src/ssr/seed.d.ts.map +1 -0
- package/core/src/template/html.d.ts +4 -0
- package/core/src/template/html.d.ts.map +1 -0
- package/core/src/template/index.d.ts +2 -0
- package/core/src/template/index.d.ts.map +1 -0
- package/core/src/types.d.ts +50 -0
- package/core/src/types.d.ts.map +1 -0
- package/core/src/utils/cache.d.ts +12 -0
- package/core/src/utils/cache.d.ts.map +1 -0
- package/core/src/utils/compression.d.ts +5 -0
- package/core/src/utils/compression.d.ts.map +1 -0
- package/core/src/utils/index.d.ts +5 -0
- package/core/src/utils/index.d.ts.map +1 -0
- package/core/src/utils/mime.d.ts +3 -0
- package/core/src/utils/mime.d.ts.map +1 -0
- package/core/src/utils/path.d.ts +6 -0
- package/core/src/utils/path.d.ts.map +1 -0
- package/elysia/index.d.ts +2 -0
- package/elysia/index.d.ts.map +1 -0
- package/elysia/index.js +1780 -0
- package/elysia/index.js.map +32 -0
- package/elysia/src/index.d.ts +3 -0
- package/elysia/src/index.d.ts.map +1 -0
- package/elysia/src/plugin.d.ts +32 -0
- package/elysia/src/plugin.d.ts.map +1 -0
- package/elysia/src/routes/artifacts.d.ts +3 -0
- package/elysia/src/routes/artifacts.d.ts.map +1 -0
- package/elysia/src/routes/content.d.ts +3 -0
- package/elysia/src/routes/content.d.ts.map +1 -0
- package/elysia/src/routes/dev.d.ts +7 -0
- package/elysia/src/routes/dev.d.ts.map +1 -0
- package/elysia/src/routes/ssr.d.ts +21 -0
- package/elysia/src/routes/ssr.d.ts.map +1 -0
- package/elysia/src/routes/static.d.ts +19 -0
- package/elysia/src/routes/static.d.ts.map +1 -0
- package/elysia/src/types.d.ts +31 -0
- package/elysia/src/types.d.ts.map +1 -0
- package/elysia/src/utils/http.d.ts +5 -0
- package/elysia/src/utils/http.d.ts.map +1 -0
- package/package.json +3 -3
- package/react/index.d.ts +2 -0
- package/react/index.d.ts.map +1 -0
- package/react/index.js +1152 -0
- package/react/index.js.map +23 -0
- package/react/src/components/ContentRoute.d.ts +13 -0
- package/react/src/components/ContentRoute.d.ts.map +1 -0
- package/react/src/components/Link.d.ts +8 -0
- package/react/src/components/Link.d.ts.map +1 -0
- package/react/src/components/Outlet.d.ts +7 -0
- package/react/src/components/Outlet.d.ts.map +1 -0
- package/react/src/components/index.d.ts +4 -0
- package/react/src/components/index.d.ts.map +1 -0
- package/react/src/hooks/index.d.ts +7 -0
- package/react/src/hooks/index.d.ts.map +1 -0
- package/react/src/hooks/useContent.d.ts +26 -0
- package/react/src/hooks/useContent.d.ts.map +1 -0
- package/react/src/hooks/useData.d.ts +10 -0
- package/react/src/hooks/useData.d.ts.map +1 -0
- package/react/src/hooks/useNavigate.d.ts +6 -0
- package/react/src/hooks/useNavigate.d.ts.map +1 -0
- package/react/src/hooks/useParams.d.ts +6 -0
- package/react/src/hooks/useParams.d.ts.map +1 -0
- package/react/src/hooks/useRouter.d.ts +7 -0
- package/react/src/hooks/useRouter.d.ts.map +1 -0
- package/react/src/hooks/useSearchParams.d.ts +6 -0
- package/react/src/hooks/useSearchParams.d.ts.map +1 -0
- package/react/src/index.d.ts +6 -0
- package/react/src/index.d.ts.map +1 -0
- package/react/src/providers/ContentProvider.d.ts +35 -0
- package/react/src/providers/ContentProvider.d.ts.map +1 -0
- package/react/src/providers/RerouteProvider.d.ts +25 -0
- package/react/src/providers/RerouteProvider.d.ts.map +1 -0
- package/react/src/providers/RouterProvider.d.ts +23 -0
- package/react/src/providers/RouterProvider.d.ts.map +1 -0
- package/react/src/providers/index.d.ts +4 -0
- package/react/src/providers/index.d.ts.map +1 -0
- package/react/src/types/any.d.ts +3 -0
- package/react/src/types/any.d.ts.map +1 -0
- package/react/src/types/index.d.ts +3 -0
- package/react/src/types/index.d.ts.map +1 -0
- package/react/src/types/router.d.ts +32 -0
- package/react/src/types/router.d.ts.map +1 -0
- package/react/src/utils/content.d.ts +8 -0
- package/react/src/utils/content.d.ts.map +1 -0
- package/react/src/utils/head.d.ts +6 -0
- package/react/src/utils/head.d.ts.map +1 -0
- package/react/src/utils/index.d.ts +3 -0
- package/react/src/utils/index.d.ts.map +1 -0
- package/CHANGELOG.md +0 -23
- package/packages/cli/README.md +0 -264
- package/packages/core/README.md +0 -90
- package/packages/elysia/README.md +0 -250
- 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 };
|