@@ -23,11 +23,35 @@ const RepositoryBrowser = () => {
2323 const [ expandedRepo , setExpandedRepo ] = useState ( null ) ; // Track which repo is expanded
2424 const [ readmeCache , setReadmeCache ] = useState ( { } ) ; // Cache README content
2525 const [ loadingReadme , setLoadingReadme ] = useState ( null ) ; // Track loading state
26+ const [ copiedRepo , setCopiedRepo ] = useState ( null ) ; // Track which repo link was copied
2627
27- // Handle URL hash changes for navigation
28+ // Helper function to extract repository name from GitHub URL
29+ const getRepoId = ( url ) => {
30+ const match = url . match ( / g i t h u b \. c o m \/ [ ^ / ] + \/ ( [ ^ / ] + ) / ) ;
31+ return match ? match [ 1 ] : null ;
32+ } ;
33+
34+ // Helper function to copy link to clipboard
35+ const copyRepoLink = useCallback ( ( repo , e ) => {
36+ e . stopPropagation ( ) ;
37+ const repoId = getRepoId ( repo . url ) ;
38+ if ( repoId ) {
39+ const url = `${ window . location . origin } ${ window . location . pathname } #${ repoId } ` ;
40+ navigator . clipboard . writeText ( url ) . then ( ( ) => {
41+ setCopiedRepo ( repoId ) ;
42+ setTimeout ( ( ) => setCopiedRepo ( null ) , 2000 ) ;
43+ } ) . catch ( err => {
44+ console . error ( 'Failed to copy link:' , err ) ;
45+ } ) ;
46+ }
47+ } , [ ] ) ;
48+
49+ // Handle URL hash changes for navigation (both product tabs and repository deep links)
2850 useEffect ( ( ) => {
2951 const handleHashChange = ( ) => {
3052 const hash = window . location . hash . replace ( '#' , '' ) ;
53+
54+ // Check if hash is a product tab
3155 if ( hash && productTabs . find ( tab => tab . id === hash ) ) {
3256 setActiveTab ( hash ) ;
3357 // Reset filters when changing tabs
@@ -40,10 +64,30 @@ const RepositoryBrowser = () => {
4064 setComponentFilter ( 'all' ) ;
4165 setTopicFilter ( [ ] ) ;
4266 setSearchTerm ( '' ) ;
67+ } else if ( hash ) {
68+ // Check if hash is a repository ID
69+ const repo = repositories . find ( r => getRepoId ( r . url ) === hash ) ;
70+ if ( repo ) {
71+ // Expand the repository
72+ setExpandedRepo ( repo . name ) ;
73+ // Scroll to the repository after a short delay to ensure it's rendered
74+ setTimeout ( ( ) => {
75+ const element = document . getElementById ( hash ) ;
76+ if ( element ) {
77+ element . scrollIntoView ( { behavior : 'smooth' , block : 'center' } ) ;
78+ // Add a temporary highlight effect
79+ element . style . transition = 'background-color 0.5s' ;
80+ element . style . backgroundColor = '#e8f4fd' ;
81+ setTimeout ( ( ) => {
82+ element . style . backgroundColor = '' ;
83+ } , 2000 ) ;
84+ }
85+ } , 100 ) ;
86+ }
4387 }
4488 } ;
4589
46- // Set initial tab from hash
90+ // Set initial tab/repo from hash
4791 handleHashChange ( ) ;
4892
4993 // Listen for hash changes
@@ -56,7 +100,7 @@ const RepositoryBrowser = () => {
56100 const pollInterval = setInterval ( ( ) => {
57101 const currentHash = window . location . hash . replace ( '#' , '' ) ;
58102 const expectedHash = activeTab === 'all' ? '' : activeTab ;
59- if ( currentHash !== expectedHash ) {
103+ if ( currentHash !== expectedHash && ! repositories . find ( r => getRepoId ( r . url ) === currentHash ) ) {
60104 handleHashChange ( ) ;
61105 }
62106 } , 100 ) ;
@@ -251,24 +295,58 @@ const RepositoryBrowser = () => {
251295 return preview ;
252296 } else {
253297 setLoadingReadme ( null ) ;
254- return '<p style="color: #666;">README not available</p>' ;
298+ // Return null for failed fetches (403, 404, etc.) so we don't show empty section
299+ setReadmeCache ( prev => ( {
300+ ...prev ,
301+ [ repoName ] : null
302+ } ) ) ;
303+ return null ;
255304 }
256305 } catch ( error ) {
257306 console . error ( 'Error fetching README:' , error ) ;
258307 setLoadingReadme ( null ) ;
259- return '<p style="color: #666;">Error loading README</p>' ;
308+ // Return null for errors so we don't show empty section
309+ setReadmeCache ( prev => ( {
310+ ...prev ,
311+ [ repoName ] : null
312+ } ) ) ;
313+ return null ;
260314 }
261315 } , [ readmeCache ] ) ;
262316
263- // Toggle README preview
317+ // Toggle README preview and update URL hash
264318 const toggleReadme = async ( repoName ) => {
319+ const repo = repositories . find ( r => r . name === repoName ) ;
320+ const repoId = repo ? getRepoId ( repo . url ) : null ;
321+
265322 if ( expandedRepo === repoName ) {
266323 setExpandedRepo ( null ) ;
324+ // Remove hash when collapsing
325+ if ( window . location . hash ) {
326+ window . history . pushState ( null , '' , window . location . pathname ) ;
327+ }
267328 } else {
268- setExpandedRepo ( repoName ) ;
329+ // Try to fetch README if not in cache
269330 if ( ! readmeCache [ repoName ] ) {
270- await fetchReadme ( repoName ) ;
331+ const readmeContent = await fetchReadme ( repoName ) ;
332+ // Only expand if README was successfully fetched
333+ if ( readmeContent !== null ) {
334+ setExpandedRepo ( repoName ) ;
335+ // Update hash when expanding
336+ if ( repoId ) {
337+ window . history . pushState ( null , '' , `#${ repoId } ` ) ;
338+ }
339+ }
340+ // If README fetch failed (null), don't expand
341+ } else if ( readmeCache [ repoName ] !== null ) {
342+ // Only expand if cached README is not null (meaning it was successful)
343+ setExpandedRepo ( repoName ) ;
344+ // Update hash when expanding
345+ if ( repoId ) {
346+ window . history . pushState ( null , '' , `#${ repoId } ` ) ;
347+ }
271348 }
349+ // If cached README is null, don't expand
272350 }
273351 } ;
274352
@@ -480,23 +558,28 @@ const RepositoryBrowser = () => {
480558
481559 { /* Repository list */ }
482560 < div style = { { display : 'grid' , gap : '1.5rem' } } >
483- { filteredRepos . map ( repo => (
484- < div
485- key = { repo . name }
486- style = { {
487- border : '1px solid #e0e0e0' ,
488- borderRadius : '8px' ,
489- padding : '1.5rem' ,
490- backgroundColor : '#fff' ,
491- transition : 'box-shadow 0.2s'
492- } }
493- onMouseEnter = { ( e ) => {
494- e . currentTarget . style . boxShadow = '0 4px 12px rgba(0,0,0,0.1)' ;
495- } }
496- onMouseLeave = { ( e ) => {
497- e . currentTarget . style . boxShadow = 'none' ;
498- } }
499- >
561+ { filteredRepos . map ( repo => {
562+ const repoId = getRepoId ( repo . url ) ;
563+ const isCopied = copiedRepo === repoId ;
564+
565+ return (
566+ < div
567+ key = { repo . name }
568+ id = { repoId }
569+ style = { {
570+ border : '1px solid #e0e0e0' ,
571+ borderRadius : '8px' ,
572+ padding : '1.5rem' ,
573+ backgroundColor : '#fff' ,
574+ transition : 'box-shadow 0.2s, background-color 0.5s'
575+ } }
576+ onMouseEnter = { ( e ) => {
577+ e . currentTarget . style . boxShadow = '0 4px 12px rgba(0,0,0,0.1)' ;
578+ } }
579+ onMouseLeave = { ( e ) => {
580+ e . currentTarget . style . boxShadow = 'none' ;
581+ } }
582+ >
500583 < div style = { { display : 'flex' , alignItems : 'center' , gap : '1rem' , marginBottom : '0.5rem' , cursor : 'pointer' } }
501584 onClick = { ( ) => toggleReadme ( repo . name ) } >
502585 < h3 style = { { margin : 0 , flex : 1 } } >
@@ -520,6 +603,33 @@ const RepositoryBrowser = () => {
520603 < span > { repo . stars } </ span >
521604 </ div >
522605 ) }
606+ < button
607+ onClick = { ( e ) => copyRepoLink ( repo , e ) }
608+ title = "Copy link to this repository"
609+ style = { {
610+ color : isCopied ? '#24a148' : '#0f62fe' ,
611+ backgroundColor : 'transparent' ,
612+ border : `1px solid ${ isCopied ? '#24a148' : '#0f62fe' } ` ,
613+ borderRadius : '4px' ,
614+ padding : '0.25rem 0.5rem' ,
615+ fontSize : '0.875rem' ,
616+ cursor : 'pointer' ,
617+ transition : 'all 0.2s' ,
618+ display : 'flex' ,
619+ alignItems : 'center' ,
620+ gap : '0.25rem'
621+ } }
622+ onMouseEnter = { ( e ) => {
623+ if ( ! isCopied ) {
624+ e . currentTarget . style . backgroundColor = '#f4f4f4' ;
625+ }
626+ } }
627+ onMouseLeave = { ( e ) => {
628+ e . currentTarget . style . backgroundColor = 'transparent' ;
629+ } }
630+ >
631+ { isCopied ? '✓ Copied!' : '📋 Share' }
632+ </ button >
523633 < a
524634 href = { repo . url }
525635 target = "_blank"
@@ -639,7 +749,8 @@ const RepositoryBrowser = () => {
639749 </ div >
640750 ) }
641751 </ div >
642- ) ) }
752+ ) ;
753+ } ) }
643754 </ div >
644755
645756 { filteredRepos . length === 0 && (
0 commit comments