|
| 1 | +<template> |
| 2 | + <UModal |
| 3 | + v-model:open="open" |
| 4 | + :title="$t('event.new')" |
| 5 | + description="Add a new event to the database" |
| 6 | + > |
| 7 | + <UButton icon="i-lucide:plus" :label="$t('event.new')" /> |
| 8 | + |
| 9 | + <template #body> |
| 10 | + <UForm :state="state" :schema="schema" class="space-y-4" @submit="onSubmit"> |
| 11 | + <UFormField |
| 12 | + required |
| 13 | + name="title" |
| 14 | + :label="$t('event.title')" |
| 15 | + :placeholder="fields.name.placeholder" |
| 16 | + :description="$t('validation.provide-in-language', { language: 'English' })" |
| 17 | + > |
| 18 | + <UInput |
| 19 | + v-model="state.title" |
| 20 | + class="w-full" |
| 21 | + @change="state.slug = slugify(state.title)" |
| 22 | + /> |
| 23 | + </UFormField> |
| 24 | + <UFormField name="cover_url" :label="$t('event.cover-url')"> |
| 25 | + <UInput |
| 26 | + v-model="state.cover_url" |
| 27 | + type="url" |
| 28 | + class="w-full" |
| 29 | + placeholder="https://example.com/cover.jpg" |
| 30 | + :cover="state.cover_url ? { src: state.cover_url, alt: '' } : undefined" |
| 31 | + > |
| 32 | + <template v-if="state.cover_url?.length" #trailing> |
| 33 | + <UButton |
| 34 | + size="sm" |
| 35 | + variant="link" |
| 36 | + color="neutral" |
| 37 | + icon="i-lucide:circle-x" |
| 38 | + :aria-label="$t('general.clear')" |
| 39 | + @click="state.cover_url = undefined" |
| 40 | + /> |
| 41 | + </template> |
| 42 | + </UInput> |
| 43 | + </UFormField> |
| 44 | + <UFormField |
| 45 | + required |
| 46 | + name="slug" |
| 47 | + :label="$t('general.slug')" |
| 48 | + description="The unique identifier for this event in the URL." |
| 49 | + > |
| 50 | + <UInput v-model="state.slug" class="w-full" @input="normalizeSlug" /> |
| 51 | + </UFormField> |
| 52 | + <fieldset class="flex justify-between"> |
| 53 | + <UFormField |
| 54 | + name="start_year" |
| 55 | + :label="$t('event.start-year')" |
| 56 | + :class="state.start_year ? 'w-50' : 'w-full'" |
| 57 | + :description="state.start_year ? undefined : 'A negative number means before Christ'" |
| 58 | + > |
| 59 | + <UInputNumber |
| 60 | + v-model="state.start_year" |
| 61 | + :max="2025" |
| 62 | + :min="-4026" |
| 63 | + class="w-full" |
| 64 | + :decrement="false" |
| 65 | + :increment="false" |
| 66 | + /> |
| 67 | + </UFormField> |
| 68 | + <UFormField |
| 69 | + v-if="state.start_year" |
| 70 | + required |
| 71 | + class="w-50" |
| 72 | + name="start_precision" |
| 73 | + :label="$t('date.precision')" |
| 74 | + > |
| 75 | + <USelect |
| 76 | + v-model="state.start_precision" |
| 77 | + class="w-full" |
| 78 | + :items="fields.datePrecision.items" |
| 79 | + /> |
| 80 | + </UFormField> |
| 81 | + </fieldset> |
| 82 | + <fieldset class="flex justify-between"> |
| 83 | + <UFormField |
| 84 | + name="end_year" |
| 85 | + :label="$t('event.end-year')" |
| 86 | + :class="state.end_year ? 'w-50' : 'w-full'" |
| 87 | + :description="state.end_year ? undefined : 'A negative number means before Christ'" |
| 88 | + > |
| 89 | + <UInputNumber |
| 90 | + v-model="state.end_year" |
| 91 | + :max="2025" |
| 92 | + :min="-4026" |
| 93 | + class="w-full" |
| 94 | + :decrement="false" |
| 95 | + :increment="false" |
| 96 | + /> |
| 97 | + </UFormField> |
| 98 | + <UFormField |
| 99 | + v-if="state.end_year" |
| 100 | + required |
| 101 | + class="w-50" |
| 102 | + name="end_precision" |
| 103 | + :label="$t('date.precision')" |
| 104 | + > |
| 105 | + <USelect |
| 106 | + v-model="state.end_precision" |
| 107 | + class="w-full" |
| 108 | + :items="fields.datePrecision.items" |
| 109 | + /> |
| 110 | + </UFormField> |
| 111 | + </fieldset> |
| 112 | + <div class="flex justify-end gap-2"> |
| 113 | + <UButton |
| 114 | + type="reset" |
| 115 | + color="neutral" |
| 116 | + variant="subtle" |
| 117 | + :label="$t('general.cancel')" |
| 118 | + @click="open = false" |
| 119 | + /> |
| 120 | + <UButton type="submit" color="primary" variant="solid" :label="$t('general.create')" /> |
| 121 | + </div> |
| 122 | + </UForm> |
| 123 | + </template> |
| 124 | + </UModal> |
| 125 | +</template> |
| 126 | +<script setup lang="ts"> |
| 127 | +import type { FormSubmitEvent } from '@nuxt/ui' |
| 128 | +
|
| 129 | +import { z } from 'zod' |
| 130 | +
|
| 131 | +import EventsRelatedCard from './EventsRelatedCard.vue' |
| 132 | +
|
| 133 | +const open = ref(false) |
| 134 | +
|
| 135 | +const { t } = useI18n() |
| 136 | +const { fields, rules } = useForm() |
| 137 | +
|
| 138 | +const schema = z |
| 139 | + .object({ |
| 140 | + cover_url: rules.url(t('event.cover-url')).optional(), |
| 141 | + end_precision: rules.datePrecision(t('date.precision')).optional(), |
| 142 | + end_year: rules.year(t('event.end-year')).optional(), |
| 143 | + slug: rules.slug, |
| 144 | + start_precision: rules.datePrecision(t('date.precision')).optional(), |
| 145 | + start_year: rules.year(t('event.start-year')).optional(), |
| 146 | + title: rules.name |
| 147 | + }) |
| 148 | + .refine( |
| 149 | + (data) => { |
| 150 | + if (!data.start_year || !data.end_year) return true |
| 151 | + return data.end_year >= data.start_year |
| 152 | + }, |
| 153 | + { |
| 154 | + message: t('validation.after-or-equal-to', { |
| 155 | + date: t('event.start-year'), |
| 156 | + field: t('event.end-year') |
| 157 | + }), |
| 158 | + path: ['end_year'] |
| 159 | + } |
| 160 | + ) |
| 161 | + .refine((data) => !data.start_year || !!data.start_precision, { |
| 162 | + message: t('validation.required', { field: t('date.precision') }), |
| 163 | + path: ['start_precision'] |
| 164 | + }) |
| 165 | + .refine((data) => !data.end_year || !!data.end_precision, { |
| 166 | + message: t('validation.required', { field: t('date.precision') }), |
| 167 | + path: ['end_precision'] |
| 168 | + }) |
| 169 | +
|
| 170 | +type Schema = z.output<typeof schema> |
| 171 | +
|
| 172 | +const state = reactive<Partial<Schema>>({}) |
| 173 | +
|
| 174 | +const resetState = () => { |
| 175 | + state.title = undefined |
| 176 | + state.cover_url = undefined |
| 177 | + state.slug = undefined |
| 178 | + state.start_year = undefined |
| 179 | + state.start_precision = undefined |
| 180 | + state.end_year = undefined |
| 181 | + state.end_precision = undefined |
| 182 | +} |
| 183 | +
|
| 184 | +const normalizeSlug = (e: Event) => { |
| 185 | + const input = e.target as HTMLInputElement |
| 186 | + input.value = slugify(input.value, false) |
| 187 | + state.slug = input.value |
| 188 | +} |
| 189 | +
|
| 190 | +const supabase = useSupabaseClient() |
| 191 | +
|
| 192 | +const { showError, showSuccess } = useFlash() |
| 193 | +async function onSubmit(event: FormSubmitEvent<Schema>) { |
| 194 | + const { error } = await supabase.from('events').insert(event.data) |
| 195 | +
|
| 196 | + if (error) { |
| 197 | + showError({ |
| 198 | + description: t('feedback.could-not-save', { item: event.data.title }) |
| 199 | + }) |
| 200 | + } else { |
| 201 | + showSuccess({ |
| 202 | + description: t('feedback.saved-successfully', { item: event.data.title }) |
| 203 | + }) |
| 204 | + open.value = false |
| 205 | + resetState() |
| 206 | + refreshNuxtData('events') |
| 207 | + } |
| 208 | +} |
| 209 | +</script> |
0 commit comments