Compare commits

..

No commits in common. '9a044801dbb0d8545e49920a82f4349c85b46010' and '56764e4aa6d6bfb8c845ce5621e3e173c7c73c78' have entirely different histories.

@ -1 +0,0 @@
ALTER TABLE "shortener" DROP CONSTRAINT "shortener_code_unique";

@ -1,457 +0,0 @@
{
"id": "c91acbf2-ca0a-4726-b2b2-70fd1f8bd9ff",
"prevId": "49d0736b-4fb4-4ab7-bc0e-b3401b2dcc1d",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.email_verification_token": {
"name": "email_verification_token",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.project": {
"name": "project",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"uuid": {
"name": "uuid",
"type": "uuid",
"primaryKey": false,
"notNull": false,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"qr_background": {
"name": "qr_background",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"default": "'#ffffff'"
},
"qr_foreground": {
"name": "qr_foreground",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"default": "'#000000'"
},
"domain_status": {
"name": "domain_status",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "'verified'"
},
"enable_custom_domain": {
"name": "enable_custom_domain",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"custom_ip": {
"name": "custom_ip",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"custom_domain_id": {
"name": "custom_domain_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"custom_domain": {
"name": "custom_domain",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"qr_corner_square_style": {
"name": "qr_corner_square_style",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'square'"
},
"qr_dot_style": {
"name": "qr_dot_style",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'square'"
},
"qr_image_base64": {
"name": "qr_image_base64",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.shortener": {
"name": "shortener",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"link": {
"name": "link",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"ios": {
"name": "ios",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"ios_link": {
"name": "ios_link",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"android": {
"name": "android",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"android_link": {
"name": "android_link",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"code": {
"name": "code",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"project_id": {
"name": "project_id",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"uuid": {
"name": "uuid",
"type": "uuid",
"primaryKey": false,
"notNull": false,
"default": "gen_random_uuid()"
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"google_id": {
"name": "google_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"username": {
"name": "username",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"plan": {
"name": "plan",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "'free'"
},
"stripe_customer_id": {
"name": "stripe_customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"qr_background": {
"name": "qr_background",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"default": "'#fff'"
},
"qr_foreground": {
"name": "qr_foreground",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"default": "'#000'"
},
"qr_corner_square_style": {
"name": "qr_corner_square_style",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'square'"
},
"qr_dot_style": {
"name": "qr_dot_style",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'square'"
},
"qr_image_base64": {
"name": "qr_image_base64",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
}
},
"public.visitor": {
"name": "visitor",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"shortener_id": {
"name": "shortener_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"country_code": {
"name": "country_code",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"country": {
"name": "country",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"device_type": {
"name": "device_type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"device_vendor": {
"name": "device_vendor",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"os": {
"name": "os",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"browser": {
"name": "browser",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"referer": {
"name": "referer",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"default": "''"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

@ -190,13 +190,6 @@
"when": 1725897203517,
"tag": "0026_natural_tattoo",
"breakpoints": true
},
{
"idx": 27,
"version": "7",
"when": 1726769139213,
"tag": "0027_funny_wasp",
"breakpoints": true
}
]
}

@ -20,16 +20,17 @@
} from 'lucide-svelte'
import DeleteShortenerDialog from './DeleteShortenerDialog.svelte'
import EditLinkPage from '$lib/../routes/(app)/dashboard/links/[id]/edit/+page.svelte'
import ProjectEditLinkPage from '$lib/../routes/(app)/dashboard/projects/[id]/links/[linkid]/edit/+page.svelte'
import { goto, preloadData, pushState } from '$app/navigation'
import { cn } from '$lib/utils'
import type { page } from '$app/stores'
export let shortener: Shortener & {
projectName: string | null
projectUuid: string | null
visitorCount: number
project: Project | null
}
export let project: Project | null
export let shortener_url: string
let deleteDialogOpen = false
@ -40,27 +41,21 @@
}
const getUrl = () => {
if (project) {
return `/dashboard/projects/${project.uuid}`
if (shortener.projectUuid) {
return `/dashboard/projects/${shortener.projectUuid}`
}
return '/dashboard'
}
let editProjectLinkOpen = false
let editData:
| typeof $page.state.editLink
| typeof $page.state.editProjectLink
let editData: typeof $page.state.editLink
const showEditModal = async (code: string) => {
const href = project
? `/dashboard/projects/${project.uuid}/links/${shortener.id}/edit`
: `/dashboard/links/${code}/edit`
const href = `/dashboard/links/${code}/edit`
const result = await preloadData(href)
if (result.type === 'loaded' && result.status === 200) {
editData = project
? (result.data as typeof $page.state.editProjectLink)
: (result.data as typeof $page.state.editLink)
editData = result.data as typeof $page.state.editLink
editProjectLinkOpen = true
} else {
// something bad happened! try navigating
@ -89,8 +84,8 @@
isLoadingQrModal = false
}
const shortenerUrl = project?.enable_custom_domain
? project.custom_domain || shortener_url
const shortenerUrl = shortener.project?.enable_custom_domain
? shortener.project.custom_domain || shortener_url
: shortener_url
</script>
@ -161,7 +156,7 @@
<div class="flex items-center justify-between">
<div class="flex gap-2">
<Button
href={`/dashboard/links/${shortener.id}`}
href={`/dashboard/links/${shortener.code}`}
class="bg-secondary flex h-8 items-center justify-center gap-1 rounded text-sm">
<BarChart size={20} />
<div>
@ -206,8 +201,8 @@
{/if}
</div>
<div class="flex gap-4">
{#if project}
<Badge variant="secondary">{project.name}</Badge>
{#if shortener.projectName}
<Badge variant="secondary">{shortener.projectName}</Badge>
{/if}
<Badge variant="outline" class="flex gap-2">
{#if shortener.active}
@ -238,11 +233,7 @@
</Dialog.Description>
</Dialog.Header>
<ScrollArea class="max-h-[calc(100vh-200px)]">
{#if project}
<ProjectEditLinkPage data={editData} shallowRouting />
{:else}
<EditLinkPage data={editData} shallowRouting />
{/if}
</ScrollArea>
</Dialog.Content>
</Dialog.Root>

@ -49,7 +49,7 @@ export const shortener = pgTable('shortener', {
ios_link: varchar('ios_link', { length: 255 }),
android: boolean('android').notNull().default(false),
android_link: varchar('android_link', { length: 255 }),
code: varchar('code', { length: 255 }).notNull(),
code: varchar('code', { length: 255 }).notNull().unique(),
createdAt: timestamp('created_at', { mode: 'string' })
.defaultNow()
.notNull(),

@ -17,7 +17,6 @@ import type { Actions } from './$types'
import { nanoid } from 'nanoid'
import { isAlphanumeric } from '$lib/utils'
import { generateId } from 'lucia'
import type { Project } from '$lib/db/types'
export const load = (async (event) => {
const user = event.locals.user
@ -144,19 +143,6 @@ export const actions: Actions = {
})
}
const user = event.locals.user
let project: Project | undefined = undefined
const selected_project = form.data.project
if (selected_project) {
project = await db.query.project.findFirst({
where: (project, { eq, and }) =>
and(
eq(project.userId, user.id),
eq(project.uuid, selected_project),
),
})
}
if (form.data.custom_code_enable) {
if (!form.data.custom_code) {
return setError(
@ -173,60 +159,27 @@ export const actions: Actions = {
)
}
const customCodeExist = await db.query.shortener.findMany({
where: (shortener, { eq, and, ne }) =>
and(eq(shortener.code, form.data.custom_code)),
with: {
project: true,
},
const customCodeExist = await db.query.shortener.findFirst({
where: (shortener, { eq }) =>
eq(shortener.code, form.data.custom_code),
})
for (const shortener of customCodeExist) {
if (!shortener.project) {
if (
!project ||
(project && !project.enable_custom_domain)
) {
return setError(
form,
'custom_code',
'Duplicated Custom Code',
)
if (customCodeExist) {
return setError(form, 'custom_code', 'Duplicated Custom Code')
}
} else {
if (project) {
if (
!shortener.project.enable_custom_domain &&
!project.enable_custom_domain
) {
return setError(
form,
'custom_code',
'Duplicated Custom Code',
)
}
if (
shortener.project.custom_domain ===
project.custom_domain
) {
return setError(
form,
'custom_code',
'Duplicated Custom Code',
)
}
} else {
if (!shortener.project.enable_custom_domain) {
return setError(
form,
'custom_code',
'Duplicated Custom Code',
)
}
}
}
}
const user = event.locals.user
let project = undefined
const selected_project = form.data.project
if (selected_project) {
project = await db.query.project.findFirst({
where: (project, { eq, and }) =>
and(
eq(project.userId, user.id),
eq(project.uuid, selected_project),
),
})
}
const code = form.data.custom_code_enable

@ -25,7 +25,9 @@
import ShortenerCard from '$lib/components/ShortenerCard.svelte'
import CustomPaginationBar from '$lib/components/Custom-Pagination-Bar.svelte'
import Form from './(components)/form.svelte'
import EditProjectLinkPage from '../projects/[id]/links/[linkid]/edit/+page.svelte'
import ProjectLinkQRPage from '../projects/[id]/links/[linkid]/qr/+page.svelte'
import EditLinkQRPage from './[id]/edit/+page.svelte'
import LinkQRPage from './[id]/qr/+page.svelte'
export let data: PageData
@ -360,7 +362,6 @@
{#each shorteners as shortener}
<ShortenerCard
{shortener}
project={shortener.project}
shortener_url={data.shortener_url} />
{/each}
</div>
@ -398,6 +399,48 @@
path={'/dashboard/links'} />
{/await}
<Dialog.Root
bind:open={editProjectLinkOpen}
onOpenChange={(open) => {
if (!open) {
history.back()
}
}}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Edit Shortener</Dialog.Title>
<Dialog.Description>
Edit Shortener Here. Click Save To Save.
</Dialog.Description>
</Dialog.Header>
<ScrollArea class="max-h-[calc(100vh-200px)]">
<EditProjectLinkPage
data={$page.state.editProjectLink}
shallowRouting />
</ScrollArea>
</Dialog.Content>
</Dialog.Root>
<Dialog.Root
bind:open={editLinkOpen}
onOpenChange={(open) => {
if (!open) {
history.back()
}
}}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Edit Shortener</Dialog.Title>
<Dialog.Description>
Edit Shortener Here. Click Save To Save.
</Dialog.Description>
</Dialog.Header>
<ScrollArea class="max-h-[calc(100vh-200px)]">
<EditLinkQRPage data={$page.state.editLink} shallowRouting />
</ScrollArea>
</Dialog.Content>
</Dialog.Root>
<Dialog.Root
bind:open={linkQROpen}
onOpenChange={(open) => {

@ -11,7 +11,7 @@ export const load = (async (event) => {
const shortener = await db.query.shortener.findFirst({
where: (shortener, { eq, and }) =>
and(
eq(shortener.id, shortenerId),
eq(shortener.code, shortenerId),
eq(shortener.userId, user.id),
),
with: {

@ -1,5 +1,6 @@
<script lang="ts">
import * as Form from '$lib/components/ui/form'
import * as Select from '$lib/components/ui/select'
import { Input } from '$lib/components/ui/input'
import { Switch } from '$lib/components/ui/switch'
import { formSchema, type FormSchema } from '../schema'
@ -13,9 +14,12 @@
import { Loader2, LoaderCircle } from 'lucide-svelte'
import { Checkbox } from '$lib/components/ui/checkbox'
import { onMount } from 'svelte'
import type { Project } from '$lib/db/types'
export let data: SuperValidated<Infer<FormSchema>>
export let projects: Project[]
export let code: string
export let shortenerCategory: any = undefined
const form = superForm(data, {
validators: zodClient(formSchema),
@ -96,6 +100,38 @@
<Form.Description>Shortener link</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="project" class="flex flex-col gap-2">
<Form.Control let:attrs>
<Form.Label>Project</Form.Label>
<Select.Root
bind:selected={shortenerCategory}
onSelectedChange={(v) => {
v && ($formData.project = v.value)
}}
multiple={false}>
<Select.Trigger {...attrs} class="col-span-3">
<Select.Value placeholder="Select a Project" />
</Select.Trigger>
<Select.Content>
<Select.Item value={''}>None</Select.Item>
<Select.Separator />
<Select.Group>
{#each projects as project}
<Select.Item value={project.uuid}>
{project.name}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
<input
hidden
bind:value={$formData.project}
name={attrs.name} />
</Form.Control>
<Form.Description>Shortener Project</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field
{form}
name="custom_code_enable"

@ -15,7 +15,6 @@ export const load = (async (event) => {
const shortener = await db.query.shortener.findFirst({
columns: {
id: true,
code: true,
projectId: true,
ios: true,
@ -33,13 +32,30 @@ export const load = (async (event) => {
redirect(300, `/dashboard/links`)
}
const projects = await db.query.project.findMany({
where: (project, { eq }) => eq(project.userId, user.id),
})
let selectedCategory = undefined
if (shortener.projectId) {
const project = projects.find(
(project) => project.id === shortener.projectId,
)
if (project) {
selectedCategory = { value: project.uuid, label: project.name }
}
}
return {
projects,
shortener,
selectedCategory,
form: await superValidate(
{
...shortener,
custom_code_enable: true,
custom_code: shortener.code,
project: selectedCategory?.value || undefined,
},
zod(formSchema),
{ errors: false },
@ -56,8 +72,6 @@ export const actions: Actions = {
})
}
const user = event.locals.user
if (form.data.custom_code_enable) {
if (!form.data.custom_code) {
return setError(
@ -74,32 +88,38 @@ export const actions: Actions = {
)
}
const customCodeExist = await db.query.shortener.findMany({
const customCodeExist = await db.query.shortener.findFirst({
where: (shortener, { eq, and, ne }) =>
and(
eq(shortener.code, form.data.custom_code),
ne(shortener.id, event.params.id),
ne(shortener.code, event.params.id),
),
with: {
project: true,
},
})
for (const shortener of customCodeExist) {
if (!shortener.project) {
return setError(
form,
'custom_code',
'Duplicated Custom Code',
)
if (customCodeExist) {
return setError(form, 'custom_code', 'Duplicated Custom Code')
}
}
const user = event.locals.user
let project = undefined
const selected_project = form.data.project
if (selected_project) {
project = await db.query.project.findFirst({
where: (project, { eq, and }) =>
and(
eq(project.userId, user.id),
eq(project.uuid, selected_project),
),
})
}
await db
.update(shortener)
.set({
link: form.data.link,
projectId: project ? project.id : null,
userId: user.id,
code: form.data.custom_code_enable
? form.data.custom_code
@ -109,7 +129,7 @@ export const actions: Actions = {
android: form.data.android,
android_link: form.data.android_link,
})
.where(eq(shortener.id, event.params.id))
.where(eq(shortener.code, event.params.id))
return { form }
},

@ -9,10 +9,18 @@
{#if !shallowRouting}
<ScrollArea>
<div class="max-w-2xl px-10 py-4">
<Form data={data.form} code={data.shortener.id} />
<div class="py-4 px-10 max-w-2xl">
<Form
data={data.form}
code={data.shortener.code}
projects={data.projects}
shortenerCategory={data.selectedCategory} />
</div>
</ScrollArea>
{:else}
<Form data={data.form} code={data.shortener.id} />
<Form
data={data.form}
code={data.shortener.code}
projects={data.projects}
shortenerCategory={data.selectedCategory} />
{/if}

@ -2,6 +2,7 @@ import { z } from 'zod'
export const formSchema = z.object({
link: z.string().url(),
project: z.string().optional(),
active: z.boolean(),
ios: z.boolean(),
ios_link: z

@ -0,0 +1,47 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button'
import * as AlertDialog from '$lib/components/ui/alert-dialog'
import { Loader2 } from 'lucide-svelte'
import { invalidateAll } from '$app/navigation'
import { toast } from 'svelte-sonner'
export let deleteDialogOpen = false
export let deleteShortenerCode = ''
let isDeleteLoading = false
const deleteShortener = async () => {
isDeleteLoading = true
await fetch(`/api/shortener/${deleteShortenerCode}`, {
method: 'delete',
})
isDeleteLoading = false
toast.success('Shortener deleted successfully')
deleteDialogOpen = false
await invalidateAll()
}
</script>
<AlertDialog.Root bind:open={deleteDialogOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>Are you absolutely sure?</AlertDialog.Title>
<AlertDialog.Description>
You are about to delete a shortener.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel disabled={isDeleteLoading}
>Cancel</AlertDialog.Cancel>
<Button
variant="destructive"
on:click={deleteShortener}
class="flex gap-2"
disabled={isDeleteLoading}>
{#if isDeleteLoading}
<Loader2 class="animate-spin" />
{/if}
Delete
</Button>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>

@ -0,0 +1,225 @@
<script lang="ts">
import { Button, buttonVariants } from '$lib/components/ui/button'
import * as Card from '$lib/components/ui/card'
import * as Dialog from '$lib/components/ui/dialog'
import * as DropdownMenu from '$lib/components/ui/dropdown-menu'
import * as Tooltip from '$lib/components/ui/tooltip'
import * as Avatar from '$lib/components/ui/avatar'
import { Badge } from '$lib/components/ui/badge'
import { ScrollArea } from '$lib/components/ui/scroll-area'
import type { Shortener, Project } from '$lib/db/types'
import {
BarChart,
EditIcon,
ExternalLink,
Loader2Icon,
MoreVertical,
QrCode,
TrashIcon,
} from 'lucide-svelte'
import DeleteShortenerDialog from './DeleteShortenerDialog.svelte'
import EditProjectLinkPage from '../links/[linkid]/edit/+page.svelte'
import { goto, preloadData, pushState } from '$app/navigation'
import { cn } from '$lib/utils'
import type { page } from '$app/stores'
export let shortener: Shortener & {
visitorCount: number
}
export let shortener_url: string
export let selected_project: Project
let deleteDialogOpen = false
let deleteShortenerCode = ''
const openDeleteDialog = (code: string) => {
deleteShortenerCode = code
deleteDialogOpen = true
}
let editProjectLinkOpen = false
let editData: typeof $page.state.editProjectLink
const showEditModal = async (projectUuid: string, code: string) => {
const href = `/dashboard/projects/${projectUuid}/links/${code}/edit`
const result = await preloadData(href)
if (result.type === 'loaded' && result.status === 200) {
editData = result.data as typeof $page.state.editProjectLink
editProjectLinkOpen = true
} else {
// something bad happened! try navigating
goto(href)
}
}
let isLoadingQrModal = false
const showQRModal = async (e: MouseEvent) => {
isLoadingQrModal = true
const { href } = e.currentTarget as HTMLAnchorElement
if (innerWidth < 640) goto(href)
const result = await preloadData(href)
if (result.type === 'loaded' && result.status === 200) {
pushState(href, { projectLinkQR: result.data })
} else {
goto(href)
}
isLoadingQrModal = false
}
const shortenerUrl = selected_project.enable_custom_domain
? selected_project.custom_domain || shortener_url
: shortener_url
</script>
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center justify-between gap-4">
<Avatar.Root class="overflow-visible">
<Avatar.Image
src={'https://www.google.com/s2/favicons?sz=128&domain_url=' +
shortener.link}
alt="favicon" />
<Avatar.Fallback class="bg-opacity-0">
<img src="/favicon.png" alt="favicon" />
</Avatar.Fallback>
</Avatar.Root>
<div class="flex flex-grow flex-col items-start gap-2">
<Tooltip.Root>
<Tooltip.Trigger>
<div
class="max-w-[250px] overflow-x-clip overflow-ellipsis whitespace-nowrap">
{shortener.link}
</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{shortener.link}</p>
</Tooltip.Content>
</Tooltip.Root>
<div
class="text-muted-foreground flex items-center gap-2 text-sm">
<a
href={'https://' + shortenerUrl + '/' + shortener.code}
target="_blank"
class="hover:underline">
{shortenerUrl + '/' + shortener.code}
</a>
<ExternalLink size={16} />
</div>
</div>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<MoreVertical />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<a
href={`/dashboard/projects/${selected_project.uuid}/links/${shortener.code}/edit`}
on:click|preventDefault={() =>
showEditModal(
selected_project.uuid || '',
shortener.code,
)}>
<DropdownMenu.Item class="flex items-center gap-2">
<EditIcon size={16} />Edit
</DropdownMenu.Item>
</a>
<DropdownMenu.Item
on:click={() => openDeleteDialog(shortener.id)}
class="text-destructive data-[highlighted]:bg-destructive flex items-center gap-2">
<TrashIcon size={16} />
Delete
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Card.Title>
</Card.Header>
<Card.Content>
<div class="flex items-center justify-between">
<div class="flex gap-2">
<Button
href={`/dashboard/links/${shortener.code}`}
class="bg-secondary flex h-8 items-center justify-center gap-1 rounded text-sm">
<BarChart size={20} />
<div>
{shortener.visitorCount} visits
</div>
</Button>
<a
class={cn(
buttonVariants({ variant: 'default' }),
'bg-secondary flex h-8 items-center justify-center gap-1 rounded text-sm',
)}
href={`/dashboard/projects/${selected_project.uuid}/links/${shortener.code}/qr`}
on:click|preventDefault={showQRModal}>
{#if isLoadingQrModal}
<Loader2Icon size={20} class="animate-spin" />
{:else}
<QrCode size={20} />
{/if}
</a>
{#if shortener.ios}
<Tooltip.Root>
<Tooltip.Trigger>
<Badge variant="outline" class="flex gap-2">iOS</Badge>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{shortener.ios_link}</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
{#if shortener.android}
<Tooltip.Root>
<Tooltip.Trigger>
<Badge variant="outline" class="flex gap-2">
Android
</Badge>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{shortener.android_link}</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
</div>
<div class="flex gap-4">
{#if shortener.projectName}
<Badge variant="secondary">{shortener.projectName}</Badge>
{/if}
<Badge variant="outline" class="flex gap-2">
{#if shortener.active}
<span
class="relative inline-flex h-2 w-2 rounded-full bg-green-400">
</span>
Active
{:else}
<span
class="relative inline-flex h-2 w-2 rounded-full bg-gray-600">
</span>
Inactive
{/if}
</Badge>
</div>
</div>
</Card.Content>
</Card.Root>
<DeleteShortenerDialog bind:deleteDialogOpen {deleteShortenerCode} />
<Dialog.Root bind:open={editProjectLinkOpen}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Edit Shortener</Dialog.Title>
<Dialog.Description>
Edit Shortener Here. Click Save To Save.
</Dialog.Description>
</Dialog.Header>
<ScrollArea class="max-h-[calc(100vh-200px)]">
<EditProjectLinkPage data={editData} shallowRouting />
</ScrollArea>
</Dialog.Content>
</Dialog.Root>

@ -113,19 +113,6 @@ export const actions: Actions = {
})
}
const { id } = event.params
const user = event.locals.user
const project = await db.query.project.findFirst({
where: (project, { eq, and }) =>
and(eq(project.userId, user.id), eq(project.uuid, id)),
})
if (!project) {
return fail(400, {
form,
})
}
if (form.data.custom_code_enable) {
if (!form.data.custom_code) {
return setError(
@ -142,35 +129,27 @@ export const actions: Actions = {
)
}
const customCodeExist = await db.query.shortener.findMany({
where: (shortener, { eq, and, ne }) =>
and(eq(shortener.code, form.data.custom_code)),
with: {
project: true,
},
const customCodeExist = await db.query.shortener.findFirst({
where: (shortener, { eq }) =>
eq(shortener.code, form.data.custom_code),
})
for (const shortener of customCodeExist) {
if (!shortener.project && !project.enable_custom_domain) {
return setError(
form,
'custom_code',
'Duplicated Custom Code',
)
if (customCodeExist) {
return setError(form, 'custom_code', 'Duplicated Custom Code')
}
}
if (!shortener.project) continue
const { id } = event.params
const user = event.locals.user
const project = await db.query.project.findFirst({
where: (project, { eq, and }) =>
and(eq(project.userId, user.id), eq(project.uuid, id)),
})
if (
shortener.project.custom_domain === project.custom_domain
) {
return setError(
if (!project) {
return fail(400, {
form,
'custom_code',
'Duplicated Custom Code',
)
}
}
})
}
const code = form.data.custom_code_enable

@ -3,7 +3,7 @@
import { browser } from '$app/environment'
import { page } from '$app/stores'
import ShortenerCard from '$lib/components/ShortenerCard.svelte'
import ShortenerCard from './(components)/ShortenerCard.svelte'
import CustomPaginationBar from '$lib/components/Custom-Pagination-Bar.svelte'
import { Button } from '$lib/components/ui/button'
import { Skeleton } from '$lib/components/ui/skeleton'
@ -206,7 +206,7 @@
{#each shorteners as shortener}
<ShortenerCard
{shortener}
project={data.project}
selected_project={data.selectedProject}
shortener_url={data.shortener_url} />
{/each}
</div>

@ -15,7 +15,6 @@ export const load = (async (event) => {
const shortener = await db.query.shortener.findFirst({
columns: {
id: true,
code: true,
ios: true,
ios_link: true,
@ -26,7 +25,7 @@ export const load = (async (event) => {
},
where: (shortener, { eq, and }) =>
and(
eq(shortener.id, linkid),
eq(shortener.code, linkid),
eq(shortener.projectId, selectedProject.id),
),
})
@ -57,19 +56,6 @@ export const actions: Actions = {
})
}
const { id } = event.params
const user = event.locals.user
const project = await db.query.project.findFirst({
where: (project, { eq, and }) =>
and(eq(project.userId, user.id), eq(project.uuid, id)),
})
if (!project) {
return fail(400, {
form,
})
}
if (form.data.custom_code_enable) {
if (!form.data.custom_code) {
return setError(
@ -86,38 +72,30 @@ export const actions: Actions = {
)
}
const customCodeExist = await db.query.shortener.findMany({
const customCodeExist = await db.query.shortener.findFirst({
where: (shortener, { eq, and, ne }) =>
and(
eq(shortener.code, form.data.custom_code),
ne(shortener.id, event.params.linkid),
ne(shortener.code, event.params.linkid),
),
with: {
project: true,
},
})
for (const shortener of customCodeExist) {
if (!shortener.project && !project.enable_custom_domain) {
return setError(
form,
'custom_code',
'Duplicated Custom Code',
)
if (customCodeExist) {
return setError(form, 'custom_code', 'Duplicated Custom Code')
}
}
if (!shortener.project) continue
const { id } = event.params
const user = event.locals.user
const project = await db.query.project.findFirst({
where: (project, { eq, and }) =>
and(eq(project.userId, user.id), eq(project.uuid, id)),
})
if (
shortener.project.custom_domain === project.custom_domain
) {
return setError(
if (!project) {
return fail(400, {
form,
'custom_code',
'Duplicated Custom Code',
)
}
}
})
}
await db
@ -134,7 +112,7 @@ export const actions: Actions = {
android: form.data.android,
android_link: form.data.android_link,
})
.where(eq(shortener.id, event.params.linkid))
.where(eq(shortener.code, event.params.linkid))
return { form }
},

@ -9,16 +9,16 @@
{#if !shallowRouting}
<ScrollArea>
<div class="max-w-2xl px-10 py-4">
<div class="py-4 px-10 max-w-2xl">
<Form
data={data.form}
uuid={data.project.uuid}
code={data.shortener.id} />
code={data.shortener.code} />
</div>
</ScrollArea>
{:else}
<Form
data={data.form}
uuid={data.project.uuid}
code={data.shortener.id} />
code={data.shortener.code} />
{/if}

@ -251,7 +251,6 @@ export const actions: Actions = {
return setMessage(form, 'Custom Domain Enabled')
},
disable_custom_domain: async (event) => {
return { message: 'Disabling custom domain is unavailable' }
const userId = event.locals.user.id
const existingProject = await db.query.project.findFirst({

@ -221,7 +221,7 @@
</Dialog.Content>
</Dialog.Root>
{:else}
<!-- <AlertDialog.Root>
<AlertDialog.Root>
<AlertDialog.Trigger asChild let:builder>
<Button builders={[builder]}>
Disable Custom Domain
@ -252,7 +252,7 @@
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root> -->
</AlertDialog.Root>
{/if}
</div>
</div>

@ -60,7 +60,6 @@ const getShortener = `-- name: GetShortener :one
SELECT id, link, code, created_at, user_id, project_id, active, ios, ios_link, android, android_link
FROM shortener
WHERE code = $1
AND shortener.project_id IS NULL
LIMIT 1
`

@ -5,7 +5,6 @@ FROM shortener;
SELECT *
FROM shortener
WHERE code = $1
AND shortener.project_id IS NULL
LIMIT 1;
-- name: GetShortenerWithDomain :one
SELECT shortener.*,

Loading…
Cancel
Save