199 lines
4.4 KiB
Vue
199 lines
4.4 KiB
Vue
<script setup>
|
|
import { z } from 'zod'
|
|
import { useForm } from 'vee-validate'
|
|
import { toTypedSchema } from '@vee-validate/zod'
|
|
import { Shuffle, Sparkles } from 'lucide-vue-next'
|
|
import { toast } from 'vue-sonner'
|
|
import { DependencyType } from '@/components/ui/auto-form/interface'
|
|
import { LinkSchema, nanoid } from '@/schemas/link'
|
|
|
|
const props = defineProps({
|
|
link: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits(['update:link'])
|
|
|
|
const link = ref(props.link)
|
|
const dialogOpen = ref(false)
|
|
|
|
const isEdit = !!props.link.id
|
|
|
|
const EditLinkSchema = LinkSchema.pick({
|
|
url: true,
|
|
slug: true,
|
|
}).extend({
|
|
optional: LinkSchema.omit({
|
|
id: true,
|
|
url: true,
|
|
slug: true,
|
|
createdAt: true,
|
|
updatedAt: true,
|
|
title: true,
|
|
description: true,
|
|
image: true,
|
|
}).extend({
|
|
expiration: z.coerce.date().optional(),
|
|
}).optional(),
|
|
})
|
|
|
|
const fieldConfig = {
|
|
slug: {
|
|
disabled: isEdit,
|
|
},
|
|
optional: {
|
|
comment: {
|
|
component: 'textarea',
|
|
},
|
|
},
|
|
}
|
|
|
|
const dependencies = [
|
|
{
|
|
sourceField: 'slug',
|
|
type: DependencyType.DISABLES,
|
|
targetField: 'slug',
|
|
when: () => isEdit,
|
|
},
|
|
]
|
|
|
|
const form = useForm({
|
|
validationSchema: toTypedSchema(EditLinkSchema),
|
|
initialValues: {
|
|
slug: link.value.slug,
|
|
url: link.value.url,
|
|
optional: {
|
|
comment: link.value.comment,
|
|
},
|
|
},
|
|
validateOnMount: isEdit,
|
|
keepValuesOnUnmount: isEdit,
|
|
})
|
|
|
|
function randomSlug() {
|
|
form.setFieldValue('slug', nanoid()())
|
|
}
|
|
|
|
const aiSlugPending = ref(false)
|
|
async function aiSlug() {
|
|
if (!form.values.url)
|
|
return
|
|
|
|
aiSlugPending.value = true
|
|
try {
|
|
const { slug } = await useAPI('/api/link/ai', {
|
|
query: {
|
|
url: form.values.url,
|
|
},
|
|
})
|
|
form.setFieldValue('slug', slug)
|
|
}
|
|
catch (error) {
|
|
console.log(error)
|
|
}
|
|
aiSlugPending.value = false
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (link.value.expiration) {
|
|
form.setFieldValue('optional.expiration', unix2date(link.value.expiration))
|
|
}
|
|
})
|
|
|
|
async function onSubmit(formData) {
|
|
const link = {
|
|
url: formData.url,
|
|
slug: formData.slug,
|
|
...(formData.optional || []),
|
|
expiration: formData.optional?.expiration ? date2unix(formData.optional?.expiration, 'end') : undefined,
|
|
}
|
|
const { link: newLink } = await useAPI(isEdit ? '/api/link/edit' : '/api/link/create', {
|
|
method: isEdit ? 'PUT' : 'POST',
|
|
body: link,
|
|
})
|
|
dialogOpen.value = false
|
|
emit('update:link', newLink)
|
|
if (isEdit) {
|
|
toast('Link updated successfully')
|
|
}
|
|
else {
|
|
toast('Link created successfully')
|
|
}
|
|
}
|
|
|
|
const { previewMode } = useRuntimeConfig().public
|
|
</script>
|
|
|
|
<template>
|
|
<Dialog v-model:open="dialogOpen">
|
|
<DialogTrigger as-child>
|
|
<slot>
|
|
<Button
|
|
class="ml-2"
|
|
variant="outline"
|
|
@click="randomSlug"
|
|
>
|
|
Create Link
|
|
</Button>
|
|
</slot>
|
|
</DialogTrigger>
|
|
<DialogContent class="max-w-[95svw] max-h-[95svh] md:max-w-lg grid-rows-[auto_minmax(0,1fr)_auto]">
|
|
<DialogHeader>
|
|
<DialogTitle>{{ link.id ? 'Edit Link' : 'Create Link' }}</DialogTitle>
|
|
</DialogHeader>
|
|
<p
|
|
v-if="previewMode"
|
|
class="text-sm text-muted-foreground"
|
|
>
|
|
The preview mode link is valid for up to 24 hours.
|
|
</p>
|
|
<AutoForm
|
|
class="px-2 space-y-2 overflow-y-auto"
|
|
:schema="EditLinkSchema"
|
|
:form="form"
|
|
:field-config="fieldConfig"
|
|
:dependencies="dependencies"
|
|
@submit="onSubmit"
|
|
>
|
|
<template #slug="slotProps">
|
|
<div
|
|
v-if="!isEdit"
|
|
class="relative"
|
|
>
|
|
<div class="absolute right-0 flex space-x-3 top-1">
|
|
<Shuffle
|
|
class="w-4 h-4 cursor-pointer"
|
|
@click="randomSlug"
|
|
/>
|
|
<Sparkles
|
|
class="w-4 h-4 cursor-pointer"
|
|
:class="{ 'animate-bounce': aiSlugPending }"
|
|
@click="aiSlug"
|
|
/>
|
|
</div>
|
|
<AutoFormField
|
|
v-bind="slotProps"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<DialogFooter>
|
|
<DialogClose as-child>
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
class="mt-2 sm:mt-0"
|
|
>
|
|
Close
|
|
</Button>
|
|
</DialogClose>
|
|
<Button type="submit">
|
|
Save
|
|
</Button>
|
|
</DialogFooter>
|
|
</AutoForm>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|