eeshaAI commited on
Commit
61e536d
·
1 Parent(s): f896714

feat: Google-style top bar + smart FAB visibility

Browse files
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "nextjs_tailwind_shadcn_ts",
3
- "version": "0.2.13",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev -p 3000 2>&1 | tee dev.log",
 
1
  {
2
  "name": "nextjs_tailwind_shadcn_ts",
3
+ "version": "0.2.14",
4
  "private": true,
5
  "scripts": {
6
  "dev": "next dev -p 3000 2>&1 | tee dev.log",
src/app/ai-chat/loading.tsx CHANGED
File without changes
src/app/cart/loading.tsx CHANGED
File without changes
src/app/categories/loading.tsx CHANGED
File without changes
src/app/categories/page.tsx CHANGED
@@ -1,6 +1,6 @@
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';
@@ -55,6 +55,24 @@ function CategoriesContent() {
55
  const [showSortMenu, setShowSortMenu] = useState(false);
56
  const [showSearch, setShowSearch] = useState(false);
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  const load = useCallback(async () => {
59
  setLoading(true);
60
  if (searchQuery.trim()) {
@@ -143,7 +161,7 @@ function CategoriesContent() {
143
  {/* === MOBILE LAYOUT === */}
144
  <div className="md:hidden">
145
  {/* Search bar — full width with back arrow */}
146
- <div className="sticky top-0 z-30 bg-white px-3 pt-3 pb-2 border-b border-slate-100">
147
  <div className="flex items-center gap-2">
148
  <Link href="/" className="shrink-0">
149
  <ChevronLeft className="w-5 h-5 text-black" />
 
1
  'use client';
2
 
3
+ import { useEffect, useState, useCallback, useRef, 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';
 
55
  const [showSortMenu, setShowSortMenu] = useState(false);
56
  const [showSearch, setShowSearch] = useState(false);
57
 
58
+ // Ref for the top search bar — dispatches visibility events to GlobalSpotlight
59
+ const searchBarRef = useRef<HTMLDivElement>(null);
60
+
61
+ useEffect(() => {
62
+ const el = searchBarRef.current;
63
+ if (!el) return;
64
+ const observer = new IntersectionObserver(
65
+ ([entry]) => {
66
+ window.dispatchEvent(
67
+ new CustomEvent('searchbar-visibility', { detail: { visible: entry.isIntersecting } })
68
+ );
69
+ },
70
+ { threshold: 0 }
71
+ );
72
+ observer.observe(el);
73
+ return () => observer.disconnect();
74
+ }, []);
75
+
76
  const load = useCallback(async () => {
77
  setLoading(true);
78
  if (searchQuery.trim()) {
 
161
  {/* === MOBILE LAYOUT === */}
162
  <div className="md:hidden">
163
  {/* Search bar — full width with back arrow */}
164
+ <div ref={searchBarRef} className="sticky top-0 z-30 bg-white px-3 pt-3 pb-2 border-b border-slate-100">
165
  <div className="flex items-center gap-2">
166
  <Link href="/" className="shrink-0">
167
  <ChevronLeft className="w-5 h-5 text-black" />
src/app/checkout/loading.tsx CHANGED
File without changes
src/app/group-buy/loading.tsx CHANGED
File without changes
src/app/link-account/loading.tsx CHANGED
File without changes
src/app/live-watch/loading.tsx CHANGED
File without changes
src/app/live/loading.tsx CHANGED
File without changes
src/app/login/loading.tsx CHANGED
File without changes
src/app/orders/loading.tsx CHANGED
File without changes
src/app/page.tsx CHANGED
@@ -48,6 +48,31 @@ export default function HomePage() {
48
  const [following, setFollowing] = useState<Set<string>>(new Set());
49
  const [liveCount, setLiveCount] = useState(0);
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  useEffect(() => {
52
  (async () => {
53
  try {
@@ -205,16 +230,38 @@ export default function HomePage() {
205
 
206
  return (
207
  <div className="bg-white min-h-screen max-w-xl mx-auto">
208
- {/* Search trigger */}
209
- <div className="px-3 pt-3 pb-2 bg-white sticky top-0 z-30">
 
 
 
 
 
 
 
 
 
 
 
210
  <button
211
  onClick={() => window.dispatchEvent(new CustomEvent('open-spotlight'))}
212
- className="w-full flex items-center bg-neutral-100 rounded-full px-4 py-2.5 hover:bg-neutral-200 transition-colors group"
213
  >
214
  <Search className="w-4 h-4 text-neutral-400 mr-2 group-hover:text-black transition-colors" />
215
  <span className="flex-1 text-left text-sm text-neutral-400">Search products, categories...</span>
216
  <kbd className="hidden sm:flex px-1.5 py-0.5 bg-white rounded text-[9px] font-bold text-neutral-400 border border-neutral-200">⌘K</kbd>
217
  </button>
 
 
 
 
 
 
 
 
 
 
 
218
  </div>
219
 
220
  {/* Stories bar */}
 
48
  const [following, setFollowing] = useState<Set<string>>(new Set());
49
  const [liveCount, setLiveCount] = useState(0);
50
 
51
+ // Ref for the top search bar — used to detect when it's scrolled out of view
52
+ // so the GlobalSpotlight floating search button can appear.
53
+ const searchBarRef = useRef<HTMLDivElement>(null);
54
+
55
+ // IntersectionObserver: when the top search bar leaves the viewport,
56
+ // dispatch an event that GlobalSpotlight listens for to show/hide the FAB.
57
+ useEffect(() => {
58
+ const el = searchBarRef.current;
59
+ if (!el) return;
60
+
61
+ const observer = new IntersectionObserver(
62
+ ([entry]) => {
63
+ window.dispatchEvent(
64
+ new CustomEvent('searchbar-visibility', {
65
+ detail: { visible: entry.isIntersecting },
66
+ })
67
+ );
68
+ },
69
+ { threshold: 0 }
70
+ );
71
+
72
+ observer.observe(el);
73
+ return () => observer.disconnect();
74
+ }, []);
75
+
76
  useEffect(() => {
77
  (async () => {
78
  try {
 
230
 
231
  return (
232
  <div className="bg-white min-h-screen max-w-xl mx-auto">
233
+ {/* Top bar: Logo + Search + Messenger (Google-style) */}
234
+ <div
235
+ ref={searchBarRef}
236
+ className="px-3 pt-3 pb-2 bg-white sticky top-0 z-30 flex items-center gap-2"
237
+ >
238
+ {/* Logo (left side) */}
239
+ <Link href="/" className="shrink-0 flex items-center gap-1.5">
240
+ <div className="w-8 h-8 rounded-lg bg-black flex items-center justify-center">
241
+ <span className="text-white font-extrabold text-base" style={{ fontFamily: 'var(--font-geist-mono)' }}>C</span>
242
+ </div>
243
+ </Link>
244
+
245
+ {/* Search bar (center, flex-1) */}
246
  <button
247
  onClick={() => window.dispatchEvent(new CustomEvent('open-spotlight'))}
248
+ className="flex-1 flex items-center bg-neutral-100 rounded-full px-4 py-2.5 hover:bg-neutral-200 transition-colors group"
249
  >
250
  <Search className="w-4 h-4 text-neutral-400 mr-2 group-hover:text-black transition-colors" />
251
  <span className="flex-1 text-left text-sm text-neutral-400">Search products, categories...</span>
252
  <kbd className="hidden sm:flex px-1.5 py-0.5 bg-white rounded text-[9px] font-bold text-neutral-400 border border-neutral-200">⌘K</kbd>
253
  </button>
254
+
255
+ {/* Messenger icon (right side) */}
256
+ <Link
257
+ href="/ai-chat"
258
+ className="shrink-0 w-10 h-10 rounded-full hover:bg-neutral-100 flex items-center justify-center transition-colors relative"
259
+ aria-label="Messages"
260
+ >
261
+ <MessageCircle className="w-5 h-5 text-black" />
262
+ {/* Unread indicator dot */}
263
+ <span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full border border-white" />
264
+ </Link>
265
  </div>
266
 
267
  {/* Stories bar */}
src/app/payment/loading.tsx CHANGED
File without changes
src/app/product/loading.tsx CHANGED
File without changes
src/app/profile/loading.tsx CHANGED
File without changes
src/app/search/loading.tsx CHANGED
File without changes
src/app/search/page.tsx CHANGED
@@ -1,6 +1,6 @@
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, API_BASE } from '@/lib/api';
6
  import { Card } from '@/components/ui/card';
@@ -27,6 +27,24 @@ function SearchContent() {
27
  const [followUpLoading, setFollowUpLoading] = useState(false);
28
  const [view, setView] = useState<'ai' | 'products' | 'videos'>('ai');
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  const doSearch = useCallback(async (q: string) => {
31
  if (!q.trim()) return;
32
  setLoading(true);
@@ -122,7 +140,7 @@ function SearchContent() {
122
  return (
123
  <div className="bg-white min-h-screen">
124
  {/* Header */}
125
- <div className="border-b border-neutral-200 sticky top-0 z-40 bg-white/95 backdrop-blur-xl">
126
  <div className="max-w-3xl mx-auto px-4 py-3 flex items-center gap-3">
127
  <button onClick={() => router.back()} className="shrink-0 w-9 h-9 rounded-full hover:bg-neutral-100 flex items-center justify-center transition-colors">
128
  <ChevronLeft className="w-5 h-5 text-black" />
 
1
  'use client';
2
 
3
+ import { useEffect, useState, useCallback, useRef, Suspense } from 'react';
4
  import { useSearchParams, useRouter } from 'next/navigation';
5
  import { api, formatPrice, type Product, API_BASE } from '@/lib/api';
6
  import { Card } from '@/components/ui/card';
 
27
  const [followUpLoading, setFollowUpLoading] = useState(false);
28
  const [view, setView] = useState<'ai' | 'products' | 'videos'>('ai');
29
 
30
+ // Ref for the top search bar — dispatches visibility events to GlobalSpotlight
31
+ const searchBarRef = useRef<HTMLDivElement>(null);
32
+
33
+ useEffect(() => {
34
+ const el = searchBarRef.current;
35
+ if (!el) return;
36
+ const observer = new IntersectionObserver(
37
+ ([entry]) => {
38
+ window.dispatchEvent(
39
+ new CustomEvent('searchbar-visibility', { detail: { visible: entry.isIntersecting } })
40
+ );
41
+ },
42
+ { threshold: 0 }
43
+ );
44
+ observer.observe(el);
45
+ return () => observer.disconnect();
46
+ }, []);
47
+
48
  const doSearch = useCallback(async (q: string) => {
49
  if (!q.trim()) return;
50
  setLoading(true);
 
140
  return (
141
  <div className="bg-white min-h-screen">
142
  {/* Header */}
143
+ <div ref={searchBarRef} className="border-b border-neutral-200 sticky top-0 z-40 bg-white/95 backdrop-blur-xl">
144
  <div className="max-w-3xl mx-auto px-4 py-3 flex items-center gap-3">
145
  <button onClick={() => router.back()} className="shrink-0 w-9 h-9 rounded-full hover:bg-neutral-100 flex items-center justify-center transition-colors">
146
  <ChevronLeft className="w-5 h-5 text-black" />
src/app/seller-profile/loading.tsx CHANGED
File without changes
src/app/seller/loading.tsx CHANGED
File without changes
src/app/telegram/loading.tsx CHANGED
File without changes
src/app/videos/loading.tsx CHANGED
File without changes
src/app/wishlist/loading.tsx CHANGED
File without changes
src/components/global-spotlight.tsx CHANGED
@@ -3,6 +3,7 @@
3
  import { useSpotlight, SpotlightSearch } from '@/components/spotlight-search';
4
  import { usePathname } from 'next/navigation';
5
  import { Search } from 'lucide-react';
 
6
 
7
  /**
8
  * GlobalSpotlight — renders the SpotlightSearch overlay + a floating search trigger.
@@ -11,12 +12,18 @@ import { Search } from 'lucide-react';
11
  * - Shows a floating search button on mobile (bottom-right, above nav)
12
  * - The floating button is HIDDEN on pages where search doesn't make sense:
13
  * login, checkout, payment, seller dashboard (has its own nav)
 
 
 
 
 
14
  */
15
  export function GlobalSpotlight() {
16
  const { isOpen, setIsOpen } = useSpotlight();
17
  const pathname = usePathname();
 
18
 
19
- // Pages where the floating search button should NOT appear
20
  const hideOnRoutes = [
21
  '/login',
22
  '/checkout',
@@ -30,12 +37,30 @@ export function GlobalSpotlight() {
30
  pathname === route || pathname.startsWith(route + '/')
31
  );
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  return (
34
  <>
35
  <SpotlightSearch isOpen={isOpen} onClose={() => setIsOpen(false)} />
36
 
37
- {/* Floating search trigger (mobile only, hidden on auth/seller/checkout pages) */}
38
- {!isOpen && !shouldHideButton && (
39
  <button
40
  onClick={() => setIsOpen(true)}
41
  className="fixed bottom-20 right-4 z-40 w-12 h-12 rounded-full bg-black shadow-lg flex items-center justify-center md:hidden hover:scale-105 transition-transform"
 
3
  import { useSpotlight, SpotlightSearch } from '@/components/spotlight-search';
4
  import { usePathname } from 'next/navigation';
5
  import { Search } from 'lucide-react';
6
+ import { useEffect, useState } from 'react';
7
 
8
  /**
9
  * GlobalSpotlight — renders the SpotlightSearch overlay + a floating search trigger.
 
12
  * - Shows a floating search button on mobile (bottom-right, above nav)
13
  * - The floating button is HIDDEN on pages where search doesn't make sense:
14
  * login, checkout, payment, seller dashboard (has its own nav)
15
+ * - The floating button is ALSO hidden when a top search bar is visible on
16
+ * the current page (e.g. the homepage's sticky search bar). When the user
17
+ * scrolls past the top search bar, the floating button appears.
18
+ * This is controlled via a 'searchbar-visibility' CustomEvent that pages
19
+ * dispatch from an IntersectionObserver on their search bar element.
20
  */
21
  export function GlobalSpotlight() {
22
  const { isOpen, setIsOpen } = useSpotlight();
23
  const pathname = usePathname();
24
+ const [searchBarVisible, setSearchBarVisible] = useState(false);
25
 
26
+ // Pages where the floating search button should NOT appear at all
27
  const hideOnRoutes = [
28
  '/login',
29
  '/checkout',
 
37
  pathname === route || pathname.startsWith(route + '/')
38
  );
39
 
40
+ // Listen for search-bar visibility events from pages that have a top search bar.
41
+ // When the bar is visible on screen, hide the FAB. When scrolled past, show it.
42
+ useEffect(() => {
43
+ const handler = (e: Event) => {
44
+ const detail = (e as CustomEvent<{ visible: boolean }>).detail;
45
+ setSearchBarVisible(detail?.visible ?? false);
46
+ };
47
+ window.addEventListener('searchbar-visibility', handler);
48
+ return () => window.removeEventListener('searchbar-visibility', handler);
49
+ }, []);
50
+
51
+ // Reset visibility state when navigating to a new page
52
+ useEffect(() => {
53
+ setSearchBarVisible(false);
54
+ }, [pathname]);
55
+
56
+ const showFloatingButton = !isOpen && !shouldHideButton && !searchBarVisible;
57
+
58
  return (
59
  <>
60
  <SpotlightSearch isOpen={isOpen} onClose={() => setIsOpen(false)} />
61
 
62
+ {/* Floating search trigger (mobile only) */}
63
+ {showFloatingButton && (
64
  <button
65
  onClick={() => setIsOpen(true)}
66
  className="fixed bottom-20 right-4 z-40 w-12 h-12 rounded-full bg-black shadow-lg flex items-center justify-center md:hidden hover:scale-105 transition-transform"
src/components/page-skeleton.tsx CHANGED
File without changes