5 changed files with 111 additions and 2 deletions
@ -0,0 +1,27 @@ |
|||||||
|
import { Image } from '@nextui-org/image' |
||||||
|
import { useFetchWebMetadata } from '@renderer/hooks/useFetchWebMetadata' |
||||||
|
import { cn } from '@renderer/lib/utils' |
||||||
|
|
||||||
|
export default function WebPreview({ url, className }: { url: string; className?: string }) { |
||||||
|
const { title, description, image } = useFetchWebMetadata(url) |
||||||
|
|
||||||
|
if (!title && !description && !image) { |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={cn('p-0 hover:bg-muted/50 cursor-pointer flex w-full', className)} |
||||||
|
onClick={(e) => { |
||||||
|
e.stopPropagation() |
||||||
|
window.open(url, '_blank') |
||||||
|
}} |
||||||
|
> |
||||||
|
{image && <Image src={image} className="rounded-l-lg object-cover w-2/5" removeWrapper />} |
||||||
|
<div className={`flex-1 w-0 p-2 border ${image ? 'rounded-r-lg' : 'rounded-lg'}`}> |
||||||
|
<div className="font-semibold truncate">{title}</div> |
||||||
|
<div className="text-sm text-muted-foreground line-clamp-2">{description}</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
import { TWebMetadata } from '@renderer/types' |
||||||
|
import { useEffect, useState } from 'react' |
||||||
|
import webService from '@renderer/services/web.service' |
||||||
|
|
||||||
|
export function useFetchWebMetadata(url: string) { |
||||||
|
const [metadata, setMetadata] = useState<TWebMetadata>({}) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
webService.fetchWebMetadata(url).then((metadata) => setMetadata(metadata)) |
||||||
|
}, [url]) |
||||||
|
|
||||||
|
return metadata |
||||||
|
} |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
import { TWebMetadata } from '@renderer/types' |
||||||
|
import DataLoader from 'dataloader' |
||||||
|
|
||||||
|
class WebService { |
||||||
|
static instance: WebService |
||||||
|
|
||||||
|
private webMetadataDataLoader = new DataLoader<string, TWebMetadata>(async (urls) => { |
||||||
|
return await Promise.all( |
||||||
|
urls.map(async (url) => { |
||||||
|
try { |
||||||
|
const res = await fetch(url) |
||||||
|
const html = await res.text() |
||||||
|
const parser = new DOMParser() |
||||||
|
const doc = parser.parseFromString(html, 'text/html') |
||||||
|
|
||||||
|
const title = |
||||||
|
doc.querySelector('meta[property="og:title"]')?.getAttribute('content') || |
||||||
|
doc.querySelector('title')?.textContent |
||||||
|
const description = |
||||||
|
doc.querySelector('meta[property="og:description"]')?.getAttribute('content') || |
||||||
|
(doc.querySelector('meta[name="description"]') as HTMLMetaElement | null)?.content |
||||||
|
const image = (doc.querySelector('meta[property="og:image"]') as HTMLMetaElement | null) |
||||||
|
?.content |
||||||
|
|
||||||
|
return { title, description, image } |
||||||
|
} catch (e) { |
||||||
|
console.error(e) |
||||||
|
return {} |
||||||
|
} |
||||||
|
}) |
||||||
|
) |
||||||
|
}) |
||||||
|
|
||||||
|
constructor() { |
||||||
|
if (!WebService.instance) { |
||||||
|
WebService.instance = this |
||||||
|
} |
||||||
|
return WebService.instance |
||||||
|
} |
||||||
|
|
||||||
|
async fetchWebMetadata(url: string) { |
||||||
|
return await this.webMetadataDataLoader.load(url) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const instance = new WebService() |
||||||
|
|
||||||
|
export default instance |
||||||
Loading…
Reference in new issue