allow to change wishlist item

Signed-off-by: Benny Samir Hierl <bennysamir@posteo.de>
This commit is contained in:
Benny Samir Hierl 2022-02-16 22:39:29 +01:00
parent b291a09590
commit dd388bebf2
7 changed files with 265 additions and 116 deletions

View file

@ -9,42 +9,42 @@
name="title" name="title"
type="text" type="text"
:value="wishlist.title" :value="wishlist.title"
:label="t('components.wishlist-header.main.form.title.label')" :label="t('components.form-wishlist.title.label')"
/> />
<InputCheckbox <InputCheckbox
name="public" name="public"
:value="wishlist.public" :value="wishlist.public"
:label="t('components.wishlist-header.main.form.public.label')" :label="t('components.form-wishlist.public.label')"
/> />
<InputTextArea <InputTextArea
name="description" name="description"
type="text" type="text"
:value="wishlist.description" :value="wishlist.description"
height-class="h-20" height-class="h-20"
:label="t('components.wishlist-header.main.form.description.label')" :label="t('components.form-wishlist.description.label')"
/> />
<InputText <InputText
name="imageSrc" name="imageSrc"
type="text" type="text"
:value="wishlist.imageSrc" :value="wishlist.imageSrc"
:label="t('components.wishlist-header.main.form.image-src.label')" :label="t('components.form-wishlist.image-src.label')"
/> />
<InputFile <InputFile
name="imageFile" name="imageFile"
:label="t('components.wishlist-header.main.form.image-file.label')" :label="t('components.form-wishlist.image-file.label')"
/> />
<InputText <InputText
name="slugUrlText" name="slugUrlText"
type="text" type="text"
:value="wishlist.slugUrlText" :value="wishlist.slugUrlText"
:label="t('components.wishlist-header.main.form.slug-text.label')" :label="t('components.form-wishlist.slug-text.label')"
/> />
<ButtonBase <ButtonBase
class="h-12 w-full" class="h-12 w-full"
mode="primary" mode="primary"
:disabled="!meta.valid" :disabled="!meta.valid"
:icon="IconSave" :icon="IconSave"
>{{ t('components.wishlist-header.main.form.submit.text') }}</ButtonBase >{{ t('components.form-wishlist.submit.text') }}</ButtonBase
> >
</Form> </Form>
</template> </template>
@ -74,26 +74,26 @@ const { t } = useI18n()
const schema = object().shape( const schema = object().shape(
{ {
title: string().required( title: string().required(
t('components.wishlist-header.main.form.title.error-requried') t('components.form-wishlist.title.error-requried')
), ),
public: boolean(), public: boolean(),
description: string().max( description: string().max(
300, 300,
t('components.wishlist-header.main.form.description.error-max') t('components.form-wishlist.description.error-max')
), ),
slugUrlText: string().required( slugUrlText: string().required(
t('components.wishlist-header.main.form.slug-text.error-requried') t('components.form-wishlist.slug-text.error-requried')
), ),
imageSrc: string().when('imageFile', { imageSrc: string().when('imageFile', {
is: (imageFile: string) => !imageFile || imageFile.length === 0, is: (imageFile: string) => !imageFile || imageFile.length === 0,
then: string().required( then: string().required(
t('components.wishlist-header.main.form.image-src.error-requried') t('components.form-wishlist.image-src.error-requried')
), ),
}), }),
imageFile: string().when('imageSrc', { imageFile: string().when('imageSrc', {
is: (imageSrc: string) => !imageSrc || imageSrc.length === 0, is: (imageSrc: string) => !imageSrc || imageSrc.length === 0,
then: string().required( then: string().required(
t('components.wishlist-header.main.form.image-file.error-requried') t('components.form-wishlist.image-file.error-requried')
), ),
}), }),
}, },

View file

@ -1,20 +1,6 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import ImagePreview from './ImagePreview.vue'
import { ButtonBase } from './'
import IconDelete from './icons/IconDelete.vue'
import { WishlistItem } from '@/types'
defineProps<{
item: WishlistItem
}>()
const { t } = useI18n()
const emits = defineEmits(['delete'])
</script>
<template> <template>
<div <div
class="flex h-fit flex-col space-x-0 overflow-hidden rounded-md border-2 border-stone-200 dark:border-stone-700 sm:h-40 sm:flex-row sm:space-x-2" class="flex h-fit flex-col space-x-0 overflow-hidden rounded-md border-2 border-stone-200 dark:border-stone-700 sm:flex-row sm:space-x-2"
> >
<ImagePreview <ImagePreview
class="max-h-44 flex-shrink-0 flex-grow-0 object-cover sm:aspect-[3/2]" class="max-h-44 flex-shrink-0 flex-grow-0 object-cover sm:aspect-[3/2]"
@ -22,8 +8,51 @@ const emits = defineEmits(['delete'])
:alt="item.title" :alt="item.title"
/> />
<div class="flex w-full flex-col justify-between p-2"> <div class="flex w-full flex-col justify-between space-y-2 p-2">
<span></span> <Form
@submit="onSubmit"
:validation-schema="schema"
v-slot="{ meta }"
class="w-full flex-col"
>
<InputText
name="title"
type="text"
:value="item.title"
:label="t('components.form-wishlist-item.title.label')"
/>
<InputTextArea
name="description"
type="text"
:value="item.description"
height-class="h-20"
:label="t('components.form-wishlist-item.description.label')"
/>
<InputText
name="url"
type="text"
:value="item.url"
:label="t('components.form-wishlist-item.url.label')"
/>
<InputText
name="imageSrc"
type="text"
:value="item.imageSrc"
:label="t('components.form-wishlist-item.image-src.label')"
/>
<InputCheckbox
name="bought"
:value="item.bought"
:label="t('components.form-wishlist-item.bought.label')"
/>
<ButtonBase
class="h-12 w-full"
mode="primary"
:disabled="!meta.valid"
:icon="IconSave"
>{{ t('components.form-wishlist-item.submit.text') }}</ButtonBase
>
</Form>
<ButtonBase <ButtonBase
class="h-12 w-full" class="h-12 w-full"
mode="danger" mode="danger"
@ -34,3 +63,41 @@ const emits = defineEmits(['delete'])
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { Form } from 'vee-validate'
import ImagePreview from './ImagePreview.vue'
import { object, string, boolean } from 'yup'
import {
ButtonBase,
InputText,
InputCheckbox,
InputTextArea,
} from '@/components'
import { IconSave, IconDelete } from '@/components/icons'
import { WishlistItem } from '@/types'
defineProps<{
item: WishlistItem
}>()
const { t } = useI18n()
const emits = defineEmits(['update', 'delete'])
const schema = object({
title: string().required(
t('components.form-wishlist-item.title.error-requried')
),
description: string()
.required(t('components.form-wishlist-item.description.error-requried'))
.max(300, t('components.form-wishlist-item.description.error-max')),
url: string().url(t('components.form-wishlist-item.url.error-url')),
imageSrc: string().url(
t('components.form-wishlist-item.image-src.error-url')
),
bought: boolean(),
})
const onSubmit = async (values: any): Promise<void> => {
emits('update', values)
}
</script>

View file

@ -16,19 +16,15 @@
</div> </div>
<div> <div>
<i18n-t <i18n-t
keypath="components.wishlist-header.main.form.image-file.text-dropzone" keypath="components.file.text-dropzone"
tag="p" tag="p"
for="components.wishlist-header.main.form.image-file.text-dropzone-link" for="components.file.text-dropzone-link"
> >
<button <button
class="cursor-pointer text-cyan-600" class="cursor-pointer text-cyan-600"
@click.prevent="fileInput.click()" @click.prevent="fileInput.click()"
> >
{{ {{ t('components.file.text-dropzone-link') }}
t(
'components.wishlist-header.main.form.image-file.text-dropzone-link'
)
}}
</button> </button>
</i18n-t> </i18n-t>
<input <input

View file

@ -40,6 +40,32 @@ const update = async (updatedData: Wishlist): Promise<void> => {
} }
} }
const updateItem = async (
currentValues: WishlistItem,
newValues: WishlistItem
): Promise<void> => {
const id = state.value?.id
const payload = {
...currentValues,
...newValues,
}
try {
const { data } = await client.put(
`/wishlist/${id}/item/${currentValues.id}`,
payload
)
state.value?.items?.splice(
state.value.items.indexOf(currentValues),
1,
data
)
} catch (e: any) {
if (e.isAxiosError && !(<CustomAxiosError>e.ignore)) {
throw e
}
}
}
const itemBought = async (item: WishlistItem): Promise<void> => { const itemBought = async (item: WishlistItem): Promise<void> => {
await client.post(`/wishlist/${item.wishlistId}/item/${item.id}/bought`) await client.post(`/wishlist/${item.wishlistId}/item/${item.id}/bought`)
item.bought = true item.bought = true
@ -65,6 +91,7 @@ export const useWishlistStore = () => {
isReady, isReady,
fetch, fetch,
update, update,
updateItem,
itemBought, itemBought,
itemDelete, itemDelete,
filteredItems, filteredItems,

View file

@ -6,13 +6,11 @@
"loading": { "loading": {
"text": "Lade..." "text": "Lade..."
}, },
"wishlist": { "saved": {
"saved": { "text": "Wunschliste gespeichert"
"text": "Wunschliste gespeichert" },
}, "saving-failed": {
"saving-failed": { "text": "Wunschliste konnte nicht gespeichert werden"
"text": "Wunschliste konnte nicht gespeichert werden"
}
} }
}, },
"errors": { "errors": {
@ -81,6 +79,10 @@
} }
}, },
"components": { "components": {
"file": {
"text-dropzone-link": "hier",
"text-dropzone": "Ziehen Sie ein beliebiges Bild hierher oder klicken Sie {0}, um den Dialog zu öffnen."
},
"wishlist-item": { "wishlist-item": {
"external-product-page-link": { "external-product-page-link": {
"text": "Produktseite öffnen" "text": "Produktseite öffnen"
@ -92,39 +94,60 @@
"text": "Löschen" "text": "Löschen"
} }
}, },
"wishlist-header": { "form-wishlist": {
"main": { "title": {
"form": { "label": "Titel",
"title": { "error-requried": "Titel wird benötigt."
"label": "Titel", },
"error-requried": "Titel wird benötigt" "public": {
}, "label": "Auf der Startseite anzeigen?"
"public": { },
"label": "Auf der Startseite anzeigen?" "description": {
}, "label": "Beschreibung",
"description": { "error-max": "Die maximale Länge beträgt 300 Zeichen."
"label": "Beschreibung", },
"error-max": "Die maximale Länge beträgt 300 Zeichen" "slug-text": {
}, "label": "URL Slug-Text",
"slug-text": { "error-requried": "URL Slug-Text wird benötigt."
"label": "URL Slug-Text", },
"error-requried": "URL Slug-Text wird benötigt" "image-src": {
}, "label": "Bild-URL",
"image-src": { "error-requried": "Bild-URL wird benötigt."
"label": "Bild-URL", },
"error-requried": "Bild-URL wird benötigt" "image-file": {
}, "label": "Bild-Datei",
"image-file": { "text-dropzone-link": "hier",
"label": "Bild-Datei", "text-dropzone": "Ziehen Sie ein beliebiges Bild hierher oder klicken Sie {0}, um den Dialog zu öffnen.",
"text-dropzone-link": "hier", "error-requried": "Bild-Datei wird benötigt.",
"text-dropzone": "Ziehen Sie ein beliebiges Bild hierher oder klicken Sie {0}, um den Dialog zu öffnen.", "error-image-size": "Höhe und Breite dürfen 200px nicht überschreiten."
"error-requried": "Bild-Datei wird benötigt", },
"error-image-size": "Höhe und Breite dürfen 200px nicht überschreiten." "submit": {
}, "text": "Speichern"
"submit": { }
"text": "Speichern" },
} "form-wishlist-item": {
} "title": {
"label": "Titel",
"error-requried": "Titel wird benötigt."
},
"description": {
"label": "Beschreibung",
"error-requried": "Beschreibung wird benötigt.",
"error-max": "Die maximale Länge beträgt 300 Zeichen."
},
"url": {
"label": "Produkt-URL",
"error-url": "Keine gültige URL."
},
"image-src": {
"label": "Bild-URL",
"error-url": "Keine gültige URL."
},
"bought": {
"label": "Gekauft?"
},
"submit": {
"text": "Speichern"
} }
}, },
"header": { "header": {

View file

@ -6,13 +6,11 @@
"loading": { "loading": {
"text": "Loading..." "text": "Loading..."
}, },
"wishlist": { "saved": {
"saved": { "text": "Wishlist saved"
"text": "Wishlist saved" },
}, "saving-failed": {
"saving-failed": { "text": "Saving wishlist failed"
"text": "Saving wishlist failed"
}
} }
}, },
"errors": { "errors": {
@ -81,6 +79,10 @@
} }
}, },
"components": { "components": {
"file": {
"text-dropzone-link": "click here",
"text-dropzone": "drag and drop any image here or {0} to open dialog."
},
"wishlist-item": { "wishlist-item": {
"external-product-page-link": { "external-product-page-link": {
"text": "Open product page" "text": "Open product page"
@ -92,37 +94,57 @@
"text": "Delete" "text": "Delete"
} }
}, },
"wishlist-header": { "form-wishlist": {
"main": { "title": {
"form": { "label": "Title",
"title": { "error-requried": "Titel is required."
"label": "Title" },
}, "public": {
"public": { "label": "Show on startpage?"
"label": "Show on startpage?" },
}, "description": {
"description": { "label": "Description",
"label": "Description", "error-max": "The max. length is 300 chars."
"error-max": "The max. length is 300 chars." },
}, "slug-text": {
"slug-text": { "label": "URL Slug-Text"
"label": "URL Slug-Text" },
}, "image-src": {
"image-src": { "label": "Image-URL",
"label": "Image-URL", "error-requried": "Image-URL is required."
"error-requried": "Image-URL is required" },
}, "image-file": {
"image-file": { "label": "Image-File",
"label": "Image-File", "error-requried": "Image-File is required.",
"text-dropzone-link": "click here", "error-image-size": "Height and Width must not exceed 200px."
"text-dropzone": "drag and drop any image here or {0} to open dialog.", },
"error-requried": "Image-File is required", "submit": {
"error-image-size": "Height and Width must not exceed 200px." "text": "Save"
}, }
"submit": { },
"text": "Save" "form-wishlist-item": {
} "title": {
} "label": "Title",
"error-requried": "Title is required."
},
"description": {
"label": "Description",
"error-requried": "Description is required.",
"error-max": "The max. length is 300 chars."
},
"url": {
"label": "Produkt-URL",
"error-url": "Invalid URL"
},
"image-src": {
"label": "Bild-URL",
"error-url": "Invalid URL"
},
"bought": {
"label": "Bought?"
},
"submit": {
"text": "Save"
} }
}, },
"header": { "header": {

View file

@ -24,6 +24,7 @@ const {
fetch, fetch,
isReady, isReady,
update, update,
updateItem,
itemBought, itemBought,
itemDelete, itemDelete,
filteredItems, filteredItems,
@ -33,10 +34,22 @@ await fetch(route.params.slug as string)
const handleUpdateWishlist = async (wishlist: Wishlist): Promise<void> => { const handleUpdateWishlist = async (wishlist: Wishlist): Promise<void> => {
try { try {
await update(wishlist) await update(wishlist)
toast.success(t('common.wishlist.saved.text')) toast.success(t('common.saved.text'))
router.push(`/${wishlist.slugUrlText}`) router.push(`/${wishlist.slugUrlText}`)
} catch (error) { } catch (error) {
toast.error(t('common.wishlist.saving-failed.text')) toast.error(t('common.saving-failed.text'))
}
}
const handleUpdateItem = async (
currentValues: WishlistItemType,
newValues: WishlistItemType
): Promise<void> => {
try {
await updateItem(currentValues, newValues)
toast.success(t('common.saved.text'))
} catch (error) {
toast.error(t('common.saving-failed.text'))
} }
} }
@ -93,6 +106,7 @@ const handleDeleteItem = async (item: WishlistItemType): Promise<void> => {
<FormWishlistItem <FormWishlistItem
v-else v-else
:item="item" :item="item"
@update="(updateValues) => handleUpdateItem(item, updateValues)"
@delete="handleDeleteItem(item)" @delete="handleDeleteItem(item)"
/> />
</div> </div>