mirror of https://github.com/TZGyn/shortener
Compare commits
No commits in common. 'dfa3d34768e0c2a168d06eb0c13e868a71d3ec5d' and 'f9f3db6a1f46e1437199838752e2daca35539475' have entirely different histories.
dfa3d34768
...
f9f3db6a1f
Binary file not shown.
@ -0,0 +1,130 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog'
|
||||||
|
import * as Select from '$lib/components/ui/select'
|
||||||
|
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'
|
||||||
|
import type { Project } from '$lib/db/types'
|
||||||
|
|
||||||
|
export let projects: Project[]
|
||||||
|
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,
|
||||||
|
projectId: shortenerCategory
|
||||||
|
? shortenerCategory.value
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const responseData = await response.json()
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
|
||||||
|
if (responseData.success) {
|
||||||
|
toast.success('Successfully Created Shortener')
|
||||||
|
await invalidateAll()
|
||||||
|
dialogOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputTimer: any
|
||||||
|
let previewData: any
|
||||||
|
let isPreviewLoading: boolean = false
|
||||||
|
let shortenerCategory: any = undefined
|
||||||
|
|
||||||
|
const getMetadata = async () => {
|
||||||
|
isPreviewLoading = true
|
||||||
|
clearTimeout(inputTimer)
|
||||||
|
inputTimer = setTimeout(async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/url/metadata?url=${inputLink}`,
|
||||||
|
)
|
||||||
|
previewData = await response.json()
|
||||||
|
isPreviewLoading = false
|
||||||
|
console.log(previewData)
|
||||||
|
}, 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 gap-4 items-center">
|
||||||
|
<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 gap-4 items-center">
|
||||||
|
<Label>Category</Label>
|
||||||
|
<Select.Root
|
||||||
|
bind:selected={shortenerCategory}
|
||||||
|
multiple={false}>
|
||||||
|
<Select.Trigger class="col-span-3">
|
||||||
|
<Select.Value placeholder="Select a Category" />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{#each projects as project}
|
||||||
|
<Select.Item value={project.id}
|
||||||
|
>{project.name}</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 gap-4 items-center">
|
||||||
|
<div class="font-bold">Preview</div>
|
||||||
|
<div class="flex flex-col col-span-4 justify-center border">
|
||||||
|
<div class="overflow-hidden relative h-64">
|
||||||
|
{#if isPreviewLoading}
|
||||||
|
<div class="flex justify-center items-center h-full">
|
||||||
|
<Loader2 class="animate-spin" />
|
||||||
|
</div>
|
||||||
|
{:else if previewData}
|
||||||
|
<img
|
||||||
|
src={previewData.image}
|
||||||
|
alt=""
|
||||||
|
class="object-cover w-full h-64" />
|
||||||
|
<div
|
||||||
|
class="absolute bottom-2 left-2 px-2 rounded-lg bg-secondary">
|
||||||
|
{previewData.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,100 +1,44 @@
|
|||||||
import { project, shortener, visitor } from '$lib/db/schema'
|
import { z } from 'zod'
|
||||||
import {
|
|
||||||
and,
|
|
||||||
asc,
|
|
||||||
desc,
|
|
||||||
eq,
|
|
||||||
getTableColumns,
|
|
||||||
ilike,
|
|
||||||
sql,
|
|
||||||
} from 'drizzle-orm'
|
|
||||||
import type { RequestHandler } from './$types'
|
import type { RequestHandler } from './$types'
|
||||||
import { db } from '$lib/db'
|
import { db } from '$lib/db'
|
||||||
|
import { shortener } from '$lib/db/schema'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import { generateId } from 'lucia'
|
||||||
|
|
||||||
export const GET: RequestHandler = async (event) => {
|
export const GET: RequestHandler = async () => {
|
||||||
const user = event.locals.user
|
return new Response()
|
||||||
|
|
||||||
const project_id = event.url.searchParams.get('project')
|
|
||||||
const search = event.url.searchParams.get('search')
|
|
||||||
let sortBy = event.url.searchParams.get('sortBy')
|
|
||||||
let page = parseInt(event.url.searchParams.get('page') ?? '1')
|
|
||||||
let perPage = parseInt(
|
|
||||||
event.url.searchParams.get('perPage') ?? '12',
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isNaN(page)) {
|
|
||||||
page = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNaN(perPage)) {
|
const shortenerInsertSchema = z.object({
|
||||||
perPage = 10
|
link: z.string().url('Link must be in url format'),
|
||||||
}
|
projectId: z.string().nullish(),
|
||||||
|
})
|
||||||
|
|
||||||
if (
|
export const POST: RequestHandler = async (event) => {
|
||||||
sortBy !== 'latest' &&
|
const body = await event.request.json()
|
||||||
sortBy !== 'oldest' &&
|
|
||||||
sortBy !== 'most_visited'
|
|
||||||
) {
|
|
||||||
sortBy = 'latest'
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortenerColumns = getTableColumns(shortener)
|
const shortenerInsert = shortenerInsertSchema.safeParse(body)
|
||||||
const projectColumns = getTableColumns(project)
|
|
||||||
|
|
||||||
const shorteners = db
|
if (!shortenerInsert.success) {
|
||||||
.select({
|
return new Response(
|
||||||
...shortenerColumns,
|
JSON.stringify({
|
||||||
projectName: project.name,
|
success: false,
|
||||||
projectUuid: project.uuid,
|
message: 'Invalid Link',
|
||||||
project: { ...projectColumns },
|
}),
|
||||||
visitorCount: sql<number>`count(${visitor.id})`,
|
|
||||||
})
|
|
||||||
.from(shortener)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(shortener.userId, user.id),
|
|
||||||
project_id && project_id !== 'all'
|
|
||||||
? eq(shortener.projectId, project_id)
|
|
||||||
: undefined,
|
|
||||||
search
|
|
||||||
? ilike(shortener.link, `%${decodeURI(search)}%`)
|
|
||||||
: undefined,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.leftJoin(visitor, eq(shortener.id, visitor.shortenerId))
|
|
||||||
.leftJoin(project, eq(shortener.projectId, project.id))
|
|
||||||
.groupBy(shortener.id, project.id)
|
|
||||||
.offset(perPage * (page - 1))
|
|
||||||
.limit(perPage)
|
|
||||||
|
|
||||||
if (sortBy === 'latest') {
|
|
||||||
shorteners.orderBy(desc(shortener.createdAt))
|
|
||||||
} else if (sortBy === 'oldest') {
|
|
||||||
shorteners.orderBy(asc(shortener.createdAt))
|
|
||||||
} else if (sortBy === 'most_visited') {
|
|
||||||
shorteners.orderBy(sql`count(${visitor.id}) desc`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pagination = db
|
const user = event.locals.user
|
||||||
.select({
|
|
||||||
total: sql<number>`count(*)`.as('total'),
|
const code = nanoid(8)
|
||||||
})
|
|
||||||
.from(shortener)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(shortener.userId, user.id),
|
|
||||||
project_id && project_id !== 'all'
|
|
||||||
? eq(shortener.projectId, project_id)
|
|
||||||
: undefined,
|
|
||||||
search
|
|
||||||
? ilike(shortener.link, `%${decodeURI(search)}%`)
|
|
||||||
: undefined,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response.json({
|
await db.insert(shortener).values({
|
||||||
success: true,
|
id: generateId(8),
|
||||||
shorteners: await shorteners,
|
link: shortenerInsert.data.link,
|
||||||
pagination: (await pagination)[0],
|
projectId: shortenerInsert.data.projectId,
|
||||||
|
userId: user.id,
|
||||||
|
code: code,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true }))
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue