diff --git a/src/components/FormWishlist.vue b/src/components/FormWishlist.vue new file mode 100644 index 0000000..c826d46 --- /dev/null +++ b/src/components/FormWishlist.vue @@ -0,0 +1,124 @@ +<template> + <Form + @submit="onSubmit" + :validation-schema="schema" + v-slot="{ meta }" + class="w-full flex-col" + > + <InputText + name="title" + type="text" + :value="wishlist.title" + :label="t('components.wishlist-header.main.form.title.label')" + /> + <InputCheckbox + name="public" + :value="wishlist.public" + :label="t('components.wishlist-header.main.form.public.label')" + /> + <InputTextArea + name="description" + type="text" + :value="wishlist.description" + height-class="h-20" + :label="t('components.wishlist-header.main.form.description.label')" + /> + <InputText + name="imageSrc" + type="text" + :value="wishlist.imageSrc" + :label="t('components.wishlist-header.main.form.image-src.label')" + /> + <InputFile + name="imageFile" + :label="t('components.wishlist-header.main.form.image-file.label')" + /> + <InputText + name="slugUrlText" + type="text" + :value="wishlist.slugUrlText" + :label="t('components.wishlist-header.main.form.slug-text.label')" + /> + <ButtonBase + class="h-12 w-full" + mode="primary" + :disabled="!meta.valid" + :icon="IconSave" + >{{ t('components.wishlist-header.main.form.submit.text') }}</ButtonBase + > + </Form> +</template> + +<script setup lang="ts"> +import { Wishlist } from '@/types' +import { PropType } from 'vue' +import { useRouter } from 'vue-router' +import { useI18n } from 'vue-i18n' +import { Form } from 'vee-validate' +import { object, string, boolean } from 'yup' +import { useToast } from 'vue-toastification' +import { + ButtonBase, + InputText, + InputFile, + InputCheckbox, + InputTextArea, +} from '@/components' +import { IconSave } from '@/components/icons' +import { useWishlistStore } from '@/composables' + +defineProps({ + wishlist: { + type: Object as PropType<Wishlist>, + requried: true, + }, +}) + +const router = useRouter() +const toast = useToast() + +const { update } = useWishlistStore() + +const { t } = useI18n() + +const schema = object().shape( + { + title: string().required( + t('components.wishlist-header.main.form.title.error-requried') + ), + public: boolean(), + description: string().max( + 300, + t('components.wishlist-header.main.form.description.error-max') + ), + slugUrlText: string().required( + t('components.wishlist-header.main.form.slug-text.error-requried') + ), + imageSrc: string().when('imageFile', { + is: (imageFile: string) => !imageFile || imageFile.length === 0, + then: string().required( + t('components.wishlist-header.main.form.image-src.error-requried') + ), + }), + imageFile: string().when('imageSrc', { + is: (imageSrc: string) => !imageSrc || imageSrc.length === 0, + then: string().required( + t('components.wishlist-header.main.form.image-file.error-requried') + ), + }), + }, + //@ts-expect-error ... + ['imageSrc', 'imageFile'] +) + +const onSubmit = async (values: any): Promise<void> => { + try { + values.imageSrc = values.imageFile || values.imageSrc + await update(values) + toast.success(t('common.wishlist.saved.text')) + router.push(`/${values.slugUrlText}`) + } catch (error) { + toast.error(t('common.wishlist.saving-failed.text')) + } +} +</script> diff --git a/src/components/WishlistHeader.vue b/src/components/WishlistHeader.vue deleted file mode 100644 index 2531480..0000000 --- a/src/components/WishlistHeader.vue +++ /dev/null @@ -1,141 +0,0 @@ -<template> - <div - class="flex flex-col items-center space-x-0 space-y-2 md:flex-row md:space-x-6 md:space-y-0" - v-if="modelValue !== undefined" - > - <ImageTile :image-src="modelValue.imageSrc" class="shrink-0"></ImageTile> - <div v-if="!editModeIsActive"> - <h1 class="mb-2 text-center text-2xl font-bold md:text-left"> - {{ modelValue.title }} - </h1> - <p class="text-lg"> - {{ modelValue.description }} - </p> - </div> - <Form - v-else - @submit="onSubmit" - :validation-schema="schema" - v-slot="{ meta }" - class="w-full flex-col" - > - <InputText - name="title" - type="text" - :value="modelValue.title" - :label="t('components.wishlist-header.main.form.title.label')" - /> - <InputCheckbox - name="public" - :value="modelValue.public" - :label="t('components.wishlist-header.main.form.public.label')" - /> - <InputTextArea - name="description" - type="text" - :value="modelValue.description" - height-class="h-20" - :label="t('components.wishlist-header.main.form.description.label')" - /> - <InputText - name="imageSrc" - type="text" - :value="modelValue.imageSrc" - :label="t('components.wishlist-header.main.form.image-src.label')" - /> - <InputFile - name="imageFile" - :label="t('components.wishlist-header.main.form.image-file.label')" - /> - <InputText - name="slugUrlText" - type="text" - :value="modelValue.slugUrlText" - :label="t('components.wishlist-header.main.form.slug-text.label')" - /> - <ButtonBase - class="h-12 w-full" - mode="primary" - :disabled="!meta.valid" - :icon="IconSave" - >{{ t('components.wishlist-header.main.form.submit.text') }}</ButtonBase - > - </Form> - </div> -</template> - -<script setup lang="ts"> -import { Wishlist } from '@/types' -import { PropType } from 'vue' -import { useRouter } from 'vue-router' -import { useI18n } from 'vue-i18n' -import { Form } from 'vee-validate' -import { object, string, boolean } from 'yup' -import { useToast } from 'vue-toastification' -import { - ButtonBase, - ImageTile, - InputText, - InputFile, - InputCheckbox, - InputTextArea, -} from '@/components' -import { IconSave } from '@/components/icons' -import { useEditMode, useWishlistStore } from '@/composables' - -defineProps({ - modelValue: { - type: Object as PropType<Wishlist>, - requried: true, - }, -}) - -const router = useRouter() -const toast = useToast() - -const { isActive: editModeIsActive } = useEditMode() -const { update } = useWishlistStore() - -const { t } = useI18n() - -const schema = object().shape( - { - title: string().required( - t('components.wishlist-header.main.form.title.error-requried') - ), - public: boolean(), - description: string().max( - 300, - t('components.wishlist-header.main.form.description.error-max') - ), - slugUrlText: string().required( - t('components.wishlist-header.main.form.slug-text.error-requried') - ), - imageSrc: string().when('imageFile', { - is: (imageFile: string) => !imageFile || imageFile.length === 0, - then: string().required( - t('components.wishlist-header.main.form.image-src.error-requried') - ), - }), - imageFile: string().when('imageSrc', { - is: (imageSrc: string) => !imageSrc || imageSrc.length === 0, - then: string().required( - t('components.wishlist-header.main.form.image-file.error-requried') - ), - }), - }, - //@ts-expect-error ... - ['imageSrc', 'imageFile'] -) - -const onSubmit = async (values: any): Promise<void> => { - try { - values.imageSrc = values.imageFile || values.imageSrc - await update(values) - toast.success(t('common.wishlist.saved.text')) - router.push(`/${values.slugUrlText}`) - } catch (error) { - toast.error(t('common.wishlist.saving-failed.text')) - } -} -</script> diff --git a/src/components/WishlistItem.vue b/src/components/WishlistItem.vue index 47ff30d..068d115 100644 --- a/src/components/WishlistItem.vue +++ b/src/components/WishlistItem.vue @@ -4,11 +4,9 @@ import IconLink from './icons/IconLink.vue' import ImagePreview from './ImagePreview.vue' import { ButtonBase } from './' import IconCart from './icons/IconCart.vue' +import { WishlistItem } from '@/types' defineProps<{ - title: string - image: string - url?: string - description: string + item: WishlistItem }>() const { t } = useI18n() </script> @@ -19,15 +17,15 @@ const { t } = useI18n() > <ImagePreview class="max-h-44 flex-shrink-0 flex-grow-0 object-cover sm:aspect-[3/2]" - :src="image" - :alt="title" + :src="item.imageSrc" + :alt="item.title" /> <div class="flex flex-col justify-between p-2"> <div> - <h1 class="mb-1 text-lg font-bold">{{ title }}</h1> + <h1 class="mb-1 text-lg font-bold">{{ item.title }}</h1> <p class="text-sm sm:line-clamp-3"> - {{ description }} + {{ item.description }} </p> </div> <div class="flex flex-row items-baseline space-x-2"> @@ -38,8 +36,8 @@ const { t } = useI18n() >{{ t('components.wishlist-item.bought-button.text') }}</ButtonBase > <a - v-if="url" - :href="url" + v-if="item.url" + :href="item.url" target="_blank" rel="noopener" class="mt-1 flex w-fit flex-row items-center text-sm text-stone-500 dark:text-white/60" diff --git a/src/components/index.ts b/src/components/index.ts index 2d60a54..79ff6c5 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -7,5 +7,5 @@ export { default as InputText } from './InputText.vue' export { default as InputFile } from './InputFile.vue' export { default as InputTextArea } from './InputTextArea.vue' export { default as Modal } from './Modal.vue' -export { default as WishlistHeader } from './WishlistHeader.vue' +export { default as FormWishlist } from './FormWishlist.vue' export { default as WishlistItem } from './WishlistItem.vue' diff --git a/src/views/DetailView.vue b/src/views/DetailView.vue index 8b27000..ad0b7de 100644 --- a/src/views/DetailView.vue +++ b/src/views/DetailView.vue @@ -2,14 +2,14 @@ import { useI18n } from 'vue-i18n' import { WishlistItem as WishlistItemType } from '@/types' import { useRoute } from 'vue-router' -import { useWishlistStore, useModal } from '@/composables' -import WishlistItem from '@/components/WishlistItem.vue' -import WishlistHeader from '@/components/WishlistHeader.vue' +import { useWishlistStore, useModal, useEditMode } from '@/composables' +import { FormWishlist, ImageTile, WishlistItem } from '@/components' import { IconNoGift } from '../components/icons' const route = useRoute() const modal = useModal() const { t } = useI18n() +const { isActive: editModeIsActive } = useEditMode() const { state, fetch, isReady, itemBought, filteredItems } = useWishlistStore() await fetch(route.params.slug as string) @@ -29,20 +29,35 @@ const bought = async (item: WishlistItemType): Promise<void> => { <template> <div v-if="isReady" class="h-full"> - <WishlistHeader v-model="state" /> + <div + class="flex flex-col items-center space-x-0 space-y-2 md:flex-row md:space-x-6 md:space-y-0" + v-if="state !== undefined" + > + <ImageTile :image-src="state.imageSrc" class="shrink-0"></ImageTile> + <div v-if="!editModeIsActive"> + <h1 class="mb-2 text-center text-2xl font-bold md:text-left"> + {{ state.title }} + </h1> + <p class="text-lg"> + {{ state.description }} + </p> + </div> + <FormWishlist v-else :wishlist="state" /> + </div> <div v-if="filteredItems.length > 0" class="flex flex-col space-y-14 py-10 md:space-y-8" > - <WishlistItem - v-for="(item, index) in filteredItems" - :key="index" - :title="item.title" - :url="item.url" - :image="item.imageSrc" - :description="item.description" - @bought="bought(item)" - /> + <div v-for="(item, index) in filteredItems" :key="index"> + <WishlistItem + :item="item" + :title="item.title" + :url="item.url" + :image="item.imageSrc" + :description="item.description" + @bought="bought(item)" + /> + </div> </div> <div v-else class="flex h-1/2 w-full justify-center"> <div