Sink-UrlShortener/components/dashboard/links/Editor.vue
2024-08-18 19:38:04 +08:00

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>