eeshaAI commited on
Commit
c096778
·
1 Parent(s): 025f320

feat: Migrate frontend from static HTML to Next.js 16 App Router

Browse files

- All 16 buyer pages: home, login, product, cart, checkout, payment,
categories, search, seller-profile, profile, orders, wishlist,
videos, live, live-watch, group-buy, ai-chat, link-account, telegram
- 7 seller dashboard pages with shared sidebar layout
- 22 API route proxies to existing Supabase Edge Functions
- Suspense boundaries for useSearchParams pages
- Standalone output build for Docker deployment
- Dockerfile (multi-stage Node 20 alpine, port 7860)
- README with HF Space YAML frontmatter
- .hfignore for lean Docker context

Backend unchanged: 27 Supabase Edge Functions, Render bot, OpenWA,
PalmPay payment verifier — all remain in place.

Files changed (2) hide show
  1. src/app/product/page.tsx +8 -0
  2. src/app/search/page.tsx +397 -75
src/app/product/page.tsx CHANGED
@@ -178,6 +178,14 @@ function ProductContent() {
178
 
179
  return (
180
  <div className="bg-white pb-20">
 
 
 
 
 
 
 
 
181
  {/* === 1. IMAGE GALLERY (full-width, page indicator bottom-right) === */}
182
  <div className="relative aspect-square bg-slate-50">
183
  {images[activeImage] ? (
 
178
 
179
  return (
180
  <div className="bg-white pb-20">
181
+ {/* Back arrow (top-left, overlays the image) */}
182
+ <button
183
+ onClick={() => router.back()}
184
+ className="fixed top-3 left-3 z-50 w-9 h-9 rounded-full bg-white/90 backdrop-blur shadow flex items-center justify-center"
185
+ >
186
+ <ChevronLeft className="w-5 h-5 text-black" />
187
+ </button>
188
+
189
  {/* === 1. IMAGE GALLERY (full-width, page indicator bottom-right) === */}
190
  <div className="relative aspect-square bg-slate-50">
191
  {images[activeImage] ? (
src/app/search/page.tsx CHANGED
@@ -1,115 +1,438 @@
1
  'use client';
2
 
3
- import { useEffect, useState, useCallback , Suspense} from 'react';
4
- import { useSearchParams } from 'next/navigation';
5
- import { api, type Product } from '@/lib/api';
6
- import { ProductGrid } from '@/components/product-card';
7
- import { Input } from '@/components/ui/input';
8
  import { Button } from '@/components/ui/button';
9
- import { Search as SearchIcon, SlidersHorizontal } from 'lucide-react';
10
- import { useRouter } from 'next/navigation';
 
 
 
 
 
11
 
12
  function SearchContent() {
13
  const params = useSearchParams();
14
  const router = useRouter();
15
- const initialQuery = params.get('q') || '';
16
 
17
- const [query, setQuery] = useState(initialQuery);
18
- const [maxPrice, setMaxPrice] = useState<number | null>(null);
19
- const [results, setResults] = useState<Product[]>([]);
 
20
  const [loading, setLoading] = useState(false);
21
- const [showFilters, setShowFilters] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- const doSearch = useCallback(async (q: string, mp: number | null) => {
24
  if (!q.trim()) {
25
- setResults([]);
 
26
  return;
27
  }
28
  setLoading(true);
29
- const result = await api.products.search(q, mp);
30
  if (result.success) {
31
- setResults(result.results || result.products || []);
 
 
 
 
 
 
 
 
 
 
 
32
  }
33
  setLoading(false);
34
  }, []);
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  useEffect(() => {
37
- setQuery(initialQuery);
38
- doSearch(initialQuery, null);
39
- }, [initialQuery, doSearch]);
 
 
 
 
 
 
 
40
 
41
  const handleSubmit = (e: React.FormEvent) => {
42
  e.preventDefault();
43
- const params = new URLSearchParams();
44
- if (query) params.set('q', query);
45
- router.replace(`/search?${params.toString()}`);
46
- doSearch(query, maxPrice);
 
 
 
 
 
47
  };
48
 
 
 
 
 
 
 
 
 
 
49
  return (
50
- <div className="max-w-7xl mx-auto px-3 sm:px-4 lg:px-6 py-4">
51
- <h1 className="text-xl font-bold mb-3">Search</h1>
52
-
53
- <form onSubmit={handleSubmit} className="flex gap-2 mb-3">
54
- <div className="flex-1 relative">
55
- <SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
56
- <Input
57
- value={query}
58
- onChange={(e) => setQuery(e.target.value)}
59
- placeholder="Search products, brands, sellers..."
60
- className="pl-9"
61
- autoFocus
62
- />
63
- </div>
64
- <Button type="submit" className="brand-gradient text-primary-foreground">Search</Button>
65
- <Button
66
- type="button"
67
- variant="outline"
68
- onClick={() => setShowFilters(!showFilters)}
69
- >
70
- <SlidersHorizontal className="w-4 h-4" />
71
- </Button>
72
- </form>
73
-
74
- {showFilters && (
75
- <div className="bg-slate-50 rounded-xl p-3 mb-3 flex items-center gap-2">
76
- <label className="text-xs font-semibold text-slate-600">Max price:</label>
77
- <input
78
- type="number"
79
- value={maxPrice || ''}
80
- onChange={(e) => setMaxPrice(e.target.value ? Number(e.target.value) : null)}
81
- placeholder="Any"
82
- className="flex-1 max-w-[140px] px-2 py-1 border border-slate-200 rounded text-xs"
83
- />
84
- {maxPrice && (
85
- <button
86
- onClick={() => setMaxPrice(null)}
87
- className="text-xs text-red-500"
88
- >
89
- Clear
90
  </button>
91
- )}
92
  </div>
93
- )}
94
 
95
- {query && (
96
- <div className="text-xs text-slate-500 mb-3">
97
- {loading ? 'Searching...' : `${results.length} result${results.length !== 1 ? 's' : ''} for "${query}"`}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  </div>
99
- )}
100
 
101
- {!query && !loading && (
102
- <div className="text-center py-12">
103
- <SearchIcon className="w-12 h-12 mx-auto text-slate-200 mb-3" />
104
- <p className="text-sm text-slate-500">Start typing to search across thousands of products</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  </div>
106
  )}
107
 
108
- <ProductGrid products={results} loading={loading} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  </div>
110
  );
111
  }
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  export default function SearchPage() {
114
  return (
115
  <Suspense fallback={<div className="flex items-center justify-center min-h-[60vh]"><div className="w-8 h-8 border-3 border-slate-200 border-t-primary rounded-full animate-spin" /></div>}>
@@ -117,4 +440,3 @@ export default function SearchPage() {
117
  </Suspense>
118
  );
119
  }
120
-
 
1
  'use client';
2
 
3
+ import { useEffect, useState, useCallback, Suspense } from 'react';
4
+ import { useSearchParams, useRouter } from 'next/navigation';
5
+ import { api, formatPrice, type Product } from '@/lib/api';
6
+ import { Card } from '@/components/ui/card';
 
7
  import { Button } from '@/components/ui/button';
8
+ import {
9
+ Search, Camera, ChevronLeft, Store, Sparkles, Video as VideoIcon,
10
+ Star, ShoppingBag, Play, Heart, MessageCircle
11
+ } from 'lucide-react';
12
+ import Link from 'next/link';
13
+
14
+ type Tab = 'ai' | 'all' | 'videos';
15
 
16
  function SearchContent() {
17
  const params = useSearchParams();
18
  const router = useRouter();
19
+ const query = params.get('q') || '';
20
 
21
+ const [searchInput, setSearchInput] = useState(query);
22
+ const [tab, setTab] = useState<Tab>('all');
23
+ const [products, setProducts] = useState<Product[]>([]);
24
+ const [videos, setVideos] = useState<any[]>([]);
25
  const [loading, setLoading] = useState(false);
26
+ const [aiAnswer, setAiAnswer] = useState<string>('');
27
+ const [aiProducts, setAiProducts] = useState<Product[]>([]);
28
+ const [aiLoading, setAiLoading] = useState(false);
29
+
30
+ // Filter pills state
31
+ const [activeFilters, setActiveFilters] = useState<string[]>([]);
32
+
33
+ const FILTER_PILLS = [
34
+ 'Free shipping',
35
+ 'Pay on delivery',
36
+ 'Bestseller',
37
+ 'Under ₦10,000',
38
+ 'Under ₦50,000',
39
+ 'Verified Seller',
40
+ 'Flash Deals',
41
+ ];
42
 
43
+ const doSearch = useCallback(async (q: string) => {
44
  if (!q.trim()) {
45
+ setProducts([]);
46
+ setVideos([]);
47
  return;
48
  }
49
  setLoading(true);
50
+ const result = await api.products.search(q, null);
51
  if (result.success) {
52
+ setProducts(result.results || result.products || []);
53
+ }
54
+ // Also fetch videos matching the query (use the feed + filter client-side)
55
+ const vidResp = await api.videos.feed(50);
56
+ if (vidResp.success) {
57
+ const q_lower = q.toLowerCase();
58
+ const filtered = (vidResp.videos || []).filter((v: any) =>
59
+ (v.caption || '').toLowerCase().includes(q_lower) ||
60
+ (v.product?.name || '').toLowerCase().includes(q_lower) ||
61
+ (v.seller?.business_name || '').toLowerCase().includes(q_lower)
62
+ );
63
+ setVideos(filtered);
64
  }
65
  setLoading(false);
66
  }, []);
67
 
68
+ const doAiSearch = useCallback(async (q: string) => {
69
+ if (!q.trim()) return;
70
+ setAiLoading(true);
71
+ setAiAnswer('');
72
+ setAiProducts([]);
73
+
74
+ // Search products first to ground the AI
75
+ const searchResp = await api.products.search(q, null);
76
+ const foundProducts: Product[] = searchResp.success
77
+ ? (searchResp.results || searchResp.products || []).slice(0, 4)
78
+ : [];
79
+
80
+ setAiProducts(foundProducts);
81
+
82
+ // Call AI chat endpoint for an overview-style answer
83
+ try {
84
+ const aiResp = await fetch('/api/ai-chat', {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({
88
+ message: `A user searched for "${q}" on Cellex (a Nigerian e-commerce marketplace). Provide a helpful AI Overview answer (like Google's AI Overview) in 2-3 short paragraphs. Mention what types of products are available, price ranges, and any shopping tips. Be concise and friendly. ${
89
+ foundProducts.length > 0
90
+ ? `I found these products: ${foundProducts.map(p => `${p.name} (${formatPrice(p.price)})`).join(', ')}.`
91
+ : 'No specific products were found, so give general shopping advice.'
92
+ }`,
93
+ context: 'Search overview',
94
+ history: [],
95
+ }),
96
+ });
97
+ if (aiResp.ok) {
98
+ const data = await aiResp.json();
99
+ setAiAnswer(data.reply || data.message || data.content || '');
100
+ } else {
101
+ setAiAnswer(`Here's what I found for "${q}": ${foundProducts.length} products available on Cellex. Browse the results below or try refining your search with filters.`);
102
+ }
103
+ } catch {
104
+ setAiAnswer(`Here's what I found for "${q}": ${foundProducts.length} products available on Cellex. Browse the results below or try refining your search with filters.`);
105
+ }
106
+ setAiLoading(false);
107
+ }, []);
108
+
109
  useEffect(() => {
110
+ setSearchInput(query);
111
+ doSearch(query);
112
+ }, [query, doSearch]);
113
+
114
+ // When user switches to AI tab, trigger AI search if not done yet
115
+ useEffect(() => {
116
+ if (tab === 'ai' && query && !aiAnswer && !aiLoading) {
117
+ doAiSearch(query);
118
+ }
119
+ }, [tab, query, aiAnswer, aiLoading, doAiSearch]);
120
 
121
  const handleSubmit = (e: React.FormEvent) => {
122
  e.preventDefault();
123
+ if (searchInput.trim()) {
124
+ router.push(`/search?q=${encodeURIComponent(searchInput.trim())}`);
125
+ }
126
+ };
127
+
128
+ const toggleFilter = (filter: string) => {
129
+ setActiveFilters(prev =>
130
+ prev.includes(filter) ? prev.filter(f => f !== filter) : [...prev, filter]
131
+ );
132
  };
133
 
134
+ // Apply client-side filters
135
+ const filteredProducts = products.filter(p => {
136
+ if (activeFilters.includes('Under ₦10,000') && p.price >= 10000) return false;
137
+ if (activeFilters.includes('Under ₦50,000') && p.price >= 50000) return false;
138
+ if (activeFilters.includes('Bestseller') && (p.units_sold || 0) < 200) return false;
139
+ if (activeFilters.includes('Flash Deals') && (p.units_sold || 0) < 50) return false;
140
+ return true;
141
+ });
142
+
143
  return (
144
+ <div className="bg-white min-h-screen">
145
+ {/* ===== HEADER: Logo + Search bar ===== */}
146
+ <div className="border-b border-slate-100">
147
+ <div className="max-w-4xl mx-auto px-4 py-3 flex items-center gap-3">
148
+ {/* Back arrow */}
149
+ <button
150
+ onClick={() => router.back()}
151
+ className="shrink-0 w-9 h-9 rounded-full hover:bg-slate-100 flex items-center justify-center"
152
+ >
153
+ <ChevronLeft className="w-5 h-5 text-black" />
154
+ </button>
155
+
156
+ {/* Logo (hidden on mobile when searching) */}
157
+ <Link href="/" className="hidden sm:flex items-center gap-1.5 shrink-0">
158
+ <div className="w-8 h-8 rounded-lg brand-gradient flex items-center justify-center">
159
+ <span className="text-white font-extrabold text-base" style={{ fontFamily: 'var(--font-geist-mono)' }}>C</span>
160
+ </div>
161
+ <span className="text-lg font-extrabold brand-text" style={{ fontFamily: 'var(--font-geist-mono)' }}>Cellex</span>
162
+ </Link>
163
+
164
+ {/* Search bar (pre-filled) */}
165
+ <form onSubmit={handleSubmit} className="flex-1 flex items-center border-2 border-slate-200 rounded-full px-4 py-2 focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/15 transition-all">
166
+ <Search className="w-4 h-4 text-slate-400 mr-2 shrink-0" />
167
+ <input
168
+ type="text"
169
+ value={searchInput}
170
+ onChange={(e) => setSearchInput(e.target.value)}
171
+ placeholder="Search Cellex..."
172
+ className="flex-1 bg-transparent outline-none text-base text-black placeholder:text-slate-400"
173
+ />
174
+ <button type="submit" className="text-primary hover:text-primary/70 ml-2 shrink-0">
175
+ <Search className="w-4 h-4" />
 
 
 
 
 
 
 
 
176
  </button>
177
+ </form>
178
  </div>
 
179
 
180
+ {/* ===== TABS ROW: AI mode | All | Videos ===== */}
181
+ <div className="max-w-4xl mx-auto px-4 flex items-center gap-6">
182
+ <button
183
+ onClick={() => setTab('ai')}
184
+ className={`flex items-center gap-1.5 py-2.5 text-sm font-medium border-b-2 transition-colors ${
185
+ tab === 'ai'
186
+ ? 'border-primary text-primary font-bold'
187
+ : 'border-transparent text-slate-600 hover:text-black'
188
+ }`}
189
+ >
190
+ <Sparkles className="w-4 h-4" />
191
+ AI mode
192
+ </button>
193
+ <button
194
+ onClick={() => setTab('all')}
195
+ className={`py-2.5 text-sm font-medium border-b-2 transition-colors ${
196
+ tab === 'all'
197
+ ? 'border-primary text-primary font-bold'
198
+ : 'border-transparent text-slate-600 hover:text-black'
199
+ }`}
200
+ >
201
+ All
202
+ </button>
203
+ <button
204
+ onClick={() => setTab('videos')}
205
+ className={`flex items-center gap-1.5 py-2.5 text-sm font-medium border-b-2 transition-colors ${
206
+ tab === 'videos'
207
+ ? 'border-primary text-primary font-bold'
208
+ : 'border-transparent text-slate-600 hover:text-black'
209
+ }`}
210
+ >
211
+ <VideoIcon className="w-4 h-4" />
212
+ Videos
213
+ </button>
214
  </div>
215
+ </div>
216
 
217
+ {/* ===== FILTER PILLS (only on All + Videos tabs) ===== */}
218
+ {tab !== 'ai' && (
219
+ <div className="border-b border-slate-100">
220
+ <div className="max-w-4xl mx-auto px-4 py-2 flex items-center gap-2 overflow-x-auto no-scrollbar">
221
+ {FILTER_PILLS.map(pill => (
222
+ <button
223
+ key={pill}
224
+ onClick={() => toggleFilter(pill)}
225
+ className={`shrink-0 text-xs font-medium px-3 py-1.5 rounded-full border transition-all ${
226
+ activeFilters.includes(pill)
227
+ ? 'border-primary bg-primary/10 text-primary font-bold'
228
+ : 'border-slate-200 text-slate-600 hover:border-slate-300'
229
+ }`}
230
+ >
231
+ {pill}
232
+ </button>
233
+ ))}
234
+ </div>
235
  </div>
236
  )}
237
 
238
+ {/* ===== RESULTS AREA ===== */}
239
+ <div className="max-w-4xl mx-auto px-4 py-4">
240
+ {/* Result count */}
241
+ {tab !== 'ai' && !loading && (
242
+ <div className="text-sm text-slate-500 mb-4">
243
+ {tab === 'all'
244
+ ? `About ${filteredProducts.length} results for "${query}"`
245
+ : `About ${videos.length} videos for "${query}"`
246
+ }
247
+ </div>
248
+ )}
249
+
250
+ {/* === AI MODE TAB === */}
251
+ {tab === 'ai' && (
252
+ <div className="space-y-4">
253
+ {/* AI Overview card */}
254
+ <Card className="p-4 border-slate-100 bg-gradient-to-br from-cyan-50 to-white">
255
+ <div className="flex items-center gap-2 mb-3">
256
+ <div className="w-7 h-7 rounded-lg brand-gradient flex items-center justify-center">
257
+ <Sparkles className="w-4 h-4 text-white" />
258
+ </div>
259
+ <span className="font-bold text-sm text-black">AI Overview</span>
260
+ </div>
261
+ {aiLoading ? (
262
+ <div className="space-y-2">
263
+ <div className="h-4 bg-slate-100 rounded animate-pulse" />
264
+ <div className="h-4 bg-slate-100 rounded animate-pulse w-5/6" />
265
+ <div className="h-4 bg-slate-100 rounded animate-pulse w-4/6" />
266
+ </div>
267
+ ) : (
268
+ <div className="text-sm text-slate-700 leading-relaxed whitespace-pre-wrap">
269
+ {aiAnswer || 'Searching...'}
270
+ </div>
271
+ )}
272
+ </Card>
273
+
274
+ {/* AI-recommended products */}
275
+ {aiProducts.length > 0 && (
276
+ <div>
277
+ <h3 className="font-bold text-sm text-black mb-2">Top products for "{query}"</h3>
278
+ <div className="space-y-3">
279
+ {aiProducts.map(p => (
280
+ <GoogleStyleResult key={p.id} product={p} />
281
+ ))}
282
+ </div>
283
+ </div>
284
+ )}
285
+ </div>
286
+ )}
287
+
288
+ {/* === ALL TAB (Google-style vertical list) === */}
289
+ {tab === 'all' && (
290
+ <div>
291
+ {loading ? (
292
+ <div className="space-y-4">
293
+ {Array.from({ length: 5 }).map((_, i) => (
294
+ <div key={i} className="flex gap-3 animate-pulse">
295
+ <div className="w-20 h-20 bg-slate-100 rounded-lg shrink-0" />
296
+ <div className="flex-1 space-y-2">
297
+ <div className="h-4 bg-slate-100 rounded w-3/4" />
298
+ <div className="h-3 bg-slate-100 rounded w-1/2" />
299
+ <div className="h-3 bg-slate-100 rounded" />
300
+ </div>
301
+ </div>
302
+ ))}
303
+ </div>
304
+ ) : filteredProducts.length === 0 ? (
305
+ <div className="text-center py-16">
306
+ <Search className="w-10 h-10 mx-auto text-slate-300 mb-2" />
307
+ <p className="text-sm text-slate-500">No products found for "{query}"</p>
308
+ <p className="text-xs text-slate-400 mt-1">Try different keywords or remove filters</p>
309
+ </div>
310
+ ) : (
311
+ <div className="space-y-4">
312
+ {filteredProducts.map(p => (
313
+ <GoogleStyleResult key={p.id} product={p} />
314
+ ))}
315
+ </div>
316
+ )}
317
+ </div>
318
+ )}
319
+
320
+ {/* === VIDEOS TAB === */}
321
+ {tab === 'videos' && (
322
+ <div>
323
+ {loading ? (
324
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
325
+ {Array.from({ length: 6 }).map((_, i) => (
326
+ <div key={i} className="bg-slate-100 rounded-lg animate-pulse aspect-[9/16]" />
327
+ ))}
328
+ </div>
329
+ ) : videos.length === 0 ? (
330
+ <div className="text-center py-16">
331
+ <VideoIcon className="w-10 h-10 mx-auto text-slate-300 mb-2" />
332
+ <p className="text-sm text-slate-500">No videos found for "{query}"</p>
333
+ </div>
334
+ ) : (
335
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
336
+ {videos.map(v => {
337
+ const seller = v.seller || {};
338
+ const sellerName = seller.business_name || 'Seller';
339
+ const product = v.product;
340
+ return (
341
+ <Link key={v.id} href="/videos" className="block group">
342
+ <Card className="overflow-hidden border-slate-100 hover:shadow-md transition-shadow">
343
+ <div className="aspect-[9/16] bg-slate-900 relative">
344
+ {v.video_url ? (
345
+ <video src={v.video_url} muted className="w-full h-full object-cover" />
346
+ ) : (
347
+ <div className="w-full h-full flex items-center justify-center">
348
+ <Play className="w-8 h-8 text-white/50" />
349
+ </div>
350
+ )}
351
+ <div className="absolute bottom-1 right-1 bg-black/60 text-white text-[10px] px-1.5 py-0.5 rounded">
352
+ ▶ {v.views_count || 0}
353
+ </div>
354
+ </div>
355
+ <div className="p-2">
356
+ <div className="text-xs font-medium text-black line-clamp-2 h-8 leading-tight">{v.caption || 'Video'}</div>
357
+ <div className="flex items-center gap-1 mt-1">
358
+ <div className="w-4 h-4 rounded-full brand-gradient flex items-center justify-center text-white text-[8px] font-bold shrink-0">
359
+ {sellerName.charAt(0)}
360
+ </div>
361
+ <span className="text-[10px] text-slate-500 truncate">@{sellerName}</span>
362
+ </div>
363
+ {product && (
364
+ <div className="text-xs font-bold price mt-0.5">{formatPrice(product.price)}</div>
365
+ )}
366
+ </div>
367
+ </Card>
368
+ </Link>
369
+ );
370
+ })}
371
+ </div>
372
+ )}
373
+ </div>
374
+ )}
375
+ </div>
376
  </div>
377
  );
378
  }
379
 
380
+ /* Google-style search result (thumbnail left + title/link + price + description + seller) */
381
+ function GoogleStyleResult({ product }: { product: Product }) {
382
+ return (
383
+ <Link href={`/product?id=${product.id}`} className="flex gap-3 group">
384
+ {/* Thumbnail */}
385
+ <div className="w-24 h-24 sm:w-28 sm:h-28 rounded-lg overflow-hidden bg-slate-50 shrink-0">
386
+ {product.image_url ? (
387
+ <img src={product.image_url} alt={product.name} className="w-full h-full object-cover" loading="lazy" />
388
+ ) : (
389
+ <div className="w-full h-full flex items-center justify-center text-slate-300">
390
+ <Store className="w-8 h-8" />
391
+ </div>
392
+ )}
393
+ </div>
394
+
395
+ {/* Text content */}
396
+ <div className="flex-1 min-w-0">
397
+ {/* Seller "URL" (like Google's green URL) */}
398
+ <div className="text-xs text-slate-500 flex items-center gap-1 mb-0.5">
399
+ <Store className="w-3 h-3" />
400
+ {product.category || 'Cellex'} · Verified Seller
401
+ </div>
402
+
403
+ {/* Title (link blue/cyan, like Google) */}
404
+ <h3 className="text-base font-medium text-primary group-hover:underline leading-snug line-clamp-2">
405
+ {product.name}
406
+ </h3>
407
+
408
+ {/* Price (cyan, bold) */}
409
+ <div className="text-lg font-bold price mt-0.5">{formatPrice(product.price)}</div>
410
+
411
+ {/* Description (gray, 2 lines) */}
412
+ {product.description && (
413
+ <p className="text-sm text-slate-600 line-clamp-2 mt-0.5 leading-snug">
414
+ {product.description}
415
+ </p>
416
+ )}
417
+
418
+ {/* Meta row: sold count + rating */}
419
+ <div className="flex items-center gap-3 mt-1 text-xs text-slate-400">
420
+ {typeof product.units_sold === 'number' && product.units_sold > 0 && (
421
+ <span>{product.units_sold} sold</span>
422
+ )}
423
+ <span className="flex items-center gap-0.5">
424
+ <Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
425
+ 4.5
426
+ </span>
427
+ <span className="flex items-center gap-0.5 text-green-600">
428
+ <ShoppingBag className="w-3 h-3" /> Pay on delivery
429
+ </span>
430
+ </div>
431
+ </div>
432
+ </Link>
433
+ );
434
+ }
435
+
436
  export default function SearchPage() {
437
  return (
438
  <Suspense fallback={<div className="flex items-center justify-center min-h-[60vh]"><div className="w-8 h-8 border-3 border-slate-200 border-t-primary rounded-full animate-spin" /></div>}>
 
440
  </Suspense>
441
  );
442
  }