Skip to content

Commit 7dfe5cd

Browse files
committed
Add link sharing functionality to PortfolioPreviewModal and improve NavSearch preview handling
1 parent dd1659e commit 7dfe5cd

File tree

2 files changed

+65
-11
lines changed

2 files changed

+65
-11
lines changed

src/components/NavSearch.tsx

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState, useRef, useEffect, useCallback } from 'react'
2+
import { useSearchParams } from 'react-router-dom'
23
import { Search, X, Star, Loader2, ExternalLink, MapPin, GitFork, Eye } from 'lucide-react'
34
import { PortfolioPreviewModal } from './PortfolioPreviewModal'
45
import type { PreviewUser, PreviewRepo } from './PortfolioPreviewModal'
@@ -45,12 +46,33 @@ type NavSearchProps = {
4546
}
4647

4748
export function NavSearch({ variant = 'default' }: NavSearchProps) {
49+
const [searchParams, setSearchParams] = useSearchParams()
4850
const [open, setOpen] = useState(false)
4951
const [query, setQuery] = useState('')
5052
const [state, setState] = useState<SearchState>({ status: 'idle' })
5153
const [previewOpen, setPreviewOpen] = useState(false)
5254
const inputRef = useRef<HTMLInputElement>(null)
5355
const containerRef = useRef<HTMLDivElement>(null)
56+
const autoPreviewRef = useRef(searchParams.get('preview'))
57+
58+
const openPreview = () => {
59+
if (state.status !== 'done') return
60+
setSearchParams({ preview: state.user.login }, { replace: true })
61+
setPreviewOpen(true)
62+
}
63+
64+
const closePreview = () => {
65+
setSearchParams({}, { replace: true })
66+
setPreviewOpen(false)
67+
}
68+
69+
const close = () => {
70+
setOpen(false)
71+
setQuery('')
72+
setState({ status: 'idle' })
73+
setSearchParams({}, { replace: true })
74+
setPreviewOpen(false)
75+
}
5476

5577
// Close dropdown when clicking outside — but NOT when the preview modal is open
5678
// (the modal is a portal outside containerRef; clicking inside it must not close the search)
@@ -68,7 +90,7 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) {
6890
useEffect(() => {
6991
const onKey = (e: KeyboardEvent) => {
7092
if (e.key !== 'Escape') return
71-
if (previewOpen) { setPreviewOpen(false) } else { close() }
93+
if (previewOpen) { closePreview() } else { close() }
7294
}
7395
window.addEventListener('keydown', onKey)
7496
return () => window.removeEventListener('keydown', onKey)
@@ -79,13 +101,6 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) {
79101
if (open) setTimeout(() => inputRef.current?.focus(), 50)
80102
}, [open])
81103

82-
const close = () => {
83-
setOpen(false)
84-
setQuery('')
85-
setState({ status: 'idle' })
86-
setPreviewOpen(false)
87-
}
88-
89104
const search = useCallback(async (username: string) => {
90105
const name = username.trim()
91106
if (!name) return
@@ -107,6 +122,28 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) {
107122
}
108123
}, [])
109124

125+
// Auto-fetch from ?preview=username on mount
126+
useEffect(() => {
127+
const username = autoPreviewRef.current
128+
if (username) {
129+
setQuery(username)
130+
void search(username)
131+
}
132+
// eslint-disable-next-line react-hooks/exhaustive-deps
133+
}, [])
134+
135+
// Auto-open modal once the auto-fetch completes
136+
useEffect(() => {
137+
if (
138+
autoPreviewRef.current &&
139+
state.status === 'done' &&
140+
state.user.login.toLowerCase() === autoPreviewRef.current.toLowerCase()
141+
) {
142+
setPreviewOpen(true)
143+
autoPreviewRef.current = null
144+
}
145+
}, [state])
146+
110147
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
111148
if (e.key === 'Enter') void search(query)
112149
}
@@ -279,7 +316,7 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) {
279316
{/* Preview CTA — full width, prominent */}
280317
<button
281318
type="button"
282-
onClick={() => setPreviewOpen(true)}
319+
onClick={openPreview}
283320
className={`w-full flex items-center justify-center gap-1.5 rounded-md py-2 text-xs font-semibold transition ${
284321
isThreejs
285322
? 'bg-blue-600/20 border border-blue-500/40 text-blue-300 hover:bg-blue-600/30'
@@ -288,6 +325,7 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) {
288325
>
289326
<Eye className="h-3.5 w-3.5" /> Preview portfolio
290327
</button>
328+
291329
{/* Secondary row */}
292330
<div className="flex gap-2">
293331
<a href={state.user.html_url} target="_blank" rel="noreferrer" className={viewBtnCls}>
@@ -309,7 +347,7 @@ export function NavSearch({ variant = 'default' }: NavSearchProps) {
309347
user={state.user}
310348
repos={state.repos}
311349
allRepos={state.allRepos}
312-
onClose={() => setPreviewOpen(false)}
350+
onClose={closePreview}
313351
/>
314352
)}
315353
</div>

src/components/PortfolioPreviewModal.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useState } from 'react'
22
import { createPortal } from 'react-dom'
3-
import { X, GitFork, Copy, Check, ExternalLink } from 'lucide-react'
3+
import { X, GitFork, Copy, Check, ExternalLink, Link2 } from 'lucide-react'
44
import HeroSection from '../templates/threejs/HeroSection'
55
import GitHubSection from '../templates/threejs/GitHubSection'
66
import StatsSection from '../templates/threejs/StatsSection'
@@ -272,13 +272,21 @@ type Props = {
272272
}
273273

274274
export function PortfolioPreviewModal({ user, repos, allRepos, onClose }: Props) {
275+
const [linkCopied, setLinkCopied] = useState(false)
276+
275277
// Lock body scroll
276278
useEffect(() => {
277279
const prev = document.body.style.overflow
278280
document.body.style.overflow = 'hidden'
279281
return () => { document.body.style.overflow = prev }
280282
}, [])
281283

284+
const copyLink = async () => {
285+
await navigator.clipboard.writeText(window.location.href).catch(() => {})
286+
setLinkCopied(true)
287+
setTimeout(() => setLinkCopied(false), 2000)
288+
}
289+
282290
// Escape is handled by NavSearch (it dismisses preview first, then closes search)
283291
// No duplicate listener here.
284292

@@ -306,6 +314,14 @@ export function PortfolioPreviewModal({ user, repos, allRepos, onClose }: Props)
306314
</span>
307315
</div>
308316
<div className="flex items-center gap-2">
317+
<button
318+
type="button"
319+
onClick={copyLink}
320+
className="hidden sm:inline-flex items-center gap-1.5 rounded-md border border-zinc-700 bg-zinc-800/60 px-3 py-1.5 text-xs font-semibold text-zinc-300 transition hover:bg-zinc-700"
321+
>
322+
{linkCopied ? <Check className="h-3.5 w-3.5 text-green-400" /> : <Link2 className="h-3.5 w-3.5" />}
323+
{linkCopied ? 'Copied!' : 'Share link'}
324+
</button>
309325
<ForkButton
310326
login={user.login}
311327
className="hidden sm:inline-flex items-center gap-1.5 rounded-md border border-indigo-500/40 bg-indigo-500/10 px-3 py-1.5 text-xs font-semibold text-indigo-300 transition hover:bg-indigo-500/20"

0 commit comments

Comments
 (0)