|
1 | 1 | import { useState, useRef, useEffect, useCallback } from 'react' |
| 2 | +import { createPortal } from 'react-dom' |
2 | 3 | import { useSearchParams } from 'react-router-dom' |
3 | 4 | import { Search, X, Star, Loader2, ExternalLink, MapPin, GitFork, Eye } from 'lucide-react' |
4 | 5 | import { PortfolioPreviewModal } from './PortfolioPreviewModal' |
@@ -54,6 +55,8 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) { |
54 | 55 | const inputRef = useRef<HTMLInputElement>(null) |
55 | 56 | const containerRef = useRef<HTMLDivElement>(null) |
56 | 57 | const autoPreviewRef = useRef(searchParams.get('preview')) |
| 58 | + // true while auto-fetching from a shared ?preview= link |
| 59 | + const [autoLoading, setAutoLoading] = useState(!!autoPreviewRef.current) |
57 | 60 |
|
58 | 61 | const openPreview = () => { |
59 | 62 | if (state.status !== 'done') return |
@@ -132,17 +135,17 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) { |
132 | 135 | // eslint-disable-next-line react-hooks/exhaustive-deps |
133 | 136 | }, []) |
134 | 137 |
|
135 | | - // Auto-open modal once the auto-fetch completes |
| 138 | + // Auto-open modal once the auto-fetch completes; clear loader on done or error |
136 | 139 | useEffect(() => { |
137 | | - if ( |
138 | | - autoPreviewRef.current && |
139 | | - state.status === 'done' && |
140 | | - state.user.login.toLowerCase() === autoPreviewRef.current.toLowerCase() |
141 | | - ) { |
| 140 | + if (!autoLoading) return |
| 141 | + if (state.status === 'done') { |
142 | 142 | setPreviewOpen(true) |
| 143 | + setAutoLoading(false) |
143 | 144 | autoPreviewRef.current = null |
| 145 | + } else if (state.status === 'error') { |
| 146 | + setAutoLoading(false) |
144 | 147 | } |
145 | | - }, [state]) |
| 148 | + }, [state, autoLoading]) |
146 | 149 |
|
147 | 150 | const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { |
148 | 151 | if (e.key === 'Enter') void search(query) |
@@ -341,6 +344,57 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) { |
341 | 344 | </div> |
342 | 345 | )} |
343 | 346 |
|
| 347 | + {/* Full-screen loading overlay — shown while auto-fetching from a shared link */} |
| 348 | + {autoLoading && createPortal( |
| 349 | + <div className="fixed inset-0 z-[200] flex flex-col items-center justify-center bg-[#050509]"> |
| 350 | + {/* Ambient glow */} |
| 351 | + <div |
| 352 | + className="pointer-events-none absolute inset-0" |
| 353 | + style={{ |
| 354 | + background: 'radial-gradient(ellipse 60% 40% at 50% 50%, rgba(59,130,246,0.07) 0%, transparent 70%)', |
| 355 | + }} |
| 356 | + /> |
| 357 | + |
| 358 | + <div className="relative z-10 flex flex-col items-center gap-6 text-center px-6"> |
| 359 | + {/* Spinner ring */} |
| 360 | + <div className="relative h-16 w-16"> |
| 361 | + <div className="absolute inset-0 rounded-full border-2 border-blue-500/15" /> |
| 362 | + <div |
| 363 | + className="absolute inset-0 rounded-full border-2 border-transparent border-t-blue-400" |
| 364 | + style={{ animation: 'spin 0.9s linear infinite' }} |
| 365 | + /> |
| 366 | + <div className="absolute inset-[6px] rounded-full border border-cyan-500/20" /> |
| 367 | + </div> |
| 368 | + |
| 369 | + {/* Label */} |
| 370 | + <div className="flex flex-col items-center gap-1.5"> |
| 371 | + <p className="text-[10px] font-bold uppercase tracking-[0.35em] text-blue-400/70"> |
| 372 | + Loading portfolio |
| 373 | + </p> |
| 374 | + <p className="text-lg font-semibold text-zinc-100"> |
| 375 | + @{searchParams.get('preview') ?? query} |
| 376 | + </p> |
| 377 | + {state.status === 'loading' && ( |
| 378 | + <p className="text-xs text-zinc-600 mt-1">Fetching GitHub data…</p> |
| 379 | + )} |
| 380 | + {state.status === 'error' && ( |
| 381 | + <p className="text-xs text-red-400 mt-1">{(state as { status: 'error'; message: string }).message}</p> |
| 382 | + )} |
| 383 | + </div> |
| 384 | + |
| 385 | + {/* Dismiss */} |
| 386 | + <button |
| 387 | + type="button" |
| 388 | + onClick={() => { setAutoLoading(false); setSearchParams({}, { replace: true }) }} |
| 389 | + className="mt-2 text-[11px] text-zinc-600 hover:text-zinc-400 transition-colors" |
| 390 | + > |
| 391 | + Cancel |
| 392 | + </button> |
| 393 | + </div> |
| 394 | + </div>, |
| 395 | + document.body |
| 396 | + )} |
| 397 | + |
344 | 398 | {/* Full-screen portfolio preview modal */} |
345 | 399 | {previewOpen && state.status === 'done' && ( |
346 | 400 | <PortfolioPreviewModal |
|
0 commit comments