Compare commits

...

2 Commits

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

@ -0,0 +1,457 @@
{
"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,6 +190,13 @@
"when": 1725897203517, "when": 1725897203517,
"tag": "0026_natural_tattoo", "tag": "0026_natural_tattoo",
"breakpoints": true "breakpoints": true
},
{
"idx": 27,
"version": "7",
"when": 1726769139213,
"tag": "0027_funny_wasp",
"breakpoints": true
} }
] ]
} }

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

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

@ -17,6 +17,7 @@ import type { Actions } from './$types'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { isAlphanumeric } from '$lib/utils' import { isAlphanumeric } from '$lib/utils'
import { generateId } from 'lucia' import { generateId } from 'lucia'
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
@ -143,6 +144,19 @@ 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_enable) {
if (!form.data.custom_code) { if (!form.data.custom_code) {
return setError( return setError(
@ -159,29 +173,62 @@ export const actions: Actions = {
) )
} }
const customCodeExist = await db.query.shortener.findFirst({ const customCodeExist = await db.query.shortener.findMany({
where: (shortener, { eq }) => where: (shortener, { eq, and, ne }) =>
eq(shortener.code, form.data.custom_code), and(eq(shortener.code, form.data.custom_code)),
with: {
project: true,
},
}) })
if (customCodeExist) { for (const shortener of customCodeExist) {
return setError(form, 'custom_code', 'Duplicated Custom Code') if (!shortener.project) {
if (
!project ||
(project && !project.enable_custom_domain)
) {
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 const code = form.data.custom_code_enable
? form.data.custom_code ? form.data.custom_code
: nanoid(8) : nanoid(8)

@ -25,9 +25,7 @@
import ShortenerCard from '$lib/components/ShortenerCard.svelte' import ShortenerCard from '$lib/components/ShortenerCard.svelte'
import CustomPaginationBar from '$lib/components/Custom-Pagination-Bar.svelte' import CustomPaginationBar from '$lib/components/Custom-Pagination-Bar.svelte'
import Form from './(components)/form.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 ProjectLinkQRPage from '../projects/[id]/links/[linkid]/qr/+page.svelte'
import EditLinkQRPage from './[id]/edit/+page.svelte'
import LinkQRPage from './[id]/qr/+page.svelte' import LinkQRPage from './[id]/qr/+page.svelte'
export let data: PageData export let data: PageData
@ -362,6 +360,7 @@
{#each shorteners as shortener} {#each shorteners as shortener}
<ShortenerCard <ShortenerCard
{shortener} {shortener}
project={shortener.project}
shortener_url={data.shortener_url} /> shortener_url={data.shortener_url} />
{/each} {/each}
</div> </div>
@ -399,48 +398,6 @@
path={'/dashboard/links'} /> path={'/dashboard/links'} />
{/await} {/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 <Dialog.Root
bind:open={linkQROpen} bind:open={linkQROpen}
onOpenChange={(open) => { onOpenChange={(open) => {

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

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import * as Form from '$lib/components/ui/form' import * as Form from '$lib/components/ui/form'
import * as Select from '$lib/components/ui/select'
import { Input } from '$lib/components/ui/input' import { Input } from '$lib/components/ui/input'
import { Switch } from '$lib/components/ui/switch' import { Switch } from '$lib/components/ui/switch'
import { formSchema, type FormSchema } from '../schema' import { formSchema, type FormSchema } from '../schema'
@ -14,12 +13,9 @@
import { Loader2, LoaderCircle } from 'lucide-svelte' import { Loader2, LoaderCircle } from 'lucide-svelte'
import { Checkbox } from '$lib/components/ui/checkbox' import { Checkbox } from '$lib/components/ui/checkbox'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import type { Project } from '$lib/db/types'
export let data: SuperValidated<Infer<FormSchema>> export let data: SuperValidated<Infer<FormSchema>>
export let projects: Project[]
export let code: string export let code: string
export let shortenerCategory: any = undefined
const form = superForm(data, { const form = superForm(data, {
validators: zodClient(formSchema), validators: zodClient(formSchema),
@ -100,38 +96,6 @@
<Form.Description>Shortener link</Form.Description> <Form.Description>Shortener link</Form.Description>
<Form.FieldErrors /> <Form.FieldErrors />
</Form.Field> </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.Field
{form} {form}
name="custom_code_enable" name="custom_code_enable"

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

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

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

@ -1,47 +0,0 @@
<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>

@ -1,225 +0,0 @@
<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,6 +113,19 @@ 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_enable) {
if (!form.data.custom_code) { if (!form.data.custom_code) {
return setError( return setError(
@ -129,29 +142,37 @@ export const actions: Actions = {
) )
} }
const customCodeExist = await db.query.shortener.findFirst({ const customCodeExist = await db.query.shortener.findMany({
where: (shortener, { eq }) => where: (shortener, { eq, and, ne }) =>
eq(shortener.code, form.data.custom_code), and(eq(shortener.code, form.data.custom_code)),
with: {
project: true,
},
}) })
if (customCodeExist) { for (const shortener of customCodeExist) {
return setError(form, 'custom_code', 'Duplicated Custom Code') if (!shortener.project && !project.enable_custom_domain) {
return setError(
form,
'custom_code',
'Duplicated Custom Code',
)
}
if (!shortener.project) continue
if (
shortener.project.custom_domain === project.custom_domain
) {
return setError(
form,
'custom_code',
'Duplicated Custom Code',
)
}
} }
} }
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,
})
}
const code = form.data.custom_code_enable const code = form.data.custom_code_enable
? form.data.custom_code ? form.data.custom_code
: nanoid(8) : nanoid(8)

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

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

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

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

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

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

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

Loading…
Cancel
Save