mirror of https://github.com/TZGyn/shortener
added preview on add shortener dialog
parent
c17d978d26
commit
f758c6fd64
@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog'
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button'
|
||||
import { Input } from '$lib/components/ui/input'
|
||||
import { Label } from '$lib/components/ui/label'
|
||||
import { Loader2, PlusCircle } from 'lucide-svelte'
|
||||
import { invalidateAll } from '$app/navigation'
|
||||
import { toast } from 'svelte-sonner'
|
||||
|
||||
export let dialogOpen: boolean
|
||||
let inputLink = ''
|
||||
let isLoading = false
|
||||
|
||||
const addShortener = async () => {
|
||||
isLoading = true
|
||||
|
||||
const response = await fetch('/api/shortener', {
|
||||
method: 'post',
|
||||
body: JSON.stringify({ link: inputLink }),
|
||||
})
|
||||
|
||||
const responseData = await response.json()
|
||||
|
||||
isLoading = false
|
||||
|
||||
if (responseData.success) {
|
||||
toast.success('Successfully Created Shortener')
|
||||
await invalidateAll()
|
||||
dialogOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
let inputTimer: any
|
||||
let data: any
|
||||
|
||||
const getMetadata = async () => {
|
||||
clearTimeout(inputTimer)
|
||||
inputTimer = setTimeout(async () => {
|
||||
const response = await fetch(
|
||||
`/api/url/metadata?url=${inputLink}`,
|
||||
)
|
||||
data = await response.json()
|
||||
console.log(data)
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open={dialogOpen}>
|
||||
<Dialog.Trigger
|
||||
class={buttonVariants({ variant: 'default' }) + 'flex gap-2'}>
|
||||
<PlusCircle />
|
||||
Add Shortner
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Add Shortener</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Create A New Shortner Here. Click Add To Save.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<div class="grid gap-8 py-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label>Link</Label>
|
||||
<Input
|
||||
id="name"
|
||||
on:input={() => getMetadata()}
|
||||
bind:value={inputLink}
|
||||
placeholder="https://example.com"
|
||||
class="col-span-3" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<div class="font-bold">Preview</div>
|
||||
<div class="col-span-4 flex flex-col justify-center border">
|
||||
<div class="relative h-64 overflow-hidden">
|
||||
{#if data}
|
||||
<img
|
||||
src={data.image}
|
||||
alt=""
|
||||
class="h-64 w-full object-cover" />
|
||||
<div
|
||||
class="bg-secondary absolute bottom-2 left-2 rounded-lg px-2">
|
||||
{data.title}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog.Footer>
|
||||
<Button on:click={addShortener} class="flex gap-2">
|
||||
{#if isLoading}
|
||||
<Loader2 class="animate-spin" />
|
||||
{/if}
|
||||
Add
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@ -0,0 +1,113 @@
|
||||
import { z } from 'zod'
|
||||
import type { RequestHandler } from './$types'
|
||||
import { parse } from 'node-html-parser'
|
||||
import he from 'he'
|
||||
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
const url = event.url.searchParams.get('url')
|
||||
|
||||
if (!url || !z.string().url().safeParse(url).success) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
title: url,
|
||||
description: 'No description',
|
||||
image: null,
|
||||
}),
|
||||
)
|
||||
}
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'shortener-bot',
|
||||
},
|
||||
})
|
||||
if (!response) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
title: url,
|
||||
description: 'No description',
|
||||
image: null,
|
||||
}),
|
||||
)
|
||||
}
|
||||
const html = await response.text()
|
||||
const ast = parse(html)
|
||||
const metaTags = ast
|
||||
.querySelectorAll('meta')
|
||||
.map(({ attributes }) => {
|
||||
const property =
|
||||
attributes.property || attributes.name || attributes.href
|
||||
return {
|
||||
property,
|
||||
content: attributes.content,
|
||||
}
|
||||
})
|
||||
const titleTag = ast.querySelector('title')?.innerText
|
||||
const linkTags = ast
|
||||
.querySelectorAll('link')
|
||||
.map(({ attributes }) => {
|
||||
const { rel, href } = attributes
|
||||
return {
|
||||
rel,
|
||||
href,
|
||||
}
|
||||
})
|
||||
|
||||
let object: any = {}
|
||||
|
||||
for (let k in metaTags) {
|
||||
let { property, content } = metaTags[k]
|
||||
|
||||
property && (object[property] = content && he.decode(content))
|
||||
}
|
||||
|
||||
for (let m in linkTags) {
|
||||
let { rel, href } = linkTags[m]
|
||||
|
||||
rel && (object[rel] = href)
|
||||
}
|
||||
|
||||
const title =
|
||||
object['og:title'] || object['twitter:title'] || titleTag
|
||||
|
||||
const description =
|
||||
object['description'] ||
|
||||
object['og:description'] ||
|
||||
object['twitter:description']
|
||||
|
||||
const image =
|
||||
object['og:image'] ||
|
||||
object['twitter:image'] ||
|
||||
object['image_src'] ||
|
||||
object['icon'] ||
|
||||
object['shortcut icon']
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
title: title || url,
|
||||
description: description || 'No description',
|
||||
image: getRelativeUrl(url, image),
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
title: url,
|
||||
description: 'No description',
|
||||
image: null,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const getRelativeUrl = (url: string, imageUrl: string) => {
|
||||
if (!imageUrl) {
|
||||
return null
|
||||
}
|
||||
if (z.string().url().safeParse(imageUrl).success) {
|
||||
return imageUrl
|
||||
}
|
||||
const { protocol, host } = new URL(url)
|
||||
const baseURL = `${protocol}//${host}`
|
||||
return new URL(imageUrl, baseURL).toString()
|
||||
}
|
||||
Loading…
Reference in New Issue