11import React , { useEffect , useState } from 'react' ;
2- import { IconChevronRight , Icon as TablerIcon } from '@tabler/icons-react' ;
2+ import { IconChevronRight , IconExternalLink } from '@tabler/icons-react' ;
3+ import cn from "clsx" ;
34
45interface ExternLinkProps {
56 href : string ;
67 icon ?: React . ReactNode | React . ElementType ; // Erlaubt sowohl React-Komponenten als auch Strings
7- manualTitle ?: string ; // Neuer Prop für manuellen Titel
8+ manualTitle ?: string ; // Eigenschaft für manuellen Titel
9+ manualDescript ?: string ; // Eigenschaft für manuelle Beschreibung
10+ children ?: React . ReactNode ; // Kinder-Elemente unterstützen
811}
912
1013interface LinkMetaData {
@@ -13,103 +16,148 @@ interface LinkMetaData {
1316 icon : string ;
1417}
1518
19+ // Stile im Stil der Nextra Cards
20+ const classes = {
21+ card : cn (
22+ "nx-group nx-flex nx-overflow-hidden nx-rounded-lg nx-border nx-border-gray-200" ,
23+ "nx-text-current nx-no-underline dark:nx-shadow-none" ,
24+ "hover:nx-shadow-gray-100 dark:hover:nx-shadow-none nx-shadow-gray-100" ,
25+ "active:nx-shadow-sm active:nx-shadow-gray-200" ,
26+ "nx-transition-all nx-duration-200 hover:nx-border-gray-300" ,
27+ "nx-bg-transparent nx-shadow-sm dark:nx-border-neutral-800 hover:nx-bg-slate-50 hover:nx-shadow-md dark:hover:nx-border-neutral-700 dark:hover:nx-bg-neutral-900" ,
28+ "my-4"
29+ ) ,
30+ title : cn (
31+ "nx-font-semibold nx-text-gray-700 hover:nx-text-gray-900" ,
32+ "dark:nx-text-neutral-200 dark:hover:nx-text-neutral-50"
33+ ) ,
34+ description : cn (
35+ "nx-text-sm nx-text-gray-500 dark:nx-text-gray-400"
36+ )
37+ } ;
38+
1639const fetchMetaData = async ( url : string ) : Promise < LinkMetaData > => {
1740 try {
1841 const response = await fetch ( url ) ;
1942 if ( ! response . ok ) {
20- throw new Error ( 'Network response was not ok' ) ;
43+ throw new Error ( 'Netzwerkantwort war nicht ok' ) ;
2144 }
22- const text = await response . text ( ) ; // Hole den HTML-Inhalt
45+ const text = await response . text ( ) ; // HTML-Inhalt abrufen
2346 const parser = new DOMParser ( ) ;
2447 const doc = parser . parseFromString ( text , 'text/html' ) ;
2548
2649 const title = doc . querySelector ( 'meta[property="og:title"]' ) ?. getAttribute ( 'content' ) ||
2750 doc . querySelector ( 'title' ) ?. textContent ||
28- 'No title ' ;
51+ 'Kein Titel ' ;
2952 const description = doc . querySelector ( 'meta[property="og:description"]' ) ?. getAttribute ( 'content' ) ||
3053 doc . querySelector ( 'meta[name="description"]' ) ?. getAttribute ( 'content' ) ||
31- 'No description ' ;
54+ 'Keine Beschreibung ' ;
3255 const icon = doc . querySelector ( 'link[rel="icon"]' ) ?. getAttribute ( 'href' ) ||
3356 doc . querySelector ( 'link[rel="shortcut icon"]' ) ?. getAttribute ( 'href' ) ||
3457 'https://example.com/default-icon.png' ;
3558
3659 return { title, description, icon : new URL ( icon , url ) . href } ;
3760 } catch ( error ) {
38- console . error ( 'Error fetching metadata :' , error ) ;
61+ console . error ( 'Fehler beim Abrufen der Metadaten :' , error ) ;
3962 return {
40- title : 'Error ' ,
41- description : 'Unable to fetch metadata .' ,
63+ title : 'Fehler ' ,
64+ description : 'Metadaten konnten nicht abgerufen werden .' ,
4265 icon : 'https://example.com/error-icon.png' ,
4366 } ;
4467 }
4568} ;
4669
47- const ExternLink : React . FC < ExternLinkProps > = ( { href, icon, manualTitle } ) => {
70+ const ExternLink : React . FC < ExternLinkProps > = ( { href, icon, manualTitle, manualDescript , children } ) => {
4871 const [ metaData , setMetaData ] = useState < LinkMetaData | null > ( null ) ;
4972 const [ loading , setLoading ] = useState ( true ) ;
5073
5174 useEffect ( ( ) => {
52- if ( ! manualTitle ) {
75+ if ( ! manualTitle && ! manualDescript ) {
5376 fetchMetaData ( href ) . then ( ( data ) => {
5477 setMetaData ( data ) ;
5578 setLoading ( false ) ;
5679 } ) ;
5780 } else {
5881 setMetaData ( {
59- title : manualTitle ,
60- description : 'No description available ',
61- icon : 'https://example.com/web-icon.png' , // Web-Icon für manuellen Titel
82+ title : manualTitle || 'Kein Titel verfügbar' ,
83+ description : manualDescript || 'Keine Beschreibung verfügbar ',
84+ icon : 'https://example.com/web-icon.png' , // Web-Icon für manuellen Titel/Beschreibung
6285 } ) ;
6386 setLoading ( false ) ;
6487 }
65- } , [ href , manualTitle ] ) ;
66-
67- if ( loading ) {
68- return < div > Loading...</ div > ; // Display a loading state while fetching metadata
69- }
88+ } , [ href , manualTitle , manualDescript ] ) ;
7089
71- // Funktion zur Kürzung der Beschreibung
90+ // Funktion zum Kürzen der Beschreibung
7291 const truncateDescription = ( description : string , maxLength : number ) => {
7392 if ( description . length <= maxLength ) {
7493 return description ;
7594 }
7695 return description . substring ( 0 , maxLength ) + '...' ; // Kürzen und '...' hinzufügen
7796 } ;
7897
98+ // Wenn Kinder-Elemente vorhanden sind, diese anstelle des automatisch generierten Inhalts anzeigen
99+ if ( children ) {
100+ return (
101+ < a
102+ href = { href }
103+ className = "nx-flex nx-items-center nx-no-underline nx-text-current nx-transition-colors hover:nx-text-blue-600 dark:hover:nx-text-blue-400"
104+ target = "_blank"
105+ rel = "noopener noreferrer"
106+ >
107+ { children }
108+ < IconExternalLink className = "nx-ml-1 nx-inline-block nx-h-4 nx-w-4" />
109+ </ a >
110+ ) ;
111+ }
112+
113+ if ( loading ) {
114+ return (
115+ < div className = "nx-flex nx-items-center nx-justify-center nx-h-20 nx-bg-zinc-100 dark:nx-bg-zinc-800 nx-rounded-lg nx-animate-pulse" >
116+ < span className = "nx-text-zinc-500 dark:nx-text-zinc-400" > Lädt...</ span >
117+ </ div >
118+ ) ;
119+ }
120+
79121 return (
80122 < a
81123 href = { href }
82- className = "w-full my-5 border border-gray-200 shadow-sm hover:shadow-md dark:border-neutral-700 dark:hover:border-neutral-600 transition-all duration-200 dark:bg-neutral-900 bg-white rounded-lg overflow-hidden flex flex-col justify-start relative"
124+ className = { classes . card }
83125 target = "_blank"
84126 rel = "noopener noreferrer"
85127 >
86- < div className = "flex items-center p-4" >
87- { /* Icon */ }
88- { icon ? (
89- typeof icon === 'string' ? (
90- < img src = { icon } alt = "Favicon" className = "w-6 h-6 mr-3" />
91- ) : (
92- React . isValidElement ( icon ) ? (
93- React . cloneElement ( icon as React . ReactElement , { className : 'w-6 h-6 mr-3' } )
128+ < div className = "nx-flex nx-items-center nx-w-full nx-p-4 nx-relative" >
129+ { /* Icon - mit größerem Abstand zum Text */ }
130+ < div className = "nx-flex-shrink-0 nx-mr-12" >
131+ { icon ? (
132+ typeof icon === 'string' ? (
133+ < img src = { icon } alt = "Favicon" className = "w-6 h-6 mr-3" />
94134 ) : (
95- React . createElement ( icon as React . ElementType , { className : 'w-6 h-6 mr-3' } )
135+ React . isValidElement ( icon ) ? (
136+ React . cloneElement ( icon as React . ReactElement , { className : 'w-6 h-6 mr-3' } )
137+ ) : (
138+ React . createElement ( icon as React . ElementType , { className : 'w-6 h-6 mr-3' } )
139+ )
96140 )
97- )
98- ) : (
99- < img src = { metaData ?. icon } alt = "Favicon" className = "w-6 h-6 mr-3" />
100- ) }
101- { /* Title */ }
102- < div className = "flex flex-col" >
103- < span className = "font-semibold text-lg text-gray-700 dark:text-gray-100" >
141+ ) : (
142+ < img src = { metaData ?. icon } alt = "Favicon" className = "w-6 h-6 mr-3" />
143+ ) }
144+ </ div >
145+
146+ { /* Inhalt - mit weniger Padding rechts, da der Pfeil absolut positioniert ist */ }
147+ < div className = "nx-flex-grow px-4" >
148+ < span className = { classes . title } >
104149 { metaData ?. title }
105150 </ span >
106- < span className = "text-sm text-gray-500 dark:text-gray-300" >
107- { metaData ?. description && truncateDescription ( metaData . description , 100 ) } { /* Hier wird die Beschreibung gekürzt */ }
108- </ span >
151+ < div className = "nx-mt-1" >
152+ < span className = { classes . description } >
153+ { metaData ?. description && truncateDescription ( metaData . description , 120 ) }
154+ </ span >
155+ </ div >
109156 </ div >
110- { /* Arrow Icon */ }
111- < span className = "absolute right-4 top-1/2 transform -translate-y-1/2 text-gray-600 dark:text-gray-300" >
112- < IconChevronRight />
157+
158+ { /* Pfeil-Icon - korrekt rechts ausgerichtet mit absoluter Positionierung */ }
159+ < span className = "absolute right-4 content-end nx-top-1/2 nx-transform nx--translate-y-1/2" >
160+ < IconChevronRight className = "nx-transition-transform nx-duration-75 group-hover:nx-translate-x-[2px]" />
113161 </ span >
114162 </ div >
115163 </ a >
0 commit comments