@@ -4,6 +4,7 @@ import { X, GripVertical, FileText, File, Folder, AppWindow, ImageIcon, FileCode
44import ReactMarkdown from 'react-markdown' ;
55import { cn } from '../../lib/utils' ;
66import { BoardCard as BoardCardType } from './types' ;
7+ import { fetchMetadata , LinkMetadata } from './utils' ;
78import { APPS_CONFIG } from '../../lib/apps' ;
89import { useFileSystem } from '../../hooks/useFileSystem' ;
910import { useOS } from '../../hooks/useOS' ;
@@ -18,10 +19,17 @@ interface CardProps {
1819export const BoardCard : React . FC < CardProps > = ( { card, onUpdate, onDelete } ) => {
1920 const [ isEditing , setIsEditing ] = useState ( false ) ;
2021 const [ previewContent , setPreviewContent ] = useState < string | null > ( null ) ;
22+ const [ metadata , setMetadata ] = useState < LinkMetadata | null > ( null ) ;
2123 const [ previewType , setPreviewType ] = useState < 'text' | 'image' | 'video' | 'code' | 'markdown' | 'html' | 'none' > ( 'none' ) ;
2224 const { readFile } = useFileSystem ( ) ;
2325 const { openApp, sendAppAction, appWindows } = useOS ( ) ;
2426
27+ useEffect ( ( ) => {
28+ if ( card . type === 'link' && card . metadata ?. url ) {
29+ fetchMetadata ( card . metadata . url ) . then ( setMetadata ) ;
30+ }
31+ } , [ card . type , card . metadata ?. url ] ) ;
32+
2533 const handleOpenFile = ( ) => {
2634 const path = card . metadata ?. path || card . metadata ?. url ;
2735 if ( ! path ) return ;
@@ -127,13 +135,37 @@ export const BoardCard: React.FC<CardProps> = ({ card, onUpdate, onDelete }) =>
127135 { ( card . type === 'link' || ! card . metadata ?. isDirectory ) && (
128136 < div className = "flex-1 bg-zinc-50/50 rounded-xl border border-zinc-100/50 overflow-hidden relative group/preview min-h-[160px]" >
129137 { card . type === 'link' ? (
130- < div className = "w-full h-full relative group-hover/preview:opacity-95 transition-opacity" >
131- < iframe
132- src = { card . metadata ?. url }
133- className = "w-full h-full border-0 pointer-events-none scale-[0.5] origin-top-left"
134- style = { { width : '200%' , height : '200%' } }
135- title = "Link Preview"
136- />
138+ < div className = "w-full h-full relative group-hover/preview:opacity-95 transition-all flex flex-col" >
139+ { metadata ?. image ? (
140+ < div className = "w-full h-24 overflow-hidden bg-zinc-100" >
141+ < img
142+ src = { metadata . image }
143+ className = "w-full h-full object-cover"
144+ alt = "link preview"
145+ onError = { ( e ) => e . currentTarget . style . display = 'none' }
146+ />
147+ </ div >
148+ ) : (
149+ < div className = "w-full h-24 bg-sky-50 flex items-center justify-center" >
150+ < Globe size = { 24 } className = "text-sky-200" />
151+ </ div >
152+ ) }
153+ < div className = "p-3 flex flex-col gap-1" >
154+ < span className = "text-[11px] font-bold text-zinc-800 line-clamp-1" >
155+ { metadata ?. title || card . content }
156+ </ span >
157+ < p className = "text-[10px] text-zinc-500 line-clamp-2 leading-tight" >
158+ { metadata ?. description || "No description available" }
159+ </ p >
160+ < div className = "flex items-center gap-1 mt-1" >
161+ < div className = "w-3 h-3 rounded-full bg-sky-100 flex items-center justify-center" >
162+ < Globe size = { 8 } className = "text-sky-500" />
163+ </ div >
164+ < span className = "text-[8px] text-zinc-400 font-bold uppercase tracking-wider" >
165+ { metadata ?. siteName || metadata ?. url . split ( '/' ) [ 2 ] ?. replace ( 'www.' , '' ) || "Website" }
166+ </ span >
167+ </ div >
168+ </ div >
137169 < div className = "absolute inset-0 z-10" />
138170 </ div >
139171 ) : ( card . type as string ) === 'widget' ? (
0 commit comments