Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cta.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"projectName": "latest-ui",
"projectName": "doras-ui",
"mode": "file-router",
"typescript": true,
"tailwind": true,
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- If you're creating a Tanstack Start/Router route, you must create the file first with a touch command, as auto generated content is populated and the file gets currupted. So touch the file, then read it and update it.
6 changes: 6 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
Expand Down Expand Up @@ -355,6 +357,8 @@

"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],

"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],

"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],

"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
Expand All @@ -371,6 +375,8 @@

"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],

"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],

"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],

"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "doras-ui",
"private": true,
"type": "module",
"repository": "https://github.com/dorasto/ui",
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
Expand All @@ -21,8 +22,10 @@
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.13",
Expand Down
Binary file added public/components/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 4 additions & 5 deletions public/r/clipboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"name": "clipboard",
"type": "registry:block",
"title": "Clipboard",
"author": "Tommy Lundy <tommerty@doras.to>",
"description": "A simple method to copy items to the clipboard",
"registryDependencies": [
"button",
Expand All @@ -12,13 +13,11 @@
{
"path": "registry/clipboard/clipboard.tsx",
"content": "\"use client\";\nimport type React from \"react\";\nimport { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { Check, Copy } from \"lucide-react\";\n\nexport interface ClipboardProps {\n\ttextToCopy: string;\n\tchildren?: React.ReactNode;\n\tclassName?: string;\n\ticonSize?: number;\n\tcopiedDuration?: number;\n\ttooltipText?: string;\n\ttooltipCopiedText?: string;\n\tshowTooltip?: boolean;\n\tcopyIcon?: React.ReactNode;\n\tcheckIcon?: React.ReactNode;\n\tcheckIconClassName?: string;\n\tcopyIconClassName?: string;\n\tonCopy?: () => void;\n\tonError?: (error: Error) => void;\n\ttooltipDelayDuration?: number;\n\tdisabled?: boolean;\n}\n\nexport default function Clipboard({\n\ttextToCopy,\n\tchildren,\n\tclassName,\n\ticonSize = 16,\n\tcopiedDuration = 1500,\n\ttooltipText = \"Click to copy\",\n\ttooltipCopiedText = \"Copied!\",\n\tshowTooltip = true,\n\tcopyIcon,\n\tcheckIcon,\n\tcheckIconClassName,\n\tcopyIconClassName,\n\tonCopy,\n\tonError,\n\ttooltipDelayDuration = 0,\n\tdisabled = false,\n}: ClipboardProps) {\n\tconst [copied, setCopied] = useState<boolean>(false);\n\n\tconst handleCopy = async () => {\n\t\ttry {\n\t\t\tawait navigator.clipboard.writeText(textToCopy);\n\t\t\tsetCopied(true);\n\t\t\tonCopy?.();\n\t\t\tsetTimeout(() => setCopied(false), copiedDuration);\n\t\t} catch (err) {\n\t\t\tconst error = err instanceof Error ? err : new Error(\"Failed to copy text\");\n\t\t\tconsole.error(\"Failed to copy text: \", error);\n\t\t\tonError?.(error);\n\t\t}\n\t};\n\n\tconst defaultCopyIcon = copyIcon || <Copy size={iconSize} aria-hidden=\"true\" />;\n\tconst defaultCheckIcon = checkIcon || (\n\t\t<Check className={cn(checkIconClassName)} size={iconSize} aria-hidden=\"true\" />\n\t);\n\n\tconst triggerElement = children ? (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\tonClick={disabled || copied ? undefined : handleCopy}\n\t\t\tclassName=\"cursor-pointer border-none bg-transparent p-0\"\n\t\t\tdisabled={disabled || copied}\n\t\t\taria-label={copied ? \"Copied\" : \"Copy to clipboard\"}\n\t\t>\n\t\t\t{children}\n\t\t</button>\n\t) : (\n\t\t<Button\n \n\t\t\tclassName={cn(\"disabled:opacity-100 relative\", className)}\n\t\t\tonClick={handleCopy}\n\t\t\taria-label={copied ? \"Copied\" : \"Copy to clipboard\"}\n\t\t\tdisabled={copied || disabled}\n\t\t>\n\t\t\t<div className={cn(\"transition-all\", copied ? \"scale-100 opacity-100\" : \"scale-0 opacity-0\")}>\n\t\t\t\t{defaultCheckIcon}\n\t\t\t</div>\n\t\t\t<div\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"absolute transition-all\",\n\t\t\t\t\tcopied ? \"scale-0 opacity-0\" : \"scale-100 opacity-100\",\n\t\t\t\t\tcopyIconClassName\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{defaultCopyIcon}\n\t\t\t</div>\n\t\t</Button>\n\t);\n\n\tif (!showTooltip) {\n\t\treturn triggerElement;\n\t}\n\n\treturn (\n\t\t<TooltipProvider delayDuration={tooltipDelayDuration}>\n\t\t\t<Tooltip>\n\t\t\t\t<TooltipTrigger asChild>{triggerElement}</TooltipTrigger>\n\t\t\t\t<TooltipContent className=\"px-2 py-1 text-xs\">{copied ? tooltipCopiedText : tooltipText}</TooltipContent>\n\t\t\t</Tooltip>\n\t\t</TooltipProvider>\n\t);\n}",
"type": "registry:component"
"type": "registry:component",
"target": "components/doras-ui/clipboard.tsx"
}
],
"meta": {
"author": "tommerty"
},
"categories": [
"dashboard"
"general"
]
}
22 changes: 22 additions & 0 deletions public/r/preview.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "preview",
"type": "registry:block",
"title": "URL Preview",
"author": "Tommy Lundy <tommerty@doras.to>",
"description": "Show website metadata and OpenGraph images when hovering over URLs",
"registryDependencies": [
"hover-card"
],
"files": [
{
"path": "registry/preview/preview.tsx",
"content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useEffect, useState } from \"react\"\nimport {\n\tHoverCard,\n\tHoverCardContent,\n\tHoverCardTrigger,\n} from \"@/components/ui/hover-card\"\nimport { IconExternalLink } from \"@tabler/icons-react\"\n\ntype PreviewMetadata = {\n\ttitle: string | null\n\tdescription: string | null\n\timage: string | null\n\turl: string | null\n}\n\nexport interface PreviewProps {\n\turl: string\n\tchildren?: React.ReactNode\n\tshowImage?: boolean\n\tshowTitle?: boolean\n\tshowDescription?: boolean\n\tclassName?: string\n\tcontentClassName?: string\n\tonError?: (error: Error) => void\n}\n\nconst fetchMetadata = async (url: string): Promise<PreviewMetadata> => {\n\ttry {\n\t\t// Use a CORS proxy for client-side requests\n\t\tconst proxyUrl = `/api/preview?url=${encodeURIComponent(url)}`\n\t\tconst response = await fetch(proxyUrl)\n\t\t\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to fetch metadata: ${response.statusText}`)\n\t\t}\n\t\t\n\t\treturn await response.json()\n\t} catch (error) {\n\t\t// Fallback to basic metadata from URL\n\t\tconst domain = new URL(url).hostname\n\t\treturn {\n\t\t\ttitle: domain,\n\t\t\tdescription: `Visit ${domain}`,\n\t\t\timage: null,\n\t\t\turl,\n\t\t}\n\t}\n}\n\nexport function Preview({\n\turl,\n\tchildren,\n\tshowImage = true,\n\tshowTitle = true,\n\tshowDescription = true,\n\tclassName,\n\tcontentClassName,\n\tonError,\n}: PreviewProps) {\n\tconst [metadata, setMetadata] = useState<PreviewMetadata | null>(null)\n\tconst [loading, setLoading] = useState(false)\n\n\tuseEffect(() => {\n\t\tasync function fetchData() {\n\t\t\tsetLoading(true)\n\t\t\ttry {\n\t\t\t\tconst data = await fetchMetadata(url)\n\t\t\t\tsetMetadata(data)\n\t\t\t} catch (error) {\n\t\t\t\tconst err = error instanceof Error ? error : new Error('Unknown error')\n\t\t\t\tonError?.(err)\n\t\t\t\t\n\t\t\t\t// Fallback metadata\n\t\t\t\tconst domain = new URL(url).hostname\n\t\t\t\tsetMetadata({\n\t\t\t\t\ttitle: domain,\n\t\t\t\t\tdescription: `Visit ${domain}`,\n\t\t\t\t\timage: null,\n\t\t\t\t\turl,\n\t\t\t\t})\n\t\t\t} finally {\n\t\t\t\tsetLoading(false)\n\t\t\t}\n\t\t}\n\n\t\tfetchData()\n\t}, [url, onError])\n\n\tconst defaultTrigger = (\n\t\t<a\n\t\t\thref={url}\n\t\t\tclassName=\"text-primary hover:underline inline-flex items-center gap-1\"\n\t\t>\n\t\t\t{new URL(url).hostname}\n\t\t\t<IconExternalLink className=\"size-3\" />\n\t\t</a>\n\t)\n\n\treturn (\n\t\t<HoverCard openDelay={200}>\n\t\t\t<HoverCardTrigger asChild className={className}>\n\t\t\t\t{children || defaultTrigger}\n\t\t\t</HoverCardTrigger>\n\t\t\t<HoverCardContent className={cn(\"md:w-80 p-0 overflow-hidden border\", contentClassName)}>\n <a href={url}>\n\t\t\t\t{loading ? (\n\t\t\t\t\t<div className=\"flex items-center justify-center py-4\">\n\t\t\t\t\t\t<div className=\"text-sm text-muted-foreground\">Loading...</div>\n\t\t\t\t\t</div>\n\t\t\t\t) : (\n\t\t\t\t\t<div>\n\t\t\t\t\t\t{showImage && metadata?.image && (\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={metadata.image}\n\t\t\t\t\t\t\t\talt={metadata.title || \"\"}\n\t\t\t\t\t\t\t\tclassName=\"aspect-video w-full border object-cover bg-muted\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<div className=\"p-3\">\n\t\t\t\t\t\t\t{showTitle && metadata?.title && (\n\t\t\t\t\t\t\t\t<h4 className=\"font-semibold text-sm leading-tight\">\n\t\t\t\t\t\t\t\t\t{metadata.title}\n\t\t\t\t\t\t\t\t</h4>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{showDescription && metadata?.description && (\n\t\t\t\t\t\t\t\t<p className=\"text-muted-foreground text-xs leading-relaxed line-clamp-2\">\n\t\t\t\t\t\t\t\t\t{metadata.description}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t)}</a>\n\t\t\t</HoverCardContent>\n\t\t</HoverCard>\n\t)\n}\n",
"type": "registry:component",
"target": "components/doras-ui/preview.tsx"
}
],
"categories": [
"general"
]
}
30 changes: 24 additions & 6 deletions registry.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,43 @@
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme",
"name": "doras-ui",
"homepage": "https://ui.doras.to",
"items": [
{
"name": "clipboard",
"type": "registry:block",
"title": "Clipboard",
"author": "Tommy Lundy <tommerty@doras.to>",
"description": "A simple method to copy items to the clipboard",
"categories": ["dashboard"],
"meta": {
"author": "tommerty"
},
"categories": ["general"],

"registryDependencies": [
"button",
"tooltip"
],
"files": [
{
"path": "registry/clipboard/clipboard.tsx",
"type": "registry:component"
"type": "registry:component",
"target": "components/doras-ui/clipboard.tsx"
}
]
},
{
"name": "preview",
"type": "registry:block",
"title": "URL Preview",
"author": "Tommy Lundy <tommerty@doras.to>",
"description": "Show website metadata and OpenGraph images when hovering over URLs",
"categories": ["general"],
"registryDependencies": [
"hover-card"
],
"files": [
{
"path": "registry/preview/preview.tsx",
"type": "registry:component",
"target": "components/doras-ui/preview.tsx"
}
]
}
Expand Down
61 changes: 61 additions & 0 deletions registry/preview/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Preview Component

A hover card component that displays rich URL previews with OpenGraph metadata.

## Files

- `preview.tsx` - Main client component with composable parts
- `preview-server.ts` - Server utility for fetching metadata

## Usage in your project

This component follows the block structure defined in `/src/content/README.md`.

### Examples Location
All examples are in `/src/content/preview/examples.tsx` as named exports:
- `Example01` - Basic usage
- `Example02` - Server-side metadata fetching
- `Example03` - Custom styling variations

### Metadata Location
Block metadata is in `/src/content/blocks-metadata.ts`

### Documentation
Full documentation is in `/src/content/preview/docs.mdx`

## Quick Start

```tsx
import {
Preview,
PreviewContent,
PreviewDescription,
PreviewImage,
PreviewLink,
PreviewTitle,
PreviewTrigger,
} from "@@/registry/preview/preview"

<Preview>
<PreviewTrigger asChild>
<a href="https://github.com">GitHub</a>
</PreviewTrigger>
<PreviewContent className="w-80">
<PreviewImage src="..." alt="..." />
<div className="space-y-2 mt-3">
<PreviewTitle>Title</PreviewTitle>
<PreviewDescription>Description</PreviewDescription>
<PreviewLink href="https://github.com">github.com</PreviewLink>
</div>
</PreviewContent>
</Preview>
```

## Server-Side Fetching

```tsx
import { fetchPreview } from "@@/registry/preview/preview-server"

const metadata = await fetchPreview("https://github.com")
// Returns: { title, description, image, url }
```
54 changes: 54 additions & 0 deletions registry/preview/preview-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const TITLE_REGEX = /<title[^>]*>([^<]+)<\/title>/
const OG_TITLE_REGEX =
/<meta[^>]*property="og:title"[^>]*content="([^"]+)"/
const DESCRIPTION_REGEX =
/<meta[^>]*name="description"[^>]*content="([^"]+)"/
const OG_DESCRIPTION_REGEX =
/<meta[^>]*property="og:description"[^>]*content="([^"]+)"/
const OG_IMAGE_REGEX =
/<meta[^>]*property="og:image"[^>]*content="([^"]+)"/
const OG_URL_REGEX = /<meta[^>]*property="og:url"[^>]*content="([^"]+)"/

export type PreviewMetadata = {
title: string | null
description: string | null
image: string | null
url: string | null
}

export const fetchPreview = async (url: string): Promise<PreviewMetadata> => {
try {
const response = await fetch(url, {
headers: {
"User-Agent":
"Mozilla/5.0 (compatible; PreviewBot/1.0; +https://example.com/bot)",
},
})

if (!response.ok) {
throw new Error(`Failed to fetch: ${response.statusText}`)
}

const data = await response.text()
const titleMatch = data.match(OG_TITLE_REGEX) || data.match(TITLE_REGEX)
const descriptionMatch =
data.match(OG_DESCRIPTION_REGEX) || data.match(DESCRIPTION_REGEX)
const imageMatch = data.match(OG_IMAGE_REGEX)
const urlMatch = data.match(OG_URL_REGEX)

return {
title: titleMatch?.at(1) ?? null,
description: descriptionMatch?.at(1) ?? null,
image: imageMatch?.at(1) ?? null,
url: urlMatch?.at(1) ?? url,
}
} catch (error) {
console.error("Error fetching preview:", error)
return {
title: null,
description: null,
image: null,
url,
}
}
}
Loading
Loading