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