Skip to content

Commit c7df5e6

Browse files
amide-initclaude
andcommitted
feat: add portfolio preview modal for searched GitHub users
After searching a GitHub username, a 'Preview portfolio' button appears in the search results panel. Clicking it opens a full-screen modal that renders a faithful threejs-style portfolio preview using the user's public GitHub data: - Preview banner bar with username and 'Fork & make yours' CTA - Navbar replica with user initials and nav labels - Hero section: avatar with glow ring, gradient name, bio, location, website, X/Twitter link, stats badges (repos/followers/following), 'View on GitHub' and 'Fork & build yours' buttons - Top repos grid (up to 6) with language colour dots and star counts - Footer CTA: 'Fork Gitfolio — deploy in 30 seconds' section Uses CSS dot-grid and canvas scan-line animation instead of WebGL to avoid exceeding the browser WebGL context limit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dfcc6e2 commit c7df5e6

2 files changed

Lines changed: 410 additions & 29 deletions

File tree

src/components/NavSearch.tsx

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,13 @@
11
import { useState, useRef, useEffect, useCallback } from 'react'
2-
import { Search, X, Star, Loader2, ExternalLink, MapPin, GitFork } from 'lucide-react'
2+
import { Search, X, Star, Loader2, ExternalLink, MapPin, GitFork, Eye } from 'lucide-react'
3+
import { PortfolioPreviewModal } from './PortfolioPreviewModal'
4+
import type { PreviewUser, PreviewRepo } from './PortfolioPreviewModal'
35

46
// ── GitHub API types ──────────────────────────────────────────────────────────
57

6-
type GHUser = {
7-
login: string
8-
name: string | null
9-
avatar_url: string
10-
bio: string | null
11-
html_url: string
12-
public_repos: number
13-
followers: number
14-
following: number
15-
location: string | null
16-
}
8+
type GHUser = PreviewUser
179

18-
type GHRepo = {
19-
id: number
20-
name: string
21-
html_url: string
22-
description: string | null
23-
stargazers_count: number
24-
language: string | null
25-
fork: boolean
26-
}
10+
type GHRepo = PreviewRepo
2711

2812
type SearchState =
2913
| { status: 'idle' }
@@ -64,6 +48,7 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) {
6448
const [open, setOpen] = useState(false)
6549
const [query, setQuery] = useState('')
6650
const [state, setState] = useState<SearchState>({ status: 'idle' })
51+
const [previewOpen, setPreviewOpen] = useState(false)
6752
const inputRef = useRef<HTMLInputElement>(null)
6853
const containerRef = useRef<HTMLDivElement>(null)
6954

@@ -93,6 +78,7 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) {
9378
setOpen(false)
9479
setQuery('')
9580
setState({ status: 'idle' })
81+
setPreviewOpen(false)
9682
}
9783

9884
const search = useCallback(async (username: string) => {
@@ -111,7 +97,7 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) {
11197
const repos = allRepos
11298
.filter((r) => !r.fork)
11399
.sort((a, b) => b.stargazers_count - a.stargazers_count)
114-
.slice(0, 4)
100+
.slice(0, 6)
115101
setState({ status: 'done', user, repos })
116102
} catch (err) {
117103
setState({ status: 'error', message: err instanceof Error ? err.message : 'Search failed' })
@@ -286,18 +272,42 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) {
286272
)}
287273

288274
{/* CTA footer */}
289-
<div className={`flex gap-2 border-t p-3 ${dividerCls}`}>
290-
<a href={state.user.html_url} target="_blank" rel="noreferrer" className={viewBtnCls}>
291-
<ExternalLink className="h-3 w-3" /> View GitHub
292-
</a>
293-
<a href={FORK_URL} target="_blank" rel="noreferrer" className={forkBtnCls}>
294-
<GitFork className="h-3 w-3" /> Fork this template
295-
</a>
275+
<div className={`border-t p-3 space-y-2 ${dividerCls}`}>
276+
{/* Preview CTA — full width, prominent */}
277+
<button
278+
type="button"
279+
onClick={() => setPreviewOpen(true)}
280+
className={`w-full flex items-center justify-center gap-1.5 rounded-md py-2 text-xs font-semibold transition ${
281+
isThreejs
282+
? 'bg-blue-600/20 border border-blue-500/40 text-blue-300 hover:bg-blue-600/30'
283+
: 'bg-blue-50 border border-blue-200 text-blue-600 hover:bg-blue-100 dark:bg-blue-600/20 dark:border-blue-500/40 dark:text-blue-300 dark:hover:bg-blue-600/30'
284+
}`}
285+
>
286+
<Eye className="h-3.5 w-3.5" /> Preview portfolio
287+
</button>
288+
{/* Secondary row */}
289+
<div className="flex gap-2">
290+
<a href={state.user.html_url} target="_blank" rel="noreferrer" className={viewBtnCls}>
291+
<ExternalLink className="h-3 w-3" /> View GitHub
292+
</a>
293+
<a href={FORK_URL} target="_blank" rel="noreferrer" className={forkBtnCls}>
294+
<GitFork className="h-3 w-3" /> Fork template
295+
</a>
296+
</div>
296297
</div>
297298
</>
298299
)}
299300
</div>
300301
)}
302+
303+
{/* Full-screen portfolio preview modal */}
304+
{previewOpen && state.status === 'done' && (
305+
<PortfolioPreviewModal
306+
user={state.user}
307+
repos={state.repos}
308+
onClose={() => setPreviewOpen(false)}
309+
/>
310+
)}
301311
</div>
302312
)
303313
}

0 commit comments

Comments
 (0)