mirror of
https://github.com/ThisIsBenny/wishlist-app.git
synced 2025-06-07 05:57:41 +00:00
allow image file upload for wishlist
Signed-off-by: Benny Samir Hierl <bennysamir@posteo.de>
This commit is contained in:
parent
37817098ae
commit
77d9916517
5 changed files with 156 additions and 22 deletions
106
src/components/InputFile.vue
Normal file
106
src/components/InputFile.vue
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative mb-8">
|
||||||
|
<label class="mb-1 block w-full" :for="name">{{ label }}</label>
|
||||||
|
<div
|
||||||
|
class="flex h-24 w-full flex-row items-center space-x-10 rounded-md border-2 border-solid border-stone-300 bg-transparent px-2 outline-none dark:border-stone-700"
|
||||||
|
:class="{
|
||||||
|
'border-rose-500': !!errorMessage,
|
||||||
|
'border-dotted bg-stone-200/20': showDropzone,
|
||||||
|
}"
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
@dragover.prevent="showDropzone = true"
|
||||||
|
@dragleave.prevent="showDropzone = false"
|
||||||
|
>
|
||||||
|
<div class="h-20 w-20">
|
||||||
|
<ImagePreview :src="value" class="h-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i18n-t
|
||||||
|
keypath="components.wishlist-header.main.form.image-file.text-dropzone"
|
||||||
|
tag="p"
|
||||||
|
for="components.wishlist-header.main.form.image-file.text-dropzone-link"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="cursor-pointer text-cyan-600"
|
||||||
|
@click.prevent="fileInput.click()"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
'components.wishlist-header.main.form.image-file.text-dropzone-link'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</i18n-t>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
class="hidden"
|
||||||
|
:name="name"
|
||||||
|
:id="name"
|
||||||
|
type="file"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="absolute mt-2 text-sm text-rose-500" v-show="errorMessage">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useField } from 'vee-validate'
|
||||||
|
import { ImagePreview } from './'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
type FileEventTarget = EventTarget & { files: FileList }
|
||||||
|
const fileInput = ref()
|
||||||
|
const showDropzone = ref(false)
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
accept: {
|
||||||
|
type: String,
|
||||||
|
default: 'image/*',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { value, errorMessage } = useField(props.name, undefined, {})
|
||||||
|
|
||||||
|
const convertBase64 = (file: File): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fileReader = new FileReader()
|
||||||
|
fileReader.readAsDataURL(file)
|
||||||
|
|
||||||
|
fileReader.onload = () => {
|
||||||
|
resolve(fileReader.result as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileReader.onerror = (error) => {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = async (event: any) => {
|
||||||
|
const file = (event.target as FileEventTarget).files[0]
|
||||||
|
value.value = await convertBase64(file)
|
||||||
|
}
|
||||||
|
const handleDrop = async (event: any) => {
|
||||||
|
let droppedFiles = event.dataTransfer.files
|
||||||
|
if (!droppedFiles) return
|
||||||
|
value.value = await convertBase64(droppedFiles[0])
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -43,6 +43,10 @@
|
||||||
:value="modelValue.imageSrc"
|
:value="modelValue.imageSrc"
|
||||||
:label="t('components.wishlist-header.main.form.image-src.label')"
|
:label="t('components.wishlist-header.main.form.image-src.label')"
|
||||||
/>
|
/>
|
||||||
|
<InputFile
|
||||||
|
name="imageFile"
|
||||||
|
:label="t('components.wishlist-header.main.form.image-file.label')"
|
||||||
|
/>
|
||||||
<InputText
|
<InputText
|
||||||
name="slugUrlText"
|
name="slugUrlText"
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -72,6 +76,7 @@ import {
|
||||||
ButtonBase,
|
ButtonBase,
|
||||||
ImageTile,
|
ImageTile,
|
||||||
InputText,
|
InputText,
|
||||||
|
InputFile,
|
||||||
InputCheckbox,
|
InputCheckbox,
|
||||||
InputTextArea,
|
InputTextArea,
|
||||||
} from '@/components'
|
} from '@/components'
|
||||||
|
@ -93,7 +98,8 @@ const { update } = useWishlistStore()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const schema = object({
|
const schema = object().shape(
|
||||||
|
{
|
||||||
title: string().required(
|
title: string().required(
|
||||||
t('components.wishlist-header.main.form.title.error-requried')
|
t('components.wishlist-header.main.form.title.error-requried')
|
||||||
),
|
),
|
||||||
|
@ -105,15 +111,26 @@ const schema = object({
|
||||||
slugUrlText: string().required(
|
slugUrlText: string().required(
|
||||||
t('components.wishlist-header.main.form.slug-text.error-requried')
|
t('components.wishlist-header.main.form.slug-text.error-requried')
|
||||||
),
|
),
|
||||||
imageSrc: string()
|
imageSrc: string().when('imageFile', {
|
||||||
.required(
|
is: (imageFile: string) => !imageFile || imageFile.length === 0,
|
||||||
|
then: string().required(
|
||||||
t('components.wishlist-header.main.form.image-src.error-requried')
|
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']
|
||||||
)
|
)
|
||||||
.url(t('components.wishlist-header.main.form.image-src.error-url')),
|
|
||||||
})
|
|
||||||
|
|
||||||
const onSubmit = async (values: any): Promise<void> => {
|
const onSubmit = async (values: any): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
values.imageSrc = values.imageFile || values.imageSrc
|
||||||
await update(values)
|
await update(values)
|
||||||
toast.success(t('common.wishlist.saved.text'))
|
toast.success(t('common.wishlist.saved.text'))
|
||||||
router.push(`/${values.slugUrlText}`)
|
router.push(`/${values.slugUrlText}`)
|
||||||
|
|
|
@ -4,6 +4,7 @@ export { default as ImagePreview } from './ImagePreview.vue'
|
||||||
export { default as ImageTile } from './ImageTile.vue'
|
export { default as ImageTile } from './ImageTile.vue'
|
||||||
export { default as InputCheckbox } from './InputCheckbox.vue'
|
export { default as InputCheckbox } from './InputCheckbox.vue'
|
||||||
export { default as InputText } from './InputText.vue'
|
export { default as InputText } from './InputText.vue'
|
||||||
|
export { default as InputFile } from './InputFile.vue'
|
||||||
export { default as InputTextArea } from './InputTextArea.vue'
|
export { default as InputTextArea } from './InputTextArea.vue'
|
||||||
export { default as Modal } from './Modal.vue'
|
export { default as Modal } from './Modal.vue'
|
||||||
export { default as WishlistHeader } from './WishlistHeader.vue'
|
export { default as WishlistHeader } from './WishlistHeader.vue'
|
||||||
|
|
|
@ -98,8 +98,13 @@
|
||||||
},
|
},
|
||||||
"image-src": {
|
"image-src": {
|
||||||
"label": "Bild-URL",
|
"label": "Bild-URL",
|
||||||
"error-requried": "Bild-URL wird benötigt",
|
"error-requried": "Bild-URL wird benötigt"
|
||||||
"error-url": "Bild-URL muss eine gültige URL sein"
|
},
|
||||||
|
"image-file": {
|
||||||
|
"label": "Bild-Datei",
|
||||||
|
"text-dropzone-link": "hier",
|
||||||
|
"text-dropzone": "Ziehen Sie ein beliebiges Bild hierher oder klicken Sie {0}, um den Dialog zu öffnen.",
|
||||||
|
"error-requried": "Bild-Datei wird benötigt"
|
||||||
},
|
},
|
||||||
"submit": {
|
"submit": {
|
||||||
"text": "Speichern"
|
"text": "Speichern"
|
||||||
|
|
|
@ -96,8 +96,13 @@
|
||||||
},
|
},
|
||||||
"image-src": {
|
"image-src": {
|
||||||
"label": "Image-URL",
|
"label": "Image-URL",
|
||||||
"error-requried": "Image-URL is required",
|
"error-requried": "Image-URL is required"
|
||||||
"error-url": "Image-URL has to be a valid url"
|
},
|
||||||
|
"image-file": {
|
||||||
|
"label": "Image-File",
|
||||||
|
"text-dropzone-link": "click here",
|
||||||
|
"text-dropzone": "drag and drop any image here or {0} to open dialog.",
|
||||||
|
"error-requried": "Image-File is required"
|
||||||
},
|
},
|
||||||
"submit": {
|
"submit": {
|
||||||
"text": "Save"
|
"text": "Save"
|
||||||
|
|
Loading…
Add table
Reference in a new issue