5 changed files with 111 additions and 2 deletions
@ -0,0 +1,27 @@
@@ -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 @@
@@ -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 @@
@@ -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