UNPKG

react-link-preview-card

Version:

A React component for generating beautiful link preview cards with metadata

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