11import  {  useState ,  useEffect  }  from  'react' ; 
22import  {  Card ,  CardContent  }  from  '@/components/ui/card' ; 
33import  {  Skeleton  }  from  '@/components/ui/skeleton' ; 
4- import  {  ExternalLink  }  from  'lucide-react' ; 
4+ import  {  ExternalLink ,   Link ,   Image  }  from  'lucide-react' ; 
55
66interface  LinkPreviewProps  { 
77  url : string ; 
@@ -14,32 +14,99 @@ interface LinkMetadata {
1414  domain : string ; 
1515} 
1616
17+ // Function to extract domain name from a URL 
18+ const  extractDomain  =  ( url : string ) : string  =>  { 
19+   try  { 
20+     const  urlObj  =  new  URL ( url ) ; 
21+     return  urlObj . hostname . replace ( 'www.' ,  '' ) ; 
22+   }  catch  ( error )  { 
23+     // If URL parsing fails, use a regex fallback 
24+     const  match  =  url . match ( / ^ (?: h t t p s ? : \/ \/ ) ? (?: [ ^ @ \n ] + @ ) ? (?: w w w \. ) ? ( [ ^ : / \n ? ] + ) / i) ; 
25+     return  match  ? match [ 1 ]  : url ; 
26+   } 
27+ } ; 
28+ 
1729export  function  LinkPreview ( {  url } : LinkPreviewProps )  { 
1830  const  [ metadata ,  setMetadata ]  =  useState < LinkMetadata  |  null > ( null ) ; 
1931  const  [ loading ,  setLoading ]  =  useState ( true ) ; 
2032  const  [ error ,  setError ]  =  useState ( false ) ; 
33+   const  [ fetchTries ,  setFetchTries ]  =  useState ( 0 ) ; 
34+ 
35+   // Clean and format the URL for display 
36+   const  displayUrl  =  url . replace ( / ^ h t t p s ? : \/ \/ ( w w w \. ) ? / ,  '' ) . replace ( / \/ $ / ,  '' ) ; 
37+   const  domain  =  extractDomain ( url ) ; 
2138
2239  useEffect ( ( )  =>  { 
2340    const  fetchMetadata  =  async  ( )  =>  { 
24-       try  { 
41+       // Reset state for new URL 
42+       if  ( fetchTries  ===  0 )  { 
2543        setLoading ( true ) ; 
2644        setError ( false ) ; 
45+       } 
2746
28-         // Use a proxy service to avoid CORS issues 
29-         // In a production app, you would use your own backend proxy or a service like Microlink 
30-         const  proxyUrl  =  `https://api.allorigins.win/get?url=${ encodeURIComponent ( url ) }  ; 
31-         const  response  =  await  fetch ( proxyUrl ) ; 
47+       try  { 
48+         // Handle special case domains directly 
49+         if  ( 
50+           url . includes ( 'youtube.com' )  ||  
51+           url . includes ( 'youtu.be' )  || 
52+           url . includes ( 'twitter.com' )  ||  
53+           url . includes ( 'x.com' ) 
54+         )  { 
55+           // For these domains, just display a simplified preview without trying to fetch metadata 
56+           setMetadata ( { 
57+             title : url . includes ( 'youtube' )  ? 'YouTube Video'  : 'Twitter Post' , 
58+             description : '' , 
59+             image : '' , 
60+             domain : domain 
61+           } ) ; 
62+           setLoading ( false ) ; 
63+           return ; 
64+         } 
65+ 
66+         // Try different proxy services based on retry count 
67+         let  proxyUrl  =  '' ; 
68+         
69+         // On first try, use allorigins 
70+         if  ( fetchTries  ===  0 )  { 
71+           proxyUrl  =  `https://api.allorigins.win/get?url=${ encodeURIComponent ( url ) }  ; 
72+         } 
73+         // On second try, use another service 
74+         else  if  ( fetchTries  ===  1 )  { 
75+           proxyUrl  =  `https://cors-anywhere.herokuapp.com/${ url }  ; 
76+         } 
77+         // On third try, give up on proxies and just show a clean preview 
78+         else  { 
79+           throw  new  Error ( 'All proxy attempts failed' ) ; 
80+         } 
81+         
82+         // Add a timeout to prevent hanging requests 
83+         const  controller  =  new  AbortController ( ) ; 
84+         const  timeoutId  =  setTimeout ( ( )  =>  controller . abort ( ) ,  5000 ) ;  // 5 second timeout 
85+         
86+         const  response  =  await  fetch ( proxyUrl ,  {  
87+           signal : controller . signal 
88+         } ) ; 
89+         
90+         clearTimeout ( timeoutId ) ; 
3291
3392        if  ( ! response . ok )  { 
34-           throw  new  Error ( 'Failed to fetch link metadata' ) ; 
93+           throw  new  Error ( `Response not OK:  ${ response . status } ` ) ; 
3594        } 
3695
37-         const   data  =  await   response . json ( ) ; 
38-         const   html   =   data . contents ; 
96+         let   html  =  '' ; 
97+         let   doc :  Document ; 
3998
40-         // Create a DOM parser to extract metadata 
41-         const  parser  =  new  DOMParser ( ) ; 
42-         const  doc  =  parser . parseFromString ( html ,  'text/html' ) ; 
99+         // Parse the response based on the proxy used 
100+         if  ( fetchTries  ===  0 )  { 
101+           const  data  =  await  response . json ( ) ; 
102+           html  =  data . contents ; 
103+           const  parser  =  new  DOMParser ( ) ; 
104+           doc  =  parser . parseFromString ( html ,  'text/html' ) ; 
105+         }  else  { 
106+           html  =  await  response . text ( ) ; 
107+           const  parser  =  new  DOMParser ( ) ; 
108+           doc  =  parser . parseFromString ( html ,  'text/html' ) ; 
109+         } 
43110
44111        // Extract metadata from Open Graph tags, Twitter cards, or regular meta tags 
45112        const  title  =  
@@ -59,30 +126,42 @@ export function LinkPreview({ url }: LinkPreviewProps) {
59126          doc . querySelector ( 'meta[name="twitter:image"]' ) ?. getAttribute ( 'content' )  ||  
60127          '' ; 
61128
62-         // Extract domain from URL 
63-         const  urlObj  =  new  URL ( url ) ; 
64-         const  domain  =  urlObj . hostname . replace ( 'www.' ,  '' ) ; 
65-         
66129        setMetadata ( { 
67-           title, 
130+           title :  title   ||   url , 
68131          description, 
69132          image, 
70133          domain
71134        } ) ; 
135+         setLoading ( false ) ; 
72136      }  catch  ( err )  { 
73137        console . error ( 'Error fetching link preview:' ,  err ) ; 
74-         setError ( true ) ; 
75-       }  finally  { 
76-         setLoading ( false ) ; 
138+         
139+         // If we haven't exceeded max retries, try another method 
140+         if  ( fetchTries  <  2 )  { 
141+           setFetchTries ( prev  =>  prev  +  1 ) ; 
142+         }  else  { 
143+           // After all retries fail, show fallback 
144+           setError ( true ) ; 
145+           setLoading ( false ) ; 
146+           
147+           // Still provide basic metadata for fallback display 
148+           setMetadata ( { 
149+             title : '' , 
150+             description : '' , 
151+             image : '' , 
152+             domain
153+           } ) ; 
154+         } 
77155      } 
78156    } ; 
79157
80158    if  ( url )  { 
81159      fetchMetadata ( ) ; 
82160    } 
83-   } ,  [ url ] ) ; 
161+   } ,  [ url ,   fetchTries ,   domain ] ) ; 
84162
85-   if  ( loading )  { 
163+   // Show loading state only on first attempt 
164+   if  ( loading  &&  fetchTries  ===  0 )  { 
86165    return  ( 
87166      < Card  className = "overflow-hidden mt-2 max-w-md" > 
88167        < CardContent  className = "p-0" > 
@@ -101,21 +180,43 @@ export function LinkPreview({ url }: LinkPreviewProps) {
101180    ) ; 
102181  } 
103182
104-   if  ( error  ||  ! metadata )  { 
105-     // Fallback to a simple link display 
183+   // Show fallback for errors or when all retries failed 
184+   if  ( error  ||  ( loading  &&  fetchTries  >=  2 ) )  { 
185+     return  ( 
186+       < a  
187+         href = { url }  
188+         target = "_blank"  
189+         rel = "noopener noreferrer"  
190+         className = "inline-flex items-center mt-1 px-3 py-2 bg-muted/30 rounded-md text-sm text-primary hover:bg-muted/50 transition-colors" 
191+       > 
192+         { url . includes ( 'youtube.com' )  ||  url . includes ( 'youtu.be' )  ? ( 
193+           < Image  className = "h-4 w-4 mr-2 text-red-500"  /> 
194+         )  : url . includes ( 'twitter.com' )  ||  url . includes ( 'x.com' )  ? ( 
195+           < Link  className = "h-4 w-4 mr-2 text-blue-400"  /> 
196+         )  : ( 
197+           < ExternalLink  className = "h-4 w-4 mr-2"  /> 
198+         ) } 
199+         < span  className = "truncate max-w-[250px]" > { displayUrl } </ span > 
200+       </ a > 
201+     ) ; 
202+   } 
203+ 
204+   // If metadata has no title but we're not in an error state, show a simplified preview 
205+   if  ( metadata  &&  ! metadata . title )  { 
106206    return  ( 
107207      < a  
108208        href = { url }  
109209        target = "_blank"  
110210        rel = "noopener noreferrer"  
111-         className = "text-blue-500 hover:underline  flex items-center mt-1 text-sm" 
211+         className = "inline- flex items-center mt-1 px-3 py-2 bg-muted/30 rounded-md  text-sm text-primary hover:bg-muted/50 transition-colors " 
112212      > 
113-         < ExternalLink  className = "h-3.5  w-3.5  mr-1 "  /> 
114-         { url } 
213+         < ExternalLink  className = "h-4  w-4  mr-2 "  /> 
214+         < span   className = "truncate max-w-[250px]" > { displayUrl } </ span > 
115215      </ a > 
116216    ) ; 
117217  } 
118218
219+   // Full link preview card with metadata 
119220  return  ( 
120221    < a  
121222      href = { url }  
@@ -126,20 +227,26 @@ export function LinkPreview({ url }: LinkPreviewProps) {
126227      < Card  className = "overflow-hidden border-muted" > 
127228        < CardContent  className = "p-0" > 
128229          < div  className = "flex flex-col sm:flex-row" > 
129-             { metadata . image  &&  ( 
230+             { metadata ? .image  &&  ( 
130231              < div  className = "sm:w-1/3 h-32 sm:h-auto" > 
131232                < div  
132233                  className = "w-full h-full bg-cover bg-center"  
133234                  style = { {  backgroundImage : `url(${ metadata . image }   } } 
235+                   onError = { ( e )  =>  { 
236+                     // Hide the image div if it fails to load 
237+                     ( e . target  as  HTMLDivElement ) . style . display  =  'none' ; 
238+                   } } 
134239                /> 
135240              </ div > 
136241            ) } 
137-             < div  className = { `${ metadata . image  ? 'sm:w-2/3'  : 'w-full' }  } > 
138-               < h3  className = "font-medium text-sm line-clamp-2" > { metadata . title } </ h3 > 
139-               < p  className = "text-xs text-muted-foreground line-clamp-2" > { metadata . description } </ p > 
242+             < div  className = { `${ metadata ?. image  ? 'sm:w-2/3'  : 'w-full' }  } > 
243+               < h3  className = "font-medium text-sm line-clamp-2" > { metadata ?. title } </ h3 > 
244+               { metadata ?. description  &&  ( 
245+                 < p  className = "text-xs text-muted-foreground line-clamp-2" > { metadata . description } </ p > 
246+               ) } 
140247              < div  className = "flex items-center text-xs text-muted-foreground pt-1" > 
141248                < ExternalLink  className = "h-3 w-3 mr-1"  /> 
142-                 { metadata . domain } 
249+                 { metadata ? .domain } 
143250              </ div > 
144251            </ div > 
145252          </ div > 
0 commit comments