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