diff --git a/apps/web/src/components/external-site-preview.stories.tsx b/apps/web/src/components/external-site-preview.stories.tsx new file mode 100644 index 0000000..6a969bd --- /dev/null +++ b/apps/web/src/components/external-site-preview.stories.tsx @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from "@storybook/tanstack-react"; + +import { ExternalSitePreview } from "./external-site-preview"; + +const meta = { + title: "Tools/ExternalSitePreview", + component: ExternalSitePreview, + args: { + url: "https://shieldcn.dev", + title: "Live preview of Shieldcn", + imageAlt: "Shieldcn Open Graph preview", + className: "h-[28rem] w-[48rem] border border-border", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const WithImageFallback: Story = { + args: { + imageUrl: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1200' height='675'%3E%3Crect width='1200' height='675' fill='%23171717'/%3E%3Ctext x='600' y='338' text-anchor='middle' dominant-baseline='middle' fill='%23fafafa' font-family='Arial' font-size='54'%3EOpen Graph preview%3C/text%3E%3C/svg%3E", + }, +}; + +export const WithoutImageFallback: Story = { + args: { imageUrl: null }, +}; + +export const Mobile: Story = { + args: { + className: "h-[30rem] w-[22rem] border border-border", + imageUrl: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1200' height='675'%3E%3Crect width='1200' height='675' fill='%23171717'/%3E%3Ctext x='600' y='338' text-anchor='middle' dominant-baseline='middle' fill='%23fafafa' font-family='Arial' font-size='54'%3EOpen Graph preview%3C/text%3E%3C/svg%3E", + }, +}; diff --git a/apps/web/src/components/external-site-preview.tsx b/apps/web/src/components/external-site-preview.tsx new file mode 100644 index 0000000..fae80e3 --- /dev/null +++ b/apps/web/src/components/external-site-preview.tsx @@ -0,0 +1,71 @@ +import { useEffect, useState } from "react"; +import { HiArrowTopRightOnSquare as External, HiGlobeAlt as Globe, HiPhoto as ImageIcon } from "react-icons/hi2"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +export function ExternalSitePreview({ + url, + title, + imageUrl, + imageAlt = "", + className, +}: { + url: string; + title: string; + imageUrl?: string | null; + imageAlt?: string; + className?: string; +}) { + const [mode, setMode] = useState<"live" | "image">("live"); + const [sandbox, setSandbox] = useState(null); + + useEffect(() => { + try { + const crossOrigin = new URL(url).origin !== window.location.origin; + // Cross-origin sites may use their own storage and runtime while the + // browser's same-origin policy still prevents access to Modulora. + setSandbox(crossOrigin ? "allow-scripts allow-same-origin" : "allow-scripts"); + } catch { + setSandbox("allow-scripts"); + } + }, [url]); + + return ( +
+ {mode === "live" ? ( + sandbox ? ( +