Compare commits

..

4 Commits

Binary file not shown.

@ -1,130 +0,0 @@
<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,15 +1,6 @@
import { db } from '$lib/db' import { db } from '$lib/db'
import {
and,
asc,
desc,
eq,
getTableColumns,
ilike,
sql,
} from 'drizzle-orm'
import type { PageServerLoad } from './$types' import type { PageServerLoad } from './$types'
import { project, shortener, visitor } from '$lib/db/schema' import { shortener } from '$lib/db/schema'
import { fail, setError, superValidate } from 'sveltekit-superforms' import { fail, setError, superValidate } from 'sveltekit-superforms'
import { zod } from 'sveltekit-superforms/adapters' import { zod } from 'sveltekit-superforms/adapters'
import { formSchema } from './schema' import { formSchema } from './schema'
@ -22,113 +13,12 @@ import type { Project } from '$lib/db/types'
export const load = (async (event) => { export const load = (async (event) => {
const user = event.locals.user const user = event.locals.user
const project_uuid = 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)) {
perPage = 10
}
if (
sortBy !== 'latest' &&
sortBy !== 'oldest' &&
sortBy !== 'most_visited'
) {
sortBy = 'latest'
}
let project_id: string | undefined
let selected_project: { value: null | string; label: string } = {
value: null,
label: 'All',
}
if (project_uuid) {
try {
const project = await db.query.project.findFirst({
where: (project, { eq }) => eq(project.uuid, project_uuid),
})
project_id = project?.id
if (project?.name) {
selected_project.label = project.name
selected_project.value = project.uuid
}
} catch (error) {
project_id = undefined
}
}
const shortenerColumns = getTableColumns(shortener)
const projectColumns = getTableColumns(project)
const shorteners = db
.select({
...shortenerColumns,
projectName: project.name,
projectUuid: project.uuid,
project: { ...projectColumns },
visitorCount: sql<number>`count(${visitor.id})`,
})
.from(shortener)
.where(
and(
eq(shortener.userId, user.id),
project_id ? 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)
const pagination = db
.select({
total: sql<number>`count(*)`.as('total'),
})
.from(shortener)
.where(
and(
eq(shortener.userId, user.id),
project_id ? eq(shortener.projectId, project_id) : undefined,
search
? ilike(shortener.link, `%${decodeURI(search)}%`)
: undefined,
),
)
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 projects = db.query.project.findMany({ const projects = db.query.project.findMany({
where: (project, { eq }) => eq(project.userId, user.id), where: (project, { eq }) => eq(project.userId, user.id),
}) })
return { return {
shorteners, projects,
projects: await projects,
selected_project,
page,
perPage,
search,
sortBy,
pagination,
form: await superValidate({ active: true }, zod(formSchema), { form: await superValidate({ active: true }, zod(formSchema), {
errors: false, errors: false,
}), }),

@ -1,74 +1,74 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types' import type { PageData } from './$types'
import { cn } from '$lib/utils'
import { goto } from '$app/navigation'
import { browser } from '$app/environment'
import { page } from '$app/stores' import { page } from '$app/stores'
import { Button } from '$lib/components/ui/button' import { Button } from '$lib/components/ui/button'
import * as Select from '$lib/components/ui/select' import * as Select from '$lib/components/ui/select'
import * as Command from '$lib/components/ui/command'
import * as Popover from '$lib/components/ui/popover'
import * as Dialog from '$lib/components/ui/dialog' import * as Dialog from '$lib/components/ui/dialog'
import { Input } from '$lib/components/ui/input' import { Input } from '$lib/components/ui/input'
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte' import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte'
import { Skeleton } from '$lib/components/ui/skeleton' import { Skeleton } from '$lib/components/ui/skeleton'
import * as Drawer from '$lib/components/ui/drawer' import * as Drawer from '$lib/components/ui/drawer'
import * as Pagination from '$lib/components/ui/pagination'
import { import {
Check, ChevronLeft,
ChevronsUpDown, ChevronRight,
SortAscIcon, SortAscIcon,
SortDescIcon,
} from 'lucide-svelte' } from 'lucide-svelte'
import ShortenerCard from '$lib/components/ShortenerCard.svelte' import ShortenerCard from '$lib/components/ShortenerCard.svelte'
import CustomPaginationBar from '$lib/components/Custom-Pagination-Bar.svelte'
import Form from './(components)/form.svelte' import Form from './(components)/form.svelte'
import ProjectLinkQRPage from '../projects/[id]/links/[linkid]/qr/+page.svelte' import ProjectLinkQRPage from '../projects/[id]/links/[linkid]/qr/+page.svelte'
import LinkQRPage from './[id]/qr/+page.svelte' import LinkQRPage from './[id]/qr/+page.svelte'
import type { Project, Shortener } from '$lib/db/types'
import type { Selected } from 'bits-ui'
export let data: PageData export let data: PageData
let dialogOpen = false let dialogOpen = false
let open: boolean = false let search: string | null = ''
let drawerSelect: boolean = false let searchUpdateTimeout: any
let selectedProject: any = data.selected_project.label
let pageNumber = 1
let perPage = 12
let sortBy: Selected<string> = {
label: 'Latest',
value: 'latest',
}
let selectedProject: Selected<string> = {
label: 'All',
value: 'all',
}
$: selectedProject = data.selected_project.label const fetchShorteners = async (
page: number,
perPage: number,
sortBy: string,
project: string,
search: string | null,
) => {
const searchParams = new URLSearchParams()
let search: string | null = data.search if (page) searchParams.set('page', page.toString())
let searchUpdateTimeout: any if (perPage) searchParams.set('perPage', perPage.toString())
if (sortBy) searchParams.set('sortBy', sortBy)
if (project) searchParams.set('project', project)
if (search) searchParams.set('search', search)
$: browser && const response = await fetch(
search && `/api/shortener?${searchParams.toString()}`,
goto(
updateSearchParam([
{ name: 'search', value: search },
{
name: 'page',
value: 1,
},
]),
) )
const updateSearchParam = ( const data = await response.json()
params: { name: string; value: any }[],
) => { return {
const urlParams = new URLSearchParams(window.location.search) shorteners: data.shorteners as (Shortener & {
params.map(({ name, value }) => { visitorCount: number
if (value) { project: Project
urlParams.set(name, value) })[],
} else { pagination: data.pagination as { total: number },
urlParams.delete(name)
}
})
const searchParams = urlParams.toString()
if (searchParams) {
return '/dashboard/links?' + searchParams
} else {
return '/dashboard/links'
} }
} }
@ -89,100 +89,63 @@
</Drawer.Header> </Drawer.Header>
<Drawer.Footer class="gap-6"> <Drawer.Footer class="gap-6">
<Select.Root <Select.Root
selected={{ label: data.sortBy, value: data.sortBy }}> selected={sortBy}
onSelectedChange={(selected) => {
if (!selected) return
sortBy = selected
pageNumber = 1
}}>
<Select.Trigger> <Select.Trigger>
<Select.Value placeholder="Sort By" /> <Select.Value placeholder="Sort By" />
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
<Select.Group> <Select.Group>
<Select.Label>Sort By</Select.Label> <Select.Label>Sort By</Select.Label>
{#each ['latest', 'oldest', 'most_visited'] as sortBy} {#each [{ label: 'Latest', value: 'latest' }, { label: 'Oldest', value: 'oldest' }, { label: 'Most Visited', value: 'most_visited' }] as sortBy}
<a <Select.Item
href={updateSearchParam([ value={sortBy.value}
{ name: 'sortBy', value: sortBy }, label={sortBy.label}>
{ name: 'page', value: 1 }, {sortBy.label}
])}> </Select.Item>
<Select.Item value={sortBy} label={sortBy}>
{sortBy}
</Select.Item>
</a>
{/each} {/each}
</Select.Group> </Select.Group>
</Select.Content> </Select.Content>
<Select.Input name="favoriteFruit" /> <Select.Input name="favoriteFruit" />
</Select.Root> </Select.Root>
<Popover.Root bind:open={drawerSelect}> {#await data.projects}
<Popover.Trigger asChild let:builder> <Skeleton class="h-[40px] w-[180px]" />
<Button {:then projects}
builders={[builder]} <Select.Root
variant="outline" selected={selectedProject}
role="combobox" onSelectedChange={(selected) => {
aria-expanded={open} if (!selected) return
class="justify-between"> selectedProject = selected
{selectedProject} pageNumber = 1
<ChevronsUpDown }}>
class="ml-2 h-4 w-4 shrink-0 opacity-50" /> <Select.Trigger>
</Button> <Select.Value placeholder="Sort By" />
</Popover.Trigger> </Select.Trigger>
<Popover.Content class="w-[200px] p-0"> <Select.Content>
<Command.Root> <Select.Group>
<Command.Input placeholder="Search project..." /> <Select.Label>Project</Select.Label>
<Command.Empty>No project found.</Command.Empty> <Select.Separator />
<Command.Group> <Select.Item value={'all'} label={'All'}>
<a All
href={updateSearchParam([ </Select.Item>
{ <Select.Separator />
name: 'project', {#each projects as project}
value: undefined, <Select.Item
}, value={project.id}
{ label={project.name}>
name: 'page', {project.name}
value: 1, </Select.Item>
},
])}>
<Command.Item
onSelect={() => {
open = false
}}>
<Check
class={cn(
'mr-2 h-4 w-4',
data.selected_project.value !== null &&
'text-transparent',
)} />
All
</Command.Item>
</a>
{#each data.projects as project}
<a
href={updateSearchParam([
{
name: 'project',
value: project.uuid,
},
{
name: 'page',
value: 1,
},
])}>
<Command.Item
onSelect={() => {
open = false
}}>
<Check
class={cn(
'mr-2 h-4 w-4',
data.selected_project.value !==
project.uuid && 'text-transparent',
)} />
{project.name}
</Command.Item>
</a>
{/each} {/each}
</Command.Group> </Select.Group>
</Command.Root> </Select.Content>
</Popover.Content> <Select.Input name="favoriteFruit" />
</Popover.Root> </Select.Root>
{/await}
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<Input <Input
type="text" type="text"
@ -193,20 +156,10 @@
clearTimeout(searchUpdateTimeout) clearTimeout(searchUpdateTimeout)
searchUpdateTimeout = setTimeout(() => { searchUpdateTimeout = setTimeout(() => {
search = target.value search = target.value
pageNumber = 1
}, 500) }, 500)
}} /> }} />
<Button <Button disabled={!search} on:click={() => (search = '')}>
disabled={!search}
on:click={() =>
goto(
updateSearchParam([
{ name: 'search', value: '' },
{
name: 'page',
value: 1,
},
]),
)}>
Clear Clear
</Button> </Button>
</div> </div>
@ -218,95 +171,52 @@
</Drawer.Content> </Drawer.Content>
</Drawer.Root> </Drawer.Root>
<div class="hidden items-center gap-4 md:flex"> <div class="hidden items-center gap-4 md:flex">
<Popover.Root bind:open> {#await data.projects}
<Popover.Trigger asChild let:builder> <Skeleton class="h-[40px] w-[180px]" />
<Button {:then projects}
builders={[builder]} <Select.Root
variant="outline" selected={selectedProject}
role="combobox" onSelectedChange={(selected) => {
aria-expanded={open} if (!selected) return
class="w-[200px] justify-between"> selectedProject = selected
{selectedProject} pageNumber = 1
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" /> }}>
</Button> <Select.Trigger class="w-[180px]">
</Popover.Trigger> <Select.Value placeholder="Sort By" />
<Popover.Content class="w-[200px] p-0"> </Select.Trigger>
<Command.Root> <Select.Content>
<Command.Input placeholder="Search project..." /> <Select.Group>
<Command.Empty>No project found.</Command.Empty> <Select.Label>Project</Select.Label>
<Command.Group> <Select.Separator />
<a <Select.Item value={'all'} label={'All'}>All</Select.Item>
href={updateSearchParam([ <Select.Separator />
{ {#each projects as project}
name: 'project', <Select.Item value={project.id} label={project.name}>
value: undefined, {project.name}
}, </Select.Item>
{
name: 'page',
value: 1,
},
])}>
<Command.Item
onSelect={() => {
open = false
}}>
<Check
class={cn(
'mr-2 h-4 w-4',
data.selected_project.value !== null &&
'text-transparent',
)} />
All
</Command.Item>
</a>
{#each data.projects as project}
<a
href={updateSearchParam([
{
name: 'project',
value: project.uuid,
},
{
name: 'page',
value: 1,
},
])}>
<Command.Item
onSelect={() => {
open = false
}}>
<Check
class={cn(
'mr-2 h-4 w-4',
data.selected_project.value !== project.uuid &&
'text-transparent',
)} />
{project.name}
</Command.Item>
</a>
{/each} {/each}
</Command.Group> </Select.Group>
</Command.Root> </Select.Content>
</Popover.Content> <Select.Input name="favoriteFruit" />
</Popover.Root> </Select.Root>
{/await}
<Select.Root <Select.Root
selected={{ label: data.sortBy, value: data.sortBy }}> selected={sortBy}
onSelectedChange={(selected) => {
if (!selected) return
sortBy = selected
pageNumber = 1
}}>
<Select.Trigger class="w-[180px]"> <Select.Trigger class="w-[180px]">
<Select.Value placeholder="Sort By" /> <Select.Value placeholder="Sort By" />
</Select.Trigger> </Select.Trigger>
<Select.Content> <Select.Content>
<Select.Group> <Select.Group>
<Select.Label>Sort By</Select.Label> <Select.Label>Sort By</Select.Label>
{#each ['latest', 'oldest', 'most_visited'] as sortBy} {#each [{ label: 'Latest', value: 'latest' }, { label: 'Oldest', value: 'oldest' }, { label: 'Most Visited', value: 'most_visited' }] as sortBy}
<a <Select.Item value={sortBy.value} label={sortBy.label}>
href={updateSearchParam([ {sortBy.label}
{ name: 'sortBy', value: sortBy }, </Select.Item>
{ name: 'page', value: 1 },
])}>
<Select.Item value={sortBy} label={sortBy}>
{sortBy}
</Select.Item>
</a>
{/each} {/each}
</Select.Group> </Select.Group>
</Select.Content> </Select.Content>
@ -324,38 +234,32 @@
clearTimeout(searchUpdateTimeout) clearTimeout(searchUpdateTimeout)
searchUpdateTimeout = setTimeout(() => { searchUpdateTimeout = setTimeout(() => {
search = target.value search = target.value
pageNumber = 1
}, 500) }, 500)
}} /> }} />
<Button <Button disabled={!search} on:click={() => (search = '')}>
disabled={!search}
on:click={() =>
goto(
updateSearchParam([
{ name: 'search', value: '' },
{
name: 'page',
value: 1,
},
]),
)}>
Clear Clear
</Button> </Button>
</div> </div>
<Form bind:dialogOpen data={data.form} projects={data.projects} /> {#await data.projects then projects}
<Form bind:dialogOpen data={data.form} {projects} />
{/await}
</div> </div>
{#await data.shorteners} {#await fetchShorteners(pageNumber, perPage, sortBy.value, selectedProject.value, search)}
<div class="flex flex-wrap gap-4 p-4"> <div class="flex-grow">
{#each [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as _} <div class="flex flex-wrap gap-4 p-4">
<Skeleton class="h-[150px] w-[500px] rounded-lg" /> {#each [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as _}
{/each} <Skeleton class="h-[150px] w-[500px] rounded-lg" />
{/each}
</div>
</div> </div>
{:then shorteners} {:then result}
{#if shorteners.length > 0} {#if result.shorteners.length > 0}
<ScrollArea class="flex-grow"> <ScrollArea class="flex-grow">
<div <div
class="grid grid-cols-1 gap-4 p-4 md:grid-cols-[repeat(auto-fit,_minmax(500px,_1fr))]"> class="grid grid-cols-1 gap-4 p-4 md:grid-cols-[repeat(auto-fit,_minmax(500px,_1fr))]">
{#each shorteners as shortener} {#each result.shorteners as shortener}
<ShortenerCard <ShortenerCard
{shortener} {shortener}
project={shortener.project} project={shortener.project}
@ -363,6 +267,73 @@
{/each} {/each}
</div> </div>
</ScrollArea> </ScrollArea>
<div class="flex items-center justify-between border-t p-4">
<Select.Root
selected={{ label: perPage.toString(), value: perPage }}
onSelectedChange={(value) => {
if (value) {
perPage = value.value
pageNumber = 1
}
}}>
<Select.Trigger class="w-[180px]">
<Select.Value placeholder="Page Size" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Page Size</Select.Label>
{#each [12, 24, 48, 96] as pageSize}
<Select.Item
value={pageSize}
label={pageSize.toString()}>
{pageSize}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
<Select.Input name="favoriteFruit" />
</Select.Root>
<Pagination.Root
class="items-end"
count={result.pagination.total}
{perPage}
page={pageNumber}
let:pages
let:currentPage
onPageChange={(page) => {
pageNumber = page
}}>
<Pagination.Content>
<Pagination.Item>
<Pagination.PrevButton>
<ChevronLeft class="h-4 w-4" />
<span class="hidden sm:block">Previous</span>
</Pagination.PrevButton>
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<Pagination.Item>
<Pagination.Ellipsis />
</Pagination.Item>
{:else}
<Pagination.Item>
<Pagination.Link
{page}
isActive={currentPage === page.value}>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton>
<span class="hidden sm:block">Next</span>
<ChevronRight class="h-4 w-4" />
</Pagination.NextButton>
</Pagination.Item>
</Pagination.Content>
</Pagination.Root>
</div>
{:else} {:else}
<div class="flex flex-grow p-4"> <div class="flex flex-grow p-4">
<div <div
@ -388,14 +359,6 @@
{/if} {/if}
{/await} {/await}
{#await data.pagination then pagination}
<CustomPaginationBar
perPage={data.perPage}
page={data.page}
total={pagination[0].total}
path={'/dashboard/links'} />
{/await}
<Dialog.Root <Dialog.Root
bind:open={linkQROpen} bind:open={linkQROpen}
onOpenChange={(open) => { onOpenChange={(open) => {

@ -1,44 +1,100 @@
import { z } from 'zod' import { project, shortener, visitor } from '$lib/db/schema'
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 () => { export const GET: RequestHandler = async (event) => {
return new Response() const user = event.locals.user
}
const shortenerInsertSchema = z.object({ const project_id = event.url.searchParams.get('project')
link: z.string().url('Link must be in url format'), const search = event.url.searchParams.get('search')
projectId: z.string().nullish(), 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',
)
export const POST: RequestHandler = async (event) => { if (isNaN(page)) {
const body = await event.request.json() page = 1
}
const shortenerInsert = shortenerInsertSchema.safeParse(body) if (isNaN(perPage)) {
perPage = 10
}
if (!shortenerInsert.success) { if (
return new Response( sortBy !== 'latest' &&
JSON.stringify({ sortBy !== 'oldest' &&
success: false, sortBy !== 'most_visited'
message: 'Invalid Link', ) {
}), sortBy = 'latest'
)
} }
const user = event.locals.user const shortenerColumns = getTableColumns(shortener)
const projectColumns = getTableColumns(project)
const code = nanoid(8) const shorteners = db
.select({
...shortenerColumns,
projectName: project.name,
projectUuid: project.uuid,
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)
await db.insert(shortener).values({ if (sortBy === 'latest') {
id: generateId(8), shorteners.orderBy(desc(shortener.createdAt))
link: shortenerInsert.data.link, } else if (sortBy === 'oldest') {
projectId: shortenerInsert.data.projectId, shorteners.orderBy(asc(shortener.createdAt))
userId: user.id, } else if (sortBy === 'most_visited') {
code: code, shorteners.orderBy(sql`count(${visitor.id}) desc`)
}) }
return new Response(JSON.stringify({ success: true })) const pagination = db
.select({
total: sql<number>`count(*)`.as('total'),
})
.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({
success: true,
shorteners: await shorteners,
pagination: (await pagination)[0],
})
} }

Loading…
Cancel
Save