added preview on add shortener dialog

pull/3/head
TZGyn 2 years ago
parent c17d978d26
commit f758c6fd64
Signed by: TZGyn
GPG Key ID: 122EAF77AE81FD4A

@ -8,17 +8,21 @@
"name": "link-shortener-svelte",
"version": "0.0.1",
"dependencies": {
"@types/he": "^1.2.3",
"apexcharts": "^3.44.0",
"argon2": "^0.31.2",
"bits-ui": "^0.9.8",
"clsx": "^2.0.0",
"drizzle-orm": "^0.29.0",
"formsnap": "^0.4.1",
"he": "^1.2.0",
"lucide-svelte": "^0.292.0",
"mode-watcher": "^0.0.7",
"mode-watcher": "^0.1.2",
"nanoid": "^5.0.3",
"node-html-parser": "^6.1.12",
"postgres": "^3.4.3",
"qrious": "^4.0.2",
"svelte-sonner": "^0.3.10",
"sveltekit-superforms": "^1.10.1",
"tailwind-merge": "^2.0.0",
"tailwind-variants": "^0.1.18",
@ -937,6 +941,11 @@
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/@types/he": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz",
"integrity": "sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA=="
},
"node_modules/@types/node": {
"version": "20.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.1.tgz",
@ -1176,6 +1185,11 @@
"integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
"dev": true
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -1453,6 +1467,21 @@
"node": ">= 0.6"
}
},
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
@ -1465,6 +1494,17 @@
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -1573,6 +1613,57 @@
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
]
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dreamopt": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/dreamopt/-/dreamopt-0.8.0.tgz",
@ -1705,6 +1796,17 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es5-ext": {
"version": "0.10.62",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
@ -2122,6 +2224,14 @@
"node": ">= 0.4"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"bin": {
"he": "bin/he"
}
},
"node_modules/heap": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz",
@ -2536,12 +2646,9 @@
}
},
"node_modules/mode-watcher": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.0.7.tgz",
"integrity": "sha512-3/RXHKzg9TjhkYo8YTx++Ai7a2Qd7BkYS3RFWaIhj0NPonXAF5JNegiZ3CTk1Q/k0wyhPQUlsWW0Y5ByCp6Y1g==",
"dependencies": {
"svelte-persisted-store": "^0.7.0"
},
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.1.2.tgz",
"integrity": "sha512-XTdPCdqC3kqSvB+Q262Kor983YJkkB2Z3vj9uqg5IqKQpOdiz+xB99Jihp8sWbyM67drC7KKp0Nt5FzCypZi2g==",
"peerDependencies": {
"svelte": "^4.0.0"
}
@ -2642,6 +2749,15 @@
"node": ">= 6.13.0"
}
},
"node_modules/node-html-parser": {
"version": "6.1.12",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.12.tgz",
"integrity": "sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==",
"dependencies": {
"css-select": "^5.1.0",
"he": "1.2.0"
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
@ -2690,6 +2806,17 @@
"set-blocking": "^2.0.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -3869,17 +3996,6 @@
"svelte": "^3.19.0 || ^4.0.0"
}
},
"node_modules/svelte-persisted-store": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/svelte-persisted-store/-/svelte-persisted-store-0.7.0.tgz",
"integrity": "sha512-PczYS60ysScQ0DmTCPXQm5rwt8FfQzSlx0lCbljbE6hTCKqXaw2bP8KH1+B7QTPmuiy/QbAk4pjYpNCmZhjyRw==",
"engines": {
"node": ">=0.14"
},
"peerDependencies": {
"svelte": "^3.48.0 || >4.0.0"
}
},
"node_modules/svelte-preprocess": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.0.tgz",
@ -3954,6 +4070,14 @@
"node": ">=12"
}
},
"node_modules/svelte-sonner": {
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.11.tgz",
"integrity": "sha512-TkjgDC7zr0waky81Z9CShXMD+4NQ7UASuRx0BhgQo8ZTDQQYk8X8MzJa3zVtZVa6RYJEiahHBXx8Zt/Ie9G5hg==",
"peerDependencies": {
"svelte": ">=3 <5"
}
},
"node_modules/sveltekit-superforms": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/sveltekit-superforms/-/sveltekit-superforms-1.10.2.tgz",

@ -32,15 +32,18 @@
},
"type": "module",
"dependencies": {
"@types/he": "^1.2.3",
"apexcharts": "^3.44.0",
"argon2": "^0.31.2",
"bits-ui": "^0.9.8",
"clsx": "^2.0.0",
"drizzle-orm": "^0.29.0",
"formsnap": "^0.4.1",
"he": "^1.2.0",
"lucide-svelte": "^0.292.0",
"mode-watcher": "^0.1.2",
"nanoid": "^5.0.3",
"node-html-parser": "^6.1.12",
"postgres": "^3.4.3",
"qrious": "^4.0.2",
"svelte-sonner": "^0.3.10",

@ -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>

@ -1,7 +1,7 @@
<script lang="ts">
import type { PageData } from './$types'
import { Separator } from '$lib/components/ui/separator'
import { Button, buttonVariants } from '$lib/components/ui/button'
import { Button } from '$lib/components/ui/button'
import * as Dialog from '$lib/components/ui/dialog'
import * as Card from '$lib/components/ui/card'
import * as DropdownMenu from '$lib/components/ui/dropdown-menu'
@ -14,39 +14,17 @@
ExternalLink,
Loader2,
MoreVertical,
PlusCircle,
QrCode,
} from 'lucide-svelte'
import { goto, invalidateAll } from '$app/navigation'
import Qr from '$lib/components/QR.svelte'
import { toast } from 'svelte-sonner'
import AddShortenerDialog from './(component)/AddShortenerDialog.svelte'
export let data: PageData
let selectedProject: any = data.selected_project
let dialogOpen = false
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 editDialogOpen = false
let editShortenerCode = ''
@ -95,38 +73,7 @@
<div class="flex min-h-[80px] items-center justify-between p-4">
<div class="text-3xl font-bold">Links</div>
<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-[425px]">
<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-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">Link</Label>
<Input
id="name"
bind:value={inputLink}
class="col-span-3" />
</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>
<AddShortenerDialog {dialogOpen} />
</div>
<Separator />

@ -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…
Cancel
Save