react-link-preview-card
Version:
A React component for generating beautiful link preview cards with metadata
1 lines • 15.7 kB
Source Map (JSON)
{"version":3,"sources":["../src/lib/components/LinkPreview/index.tsx","../src/lib/components/LinkPreview/Preview.tsx","../src/lib/components/LinkPreview/Skeleton.tsx","../src/lib/utils/youtube.ts","../src/lib/utils/cache.ts","../src/lib/utils/fallback.ts","../src/lib/utils/metadata.ts"],"sourcesContent":["import React, { useState, useEffect } from 'react';\nimport type { LinkPreviewProps, LinkPreviewData } from './types';\nimport { Preview } from './Preview';\nimport { LinkPreviewSkeleton } from './Skeleton';\nimport { fetchMetadata } from '../../utils/metadata';\n\nexport const LinkPreview: React.FC<LinkPreviewProps> = ({ url }) => {\n const [loading, setLoading] = useState(true);\n const [data, setData] = useState<LinkPreviewData | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n let mounted = true;\n const controller = new AbortController();\n\n const getPreviewData = async () => {\n try {\n setLoading(true);\n setError(null);\n const metadata = await fetchMetadata(url);\n if (mounted) {\n setData(metadata);\n }\n } catch (err) {\n if (mounted) {\n setError('Failed to load preview');\n console.error('Error fetching preview:', err);\n }\n } finally {\n if (mounted) {\n setLoading(false);\n }\n }\n };\n\n if (url) {\n getPreviewData();\n }\n\n return () => {\n mounted = false;\n controller.abort();\n };\n }, [url]);\n\n if (loading) {\n return <LinkPreviewSkeleton />;\n }\n\n if (error) {\n return (\n <div className=\"w-full max-w-md p-4 bg-red-50 rounded-lg text-red-600\">\n {error}\n </div>\n );\n }\n\n if (!data) {\n return null;\n }\n\n return <Preview data={data} />;\n};","import React from 'react';\nimport { ExternalLink, Play, FileText, Globe } from 'lucide-react';\nimport type { LinkPreviewData } from './types';\n\ninterface PreviewProps {\n data: LinkPreviewData;\n}\n\nexport const Preview: React.FC<PreviewProps> = ({ data }) => {\n const TypeIcon = () => {\n switch (data.type) {\n case 'video':\n return <Play className=\"w-5 h-5 text-gray-600\" />;\n case 'article':\n return <FileText className=\"w-5 h-5 text-gray-600\" />;\n default:\n return <Globe className=\"w-5 h-5 text-gray-600\" />;\n }\n };\n\n return (\n <a \n href={data.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"block w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300\"\n >\n <div className=\"relative\">\n <img\n src={data.image}\n alt={data.title}\n className=\"w-full h-48 object-cover\"\n onError={(e) => {\n e.currentTarget.src = 'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?w=800&q=80';\n }}\n />\n <div className=\"absolute top-2 right-2 bg-white/90 p-1.5 rounded-full\">\n <ExternalLink className=\"w-4 h-4 text-gray-600\" />\n </div>\n <div className=\"absolute top-2 left-2 bg-white/90 p-1.5 rounded-full\">\n <TypeIcon />\n </div>\n </div>\n <div className=\"p-4\">\n <h3 className=\"font-semibold text-lg text-gray-800 mb-2 line-clamp-2\">\n {data.title}\n </h3>\n <p className=\"text-gray-600 text-sm line-clamp-2\">\n {data.description}\n </p>\n </div>\n </a>\n );\n};","import React from 'react';\n\nexport const LinkPreviewSkeleton = () => {\n return (\n <div className=\"w-full max-w-md bg-white rounded-lg shadow-md overflow-hidden\">\n <div className=\"animate-pulse\">\n {/* Image skeleton */}\n <div className=\"h-48 bg-gray-200\" />\n \n {/* Content skeleton */}\n <div className=\"p-4 space-y-4\">\n {/* Title skeleton */}\n <div className=\"space-y-2\">\n <div className=\"h-5 bg-gray-200 rounded w-3/4\" />\n <div className=\"h-5 bg-gray-200 rounded w-1/2\" />\n </div>\n \n {/* Description skeleton */}\n <div className=\"space-y-2\">\n <div className=\"h-3 bg-gray-200 rounded w-full\" />\n <div className=\"h-3 bg-gray-200 rounded w-5/6\" />\n <div className=\"h-3 bg-gray-200 rounded w-4/6\" />\n </div>\n </div>\n </div>\n </div>\n );\n};","import type { LinkPreviewData } from '../components/LinkPreview/types';\n\nexport const getYouTubeVideoId = (url: string): string | null => {\n const regExp = /^.*((youtu.be\\/)|(v\\/)|(\\/u\\/\\w\\/)|(embed\\/)|(watch\\?))\\??v?=?([^#&?]*).*/;\n const match = url.match(regExp);\n return match && match[7].length === 11 ? match[7] : null;\n};\n\nexport const getYouTubeMetadata = async (url: string): Promise<LinkPreviewData> => {\n const videoId = getYouTubeVideoId(url);\n if (!videoId) throw new Error('Invalid YouTube URL');\n\n const oembedUrl = `https://www.youtube.com/oembed?url=${encodeURIComponent(url)}&format=json`;\n const response = await fetch(oembedUrl);\n const data = await response.json();\n\n return {\n title: data.title || 'YouTube Video',\n description: data.author_name ? `By ${data.author_name}` : '',\n image: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,\n url: url,\n type: 'video'\n };\n};","const CACHE_PREFIX = 'link-preview-';\nconst CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours\n\ninterface CacheItem<T> {\n data: T;\n timestamp: number;\n}\n\nexport const cacheGet = <T>(key: string): T | null => {\n try {\n const item = localStorage.getItem(`${CACHE_PREFIX}${key}`);\n if (!item) return null;\n\n const { data, timestamp }: CacheItem<T> = JSON.parse(item);\n if (Date.now() - timestamp > CACHE_DURATION) {\n localStorage.removeItem(`${CACHE_PREFIX}${key}`);\n return null;\n }\n\n return data;\n } catch {\n return null;\n }\n};\n\nexport const cacheSet = <T>(key: string, data: T): void => {\n try {\n const item: CacheItem<T> = {\n data,\n timestamp: Date.now(),\n };\n localStorage.setItem(`${CACHE_PREFIX}${key}`, JSON.stringify(item));\n } catch {\n // Ignore cache errors\n }\n};","export const FALLBACK_IMAGES = {\n default: 'https://images.unsplash.com/photo-1481627834876-b7833e8f5570?w=800&q=80',\n article: 'https://images.unsplash.com/photo-1532012197267-da84d127e765?w=800&q=80',\n video: 'https://images.unsplash.com/photo-1576097449798-7c7f90e1248a?w=800&q=80',\n};\n\nexport const getRandomFallbackImage = (type?: string): string => {\n if (type === 'video') return FALLBACK_IMAGES.video;\n if (type === 'article') return FALLBACK_IMAGES.article;\n return FALLBACK_IMAGES.default;\n};","import type { LinkPreviewData } from '../components/LinkPreview/types';\nimport { getYouTubeMetadata } from './youtube';\nimport { cacheGet, cacheSet } from './cache';\nimport { getRandomFallbackImage } from './fallback';\nimport axios from 'axios';\n\nconst CORS_PROXY = 'https://api.allorigins.win/get?url=';\n\nconst extractMetadata = (html: string, url: string): LinkPreviewData => {\n const parser = new DOMParser();\n const doc = parser.parseFromString(html, 'text/html');\n \n const getMetaContent = (selectors: string[]): string | null => {\n for (const selector of selectors) {\n const element = doc.querySelector(selector);\n const content = element?.getAttribute('content') || element?.getAttribute('value');\n if (content) return content;\n }\n return null;\n };\n\n const title = \n getMetaContent(['meta[property=\"og:title\"]', 'meta[name=\"twitter:title\"]']) ||\n doc.querySelector('title')?.textContent ||\n new URL(url).hostname;\n\n const description = \n getMetaContent([\n 'meta[property=\"og:description\"]',\n 'meta[name=\"twitter:description\"]',\n 'meta[name=\"description\"]'\n ]) || '';\n\n const image = \n getMetaContent([\n 'meta[property=\"og:image\"]',\n 'meta[property=\"og:image:url\"]',\n 'meta[name=\"twitter:image\"]'\n ]) || getRandomFallbackImage('article');\n\n const type = \n getMetaContent(['meta[property=\"og:type\"]']) || 'article';\n\n return {\n title,\n description,\n image,\n url,\n type\n };\n};\n\nconst fetchWithProxy = async (url: string): Promise<LinkPreviewData> => {\n try {\n const response = await axios.get(`${CORS_PROXY}${encodeURIComponent(url)}`);\n const contents = response.data.contents;\n return extractMetadata(contents, url);\n } catch (error) {\n throw new Error('Failed to fetch with proxy');\n }\n};\n\nconst isValidUrl = (url: string): boolean => {\n try {\n new URL(url);\n return true;\n } catch {\n return false;\n }\n};\n\nexport const fetchMetadata = async (url: string): Promise<LinkPreviewData> => {\n if (!isValidUrl(url)) {\n return {\n title: 'Invalid URL',\n description: 'The provided URL is not valid',\n image: getRandomFallbackImage(),\n url: url,\n type: 'website'\n };\n }\n\n // Check cache first\n const cachedData = cacheGet<LinkPreviewData>(url);\n if (cachedData) return cachedData;\n\n try {\n let metadata: LinkPreviewData;\n\n if (url.includes('youtube.com') || url.includes('youtu.be')) {\n metadata = await getYouTubeMetadata(url);\n } else {\n try {\n metadata = await fetchWithProxy(url);\n } catch (error) {\n console.error('Proxy fetch failed:', error);\n metadata = {\n title: new URL(url).hostname,\n description: 'No description available',\n image: getRandomFallbackImage('article'),\n url: url,\n type: 'article'\n };\n }\n }\n\n if (metadata.title !== new URL(url).hostname) {\n cacheSet(url, metadata);\n }\n \n return metadata;\n } catch (error) {\n console.error('Error fetching metadata:', error);\n \n return {\n title: new URL(url).hostname,\n description: 'No description available',\n image: getRandomFallbackImage('article'),\n url: url,\n type: 'article'\n };\n }\n};"],"mappings":"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","names":["jsx","jsxs","jsx"]}