mirror of https://github.com/TZGyn/shortener
update links page to use correct qr dialog, create and edit form
parent
37fb2871dc
commit
9c358a340b
@ -0,0 +1,217 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Form from '$lib/components/ui/form'
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog'
|
||||||
|
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'
|
||||||
|
import {
|
||||||
|
type SuperValidated,
|
||||||
|
type Infer,
|
||||||
|
superForm,
|
||||||
|
} from 'sveltekit-superforms'
|
||||||
|
import { zodClient } from 'sveltekit-superforms/adapters'
|
||||||
|
import { toast } from 'svelte-sonner'
|
||||||
|
import { Loader2, LoaderCircle, PlusCircle } from 'lucide-svelte'
|
||||||
|
import { buttonVariants } from '$lib/components/ui/button'
|
||||||
|
import { Checkbox } from '$lib/components/ui/checkbox'
|
||||||
|
import { ScrollArea } from '$lib/components/ui/scroll-area'
|
||||||
|
import type { Project } from '$lib/db/types'
|
||||||
|
|
||||||
|
export let data: SuperValidated<Infer<FormSchema>>
|
||||||
|
export let projects: Project[]
|
||||||
|
export let dialogOpen: boolean
|
||||||
|
let shortenerCategory: any = undefined
|
||||||
|
|
||||||
|
const form = superForm(data, {
|
||||||
|
validators: zodClient(formSchema),
|
||||||
|
invalidateAll: 'force',
|
||||||
|
resetForm: true,
|
||||||
|
onResult: ({ result }) => {
|
||||||
|
if (result.status === 200) {
|
||||||
|
dialogOpen = false
|
||||||
|
toast.success('Shortener added to project')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: ({ result }) => {
|
||||||
|
toast.error('Error adding shortener')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { form: formData, enhance, submitting } = form
|
||||||
|
|
||||||
|
let inputTimer: any
|
||||||
|
let previewData: any
|
||||||
|
let isPreviewLoading: boolean = false
|
||||||
|
|
||||||
|
const getMetadata = async () => {
|
||||||
|
isPreviewLoading = true
|
||||||
|
clearTimeout(inputTimer)
|
||||||
|
const link =
|
||||||
|
$formData.link.startsWith('https://') ||
|
||||||
|
$formData.link.startsWith('http://')
|
||||||
|
? $formData.link
|
||||||
|
: 'https://' + $formData.link
|
||||||
|
inputTimer = setTimeout(async () => {
|
||||||
|
const response = await fetch(`/api/url/metadata?url=${link}`)
|
||||||
|
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 Shortener Here. Click Add To Save.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<ScrollArea class="max-h-[calc(100vh-200px)]">
|
||||||
|
<div class="grid grid-cols-4 gap-4 items-center pb-4">
|
||||||
|
<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>
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
use:enhance
|
||||||
|
class="flex flex-col gap-6"
|
||||||
|
action="?/create">
|
||||||
|
<Form.Field {form} name="link" class="flex flex-col gap-2">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Form.Label>Link</Form.Label>
|
||||||
|
<Input
|
||||||
|
{...attrs}
|
||||||
|
bind:value={$formData.link}
|
||||||
|
placeholder="https://example.com"
|
||||||
|
on:input={getMetadata} />
|
||||||
|
</Form.Control>
|
||||||
|
<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="ios"
|
||||||
|
class="flex gap-2 items-center space-y-0">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Form.Label>iOS Link</Form.Label>
|
||||||
|
<Switch {...attrs} bind:checked={$formData.ios} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Field>
|
||||||
|
{#if $formData.ios}
|
||||||
|
<Form.Field
|
||||||
|
{form}
|
||||||
|
name="ios_link"
|
||||||
|
class="flex flex-col gap-2">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Input
|
||||||
|
{...attrs}
|
||||||
|
bind:value={$formData.ios_link}
|
||||||
|
placeholder="https://example.com" />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Description
|
||||||
|
>Shortener link for iOS</Form.Description>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
{/if}
|
||||||
|
<Form.Field
|
||||||
|
{form}
|
||||||
|
name="android"
|
||||||
|
class="flex gap-2 items-center space-y-0">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Form.Label>Android Link</Form.Label>
|
||||||
|
<Switch {...attrs} bind:checked={$formData.android} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Field>
|
||||||
|
{#if $formData.android}
|
||||||
|
<Form.Field
|
||||||
|
{form}
|
||||||
|
name="android_link"
|
||||||
|
class="flex flex-col gap-2">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Input
|
||||||
|
{...attrs}
|
||||||
|
bind:value={$formData.android_link}
|
||||||
|
placeholder="https://example.com" />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Description
|
||||||
|
>Shortener link for Android</Form.Description>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
{/if}
|
||||||
|
<Form.Field
|
||||||
|
{form}
|
||||||
|
name="active"
|
||||||
|
class="flex gap-2 items-center space-y-0">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Checkbox {...attrs} bind:checked={$formData.active} />
|
||||||
|
<Form.Label>Active</Form.Label>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Button class="w-fit">
|
||||||
|
{#if $submitting}
|
||||||
|
<LoaderCircle class="animate-spin" />
|
||||||
|
{/if}
|
||||||
|
Add
|
||||||
|
</Form.Button>
|
||||||
|
</form>
|
||||||
|
</ScrollArea>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
@ -0,0 +1,191 @@
|
|||||||
|
<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'
|
||||||
|
import {
|
||||||
|
type SuperValidated,
|
||||||
|
type Infer,
|
||||||
|
superForm,
|
||||||
|
} from 'sveltekit-superforms'
|
||||||
|
import { zodClient } from 'sveltekit-superforms/adapters'
|
||||||
|
import { toast } from 'svelte-sonner'
|
||||||
|
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 shortenerCategory: any = undefined
|
||||||
|
|
||||||
|
const form = superForm(data, {
|
||||||
|
validators: zodClient(formSchema),
|
||||||
|
invalidateAll: 'force',
|
||||||
|
resetForm: true,
|
||||||
|
onResult: ({ result }) => {
|
||||||
|
if (result.status === 200) {
|
||||||
|
toast.success('Project shortener updated')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: ({ result }) => {
|
||||||
|
toast.error('Error updating shortener')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { form: formData, enhance, submitting } = form
|
||||||
|
|
||||||
|
let inputTimer: any
|
||||||
|
let previewData: any
|
||||||
|
let isPreviewLoading: boolean = false
|
||||||
|
|
||||||
|
const getMetadata = async () => {
|
||||||
|
isPreviewLoading = true
|
||||||
|
clearTimeout(inputTimer)
|
||||||
|
const link =
|
||||||
|
$formData.link.startsWith('https://') ||
|
||||||
|
$formData.link.startsWith('http://')
|
||||||
|
? $formData.link
|
||||||
|
: 'https://' + $formData.link
|
||||||
|
inputTimer = setTimeout(async () => {
|
||||||
|
const response = await fetch(`/api/url/metadata?url=${link}`)
|
||||||
|
previewData = await response.json()
|
||||||
|
isPreviewLoading = false
|
||||||
|
console.log(previewData)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
getMetadata()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-4 gap-4 items-center pb-4">
|
||||||
|
<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>
|
||||||
|
<form method="POST" use:enhance class="flex flex-col gap-6">
|
||||||
|
<Form.Field {form} name="link" class="flex flex-col gap-2">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Form.Label>Link</Form.Label>
|
||||||
|
<Input
|
||||||
|
{...attrs}
|
||||||
|
bind:value={$formData.link}
|
||||||
|
placeholder="https://example.com"
|
||||||
|
on:input={getMetadata} />
|
||||||
|
</Form.Control>
|
||||||
|
<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="ios"
|
||||||
|
class="flex gap-2 items-center space-y-0">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Form.Label>iOS Link</Form.Label>
|
||||||
|
<Switch {...attrs} bind:checked={$formData.ios} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Field>
|
||||||
|
{#if $formData.ios}
|
||||||
|
<Form.Field {form} name="ios_link" class="flex flex-col gap-2">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Input
|
||||||
|
{...attrs}
|
||||||
|
bind:value={$formData.ios_link}
|
||||||
|
placeholder="https://example.com" />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Description>Shortener link for iOS</Form.Description>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
{/if}
|
||||||
|
<Form.Field
|
||||||
|
{form}
|
||||||
|
name="android"
|
||||||
|
class="flex gap-2 items-center space-y-0">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Form.Label>Android Link</Form.Label>
|
||||||
|
<Switch {...attrs} bind:checked={$formData.android} />
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Field>
|
||||||
|
{#if $formData.android}
|
||||||
|
<Form.Field
|
||||||
|
{form}
|
||||||
|
name="android_link"
|
||||||
|
class="flex flex-col gap-2">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Input
|
||||||
|
{...attrs}
|
||||||
|
bind:value={$formData.android_link}
|
||||||
|
placeholder="https://example.com" />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Description>Shortener link for Android</Form.Description>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
{/if}
|
||||||
|
<Form.Field
|
||||||
|
{form}
|
||||||
|
name="active"
|
||||||
|
class="flex gap-2 items-center space-y-0">
|
||||||
|
<Form.Control let:attrs>
|
||||||
|
<Checkbox {...attrs} bind:checked={$formData.active} />
|
||||||
|
<Form.Label>Active</Form.Label>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Button class="w-fit">
|
||||||
|
{#if $submitting}
|
||||||
|
<LoaderCircle class="animate-spin" />
|
||||||
|
{/if}
|
||||||
|
Save
|
||||||
|
</Form.Button>
|
||||||
|
</form>
|
||||||
@ -0,0 +1,139 @@
|
|||||||
|
import type { PageServerLoad } from './$types'
|
||||||
|
import { fail, setError, superValidate } from 'sveltekit-superforms'
|
||||||
|
import { zod } from 'sveltekit-superforms/adapters'
|
||||||
|
import { formSchema } from './schema'
|
||||||
|
import type { Actions } from './$types'
|
||||||
|
import { db } from '$lib/db'
|
||||||
|
import { redirect } from '@sveltejs/kit'
|
||||||
|
import { shortener } from '$lib/db/schema'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
|
||||||
|
export const load = (async (event) => {
|
||||||
|
const user = event.locals.user
|
||||||
|
const { id } = event.params
|
||||||
|
|
||||||
|
const shortener = await db.query.shortener.findFirst({
|
||||||
|
columns: {
|
||||||
|
code: true,
|
||||||
|
projectId: true,
|
||||||
|
ios: true,
|
||||||
|
ios_link: true,
|
||||||
|
android: true,
|
||||||
|
android_link: true,
|
||||||
|
link: true,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
where: (shortener, { eq, and }) =>
|
||||||
|
and(eq(shortener.code, id), eq(shortener.userId, user.id)),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!shortener) {
|
||||||
|
redirect(300, `/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,
|
||||||
|
selectedCategory,
|
||||||
|
form: await superValidate(
|
||||||
|
{
|
||||||
|
...shortener,
|
||||||
|
project: selectedCategory?.value || undefined,
|
||||||
|
},
|
||||||
|
zod(formSchema),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}) satisfies PageServerLoad
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
default: async (event) => {
|
||||||
|
const form = await superValidate(event, zod(formSchema))
|
||||||
|
if (!form.valid) {
|
||||||
|
return fail(400, {
|
||||||
|
form,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.data.link.startsWith('http://')) {
|
||||||
|
return setError(form, 'link', 'Link must be HTTPS')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
form.data.ios_link &&
|
||||||
|
form.data.ios_link.startsWith('http://')
|
||||||
|
) {
|
||||||
|
return setError(form, 'ios_link', 'Link must be HTTPS')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
form.data.android_link &&
|
||||||
|
form.data.android_link.startsWith('http://')
|
||||||
|
) {
|
||||||
|
return setError(form, 'android_link', 'Link must be HTTPS')
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let ios_link = ''
|
||||||
|
|
||||||
|
if (form.data.ios_link) {
|
||||||
|
if (form.data.ios_link.startsWith('https://')) {
|
||||||
|
ios_link = form.data.ios_link
|
||||||
|
} else {
|
||||||
|
ios_link = `https://${form.data.ios_link}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let android_link = ''
|
||||||
|
|
||||||
|
if (form.data.android_link) {
|
||||||
|
if (form.data.android_link.startsWith('https://')) {
|
||||||
|
android_link = form.data.android_link
|
||||||
|
} else {
|
||||||
|
android_link = `https://${form.data.android_link}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(shortener)
|
||||||
|
.set({
|
||||||
|
link: form.data.link.startsWith('https://')
|
||||||
|
? form.data.link
|
||||||
|
: `https://${form.data.link}`,
|
||||||
|
projectId: project ? project.id : null,
|
||||||
|
userId: user.id,
|
||||||
|
ios: form.data.ios,
|
||||||
|
ios_link: ios_link,
|
||||||
|
android: form.data.android,
|
||||||
|
android_link: android_link,
|
||||||
|
})
|
||||||
|
.where(eq(shortener.code, event.params.id))
|
||||||
|
|
||||||
|
return { form }
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ScrollArea } from '$lib/components/ui/scroll-area'
|
||||||
|
import type { PageData } from './$types'
|
||||||
|
import Form from './(components)/form.svelte'
|
||||||
|
|
||||||
|
export let data: PageData
|
||||||
|
export let shallowRouting = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !shallowRouting}
|
||||||
|
<ScrollArea>
|
||||||
|
<div class="py-4 px-10 max-w-2xl">
|
||||||
|
<Form
|
||||||
|
data={data.form}
|
||||||
|
projects={data.projects}
|
||||||
|
shortenerCategory={data.selectedCategory} />
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
{:else}
|
||||||
|
<Form
|
||||||
|
data={data.form}
|
||||||
|
projects={data.projects}
|
||||||
|
shortenerCategory={data.selectedCategory} />
|
||||||
|
{/if}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const formSchema = z.object({
|
||||||
|
link: z.string(),
|
||||||
|
project: z.string().optional(),
|
||||||
|
active: z.boolean(),
|
||||||
|
ios: z.boolean(),
|
||||||
|
ios_link: z.string().nullable(),
|
||||||
|
android: z.boolean(),
|
||||||
|
android_link: z.string().nullable(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type FormSchema = typeof formSchema
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import QRCode from 'qrcode'
|
||||||
|
import { Button } from '$lib/components/ui/button'
|
||||||
|
import { toast } from 'svelte-sonner'
|
||||||
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu'
|
||||||
|
import { Badge } from '$lib/components/ui/badge'
|
||||||
|
|
||||||
|
export let background = '#fff'
|
||||||
|
export let color = '#000'
|
||||||
|
export let value = ''
|
||||||
|
export let code = ''
|
||||||
|
|
||||||
|
let image = ''
|
||||||
|
|
||||||
|
const copyImageToClipboard = async () => {
|
||||||
|
if (!image) return
|
||||||
|
|
||||||
|
const imageData = await fetch(image)
|
||||||
|
|
||||||
|
const imageBlob = await imageData.blob()
|
||||||
|
|
||||||
|
try {
|
||||||
|
navigator.clipboard.write([
|
||||||
|
new ClipboardItem({
|
||||||
|
'image/png': imageBlob,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
toast.success('Copied Image To Clipboard')
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
'Unable to copy item to clipboard. If you are using firefox, you can change the setting dom.events.asyncclipboard.clipboarditem in about:config to true',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateQrCode() {
|
||||||
|
try {
|
||||||
|
image = await QRCode.toDataURL(value, {
|
||||||
|
errorCorrectionLevel: 'L',
|
||||||
|
margin: 1,
|
||||||
|
scale: 20,
|
||||||
|
color: {
|
||||||
|
light: background,
|
||||||
|
dark: color,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
image = await QRCode.toDataURL(value, {
|
||||||
|
errorCorrectionLevel: 'L',
|
||||||
|
margin: 1,
|
||||||
|
scale: 20,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (value) {
|
||||||
|
generateQrCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 items-center h-full">
|
||||||
|
<Badge variant="secondary">
|
||||||
|
{value}
|
||||||
|
</Badge>
|
||||||
|
<img src={image} alt={value} width={300} height={300} />
|
||||||
|
<div class="flex gap-4 w-full">
|
||||||
|
<Button class="w-full" on:click={copyImageToClipboard}
|
||||||
|
>Copy Image</Button>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild let:builder>
|
||||||
|
<Button builders={[builder]} class="w-full">QR Link</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
href={`/api/shortener/${code}/qr`}
|
||||||
|
target="_blank">Standard</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
href={`/api/shortener/${code}/qr?color=true`}
|
||||||
|
target="_blank">With Color</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { db } from '$lib/db'
|
||||||
|
import { redirect } from '@sveltejs/kit'
|
||||||
|
import type { PageServerLoad } from './$types'
|
||||||
|
|
||||||
|
export const load = (async (event) => {
|
||||||
|
const user = event.locals.user
|
||||||
|
const { id } = event.params
|
||||||
|
|
||||||
|
const shortener = await db.query.shortener.findFirst({
|
||||||
|
columns: {
|
||||||
|
code: true,
|
||||||
|
},
|
||||||
|
where: (shortener, { eq, and, isNull }) =>
|
||||||
|
and(eq(shortener.code, id), isNull(shortener.projectId)),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!shortener) {
|
||||||
|
redirect(300, `/links`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await db.query.setting.findFirst({
|
||||||
|
where: (settings, { eq }) => eq(settings.userId, user.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
return { shortener, settings }
|
||||||
|
}) satisfies PageServerLoad
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ScrollArea } from '$lib/components/ui/scroll-area'
|
||||||
|
import type { PageData } from './$types'
|
||||||
|
import QR from './(components)/qr.svelte'
|
||||||
|
|
||||||
|
export let data: PageData
|
||||||
|
export let shallowRouting = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !shallowRouting}
|
||||||
|
<ScrollArea>
|
||||||
|
<div class="py-4 px-10 max-w-2xl">
|
||||||
|
<QR
|
||||||
|
value={data.shortener_url + '/' + data.shortener.code}
|
||||||
|
code={data.shortener.code}
|
||||||
|
background={data.settings?.qr_background || '#fff'}
|
||||||
|
color={data.settings?.qr_foreground || '#000'} />
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
{:else}
|
||||||
|
<QR
|
||||||
|
value={data.shortener_url + '/' + data.shortener.code}
|
||||||
|
code={data.shortener.code}
|
||||||
|
background={data.settings?.qr_background || '#fff'}
|
||||||
|
color={data.settings?.qr_foreground || '#000'} />
|
||||||
|
{/if}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const formSchema = z.object({
|
||||||
|
link: z.string(),
|
||||||
|
project: z.string().optional(),
|
||||||
|
active: z.boolean(),
|
||||||
|
ios: z.boolean(),
|
||||||
|
ios_link: z.string(),
|
||||||
|
android: z.boolean(),
|
||||||
|
android_link: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type FormSchema = typeof formSchema
|
||||||
Loading…
Reference in New Issue