mirror of
https://github.com/ThisIsBenny/wishlist-app.git
synced 2025-06-07 05:57:41 +00:00
#2 edit mode for wishlist
Signed-off-by: Benny Samir Hierl <bennysamir@posteo.de>
This commit is contained in:
parent
d0165ff296
commit
b7ec02e5e8
12 changed files with 293 additions and 19 deletions
|
@ -30,6 +30,7 @@ export const wishlistRequestSchema = {
|
||||||
required: ['title', 'imageSrc', 'slugUrlText'],
|
required: ['title', 'imageSrc', 'slugUrlText'],
|
||||||
properties: {
|
properties: {
|
||||||
title: { type: 'string' },
|
title: { type: 'string' },
|
||||||
|
public: { type: 'boolean' },
|
||||||
imageSrc: { type: 'string' },
|
imageSrc: { type: 'string' },
|
||||||
description: { type: 'string' },
|
description: { type: 'string' },
|
||||||
slugUrlText: { type: 'string' },
|
slugUrlText: { type: 'string' },
|
||||||
|
@ -40,6 +41,7 @@ export const wishlistResponseSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: 'string' },
|
id: { type: 'string' },
|
||||||
title: { type: 'string' },
|
title: { type: 'string' },
|
||||||
|
public: { type: 'boolean' },
|
||||||
imageSrc: { type: 'string' },
|
imageSrc: { type: 'string' },
|
||||||
description: { type: 'string' },
|
description: { type: 'string' },
|
||||||
slugUrlText: { type: 'string' },
|
slugUrlText: { type: 'string' },
|
||||||
|
|
|
@ -37,7 +37,7 @@ import { useAuth, useEditMode } from '@/composables/'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { isAuthenticated, setToken } = useAuth()
|
const { isAuthenticated, setToken } = useAuth()
|
||||||
const { editMode, toggle } = useEditMode()
|
const { state: editMode, toggle } = useEditMode()
|
||||||
|
|
||||||
const toggleDark = useToggle(useDark())
|
const toggleDark = useToggle(useDark())
|
||||||
</script>
|
</script>
|
||||||
|
|
37
src/components/InputCheckbox.vue
Normal file
37
src/components/InputCheckbox.vue
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative mb-8">
|
||||||
|
<label class="mb-1 block w-full" :for="name">{{ label }}</label>
|
||||||
|
<input
|
||||||
|
class="border-2 border-solid border-stone-300 bg-transparent px-2 outline-none dark:border-stone-700"
|
||||||
|
:name="name"
|
||||||
|
:id="name"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="checked"
|
||||||
|
@change="handleChange((checked = !checked))"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useField } from 'vee-validate'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { checked, handleChange } = useField(props.name, undefined, {
|
||||||
|
type: 'checkbox',
|
||||||
|
checkedValue: props.value,
|
||||||
|
initialValue: props.value,
|
||||||
|
})
|
||||||
|
</script>
|
60
src/components/InputTextArea.vue
Normal file
60
src/components/InputTextArea.vue
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative mb-8">
|
||||||
|
<label class="mb-1 block w-full" :for="name">{{ label }}</label>
|
||||||
|
<textarea
|
||||||
|
class="w-full rounded-md border-2 border-solid border-stone-300 bg-transparent px-2 outline-none dark:border-stone-700"
|
||||||
|
:class="[heightClass, !!errorMessage ? 'border-rose-500' : '']"
|
||||||
|
:name="name"
|
||||||
|
:id="name"
|
||||||
|
:type="type"
|
||||||
|
:value="inputValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@input="handleChange"
|
||||||
|
@blur="handleBlur"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'text',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
heightClass: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
value: inputValue,
|
||||||
|
errorMessage,
|
||||||
|
handleBlur,
|
||||||
|
handleChange,
|
||||||
|
} = useField(props.name, undefined, {
|
||||||
|
initialValue: props.value,
|
||||||
|
})
|
||||||
|
</script>
|
106
src/components/WishlistHeader.vue
Normal file
106
src/components/WishlistHeader.vue
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<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')"
|
||||||
|
/>
|
||||||
|
<InputText
|
||||||
|
name="slugUrlText"
|
||||||
|
type="text"
|
||||||
|
:value="modelValue.slugUrlText"
|
||||||
|
:label="t('components.wishlist-header.main.form.slug-text.label')"
|
||||||
|
/>
|
||||||
|
<BaseButton class="h-12 w-full" mode="primary" :disabled="!meta.valid">{{
|
||||||
|
t('components.wishlist-header.main.form.submit.text')
|
||||||
|
}}</BaseButton>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Form } from 'vee-validate'
|
||||||
|
import { object, string, boolean } from 'yup'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import ImageTile from '@/components/ImageTile.vue'
|
||||||
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import InputText from '@/components/InputText.vue'
|
||||||
|
import InputCheckbox from '@/components/InputCheckbox.vue'
|
||||||
|
import InputTextArea from '@/components/InputTextArea.vue'
|
||||||
|
import { useEditMode, useWishlistStore } from '@/composables'
|
||||||
|
import { Wishlist } from '@/types'
|
||||||
|
import { PropType } from 'vue'
|
||||||
|
const { isActive: editModeIsActive } = useEditMode()
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object as PropType<Wishlist>,
|
||||||
|
requried: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { update } = useWishlistStore()
|
||||||
|
|
||||||
|
const schema = object({
|
||||||
|
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()
|
||||||
|
.required(
|
||||||
|
t('components.wishlist-header.main.form.image-src.error-requried')
|
||||||
|
)
|
||||||
|
.url(t('components.wishlist-header.main.form.image-src.error-url')),
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = async (values: any): Promise<void> => {
|
||||||
|
console.log(values)
|
||||||
|
await update(values)
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,5 +1,7 @@
|
||||||
import { ref, readonly } from 'vue'
|
import { useAuth } from './useAuth'
|
||||||
|
import { ref, readonly, computed } from 'vue'
|
||||||
|
|
||||||
|
const { isAuthenticated } = useAuth()
|
||||||
const state = ref(false)
|
const state = ref(false)
|
||||||
|
|
||||||
const activate = (): void => {
|
const activate = (): void => {
|
||||||
|
@ -14,9 +16,12 @@ const toggle = (): void => {
|
||||||
state.value = !state.value
|
state.value = !state.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isActive = computed(() => state.value && isAuthenticated.value)
|
||||||
|
|
||||||
export const useEditMode = () => {
|
export const useEditMode = () => {
|
||||||
return {
|
return {
|
||||||
editMode: readonly(state),
|
state: readonly(state),
|
||||||
|
isActive,
|
||||||
activate,
|
activate,
|
||||||
deactivate,
|
deactivate,
|
||||||
toggle,
|
toggle,
|
||||||
|
|
|
@ -16,6 +16,25 @@ const fetch = async (slugText: string): Promise<void> => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const update = async (updatedData: Wishlist): Promise<void> => {
|
||||||
|
const id = state.value?.id
|
||||||
|
const payload = {
|
||||||
|
...state.value,
|
||||||
|
...updatedData,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { data } = await client.put(`/wishlist/${id}`, payload)
|
||||||
|
state.value = {
|
||||||
|
...state.value,
|
||||||
|
...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
|
||||||
|
@ -25,6 +44,7 @@ export const useWishlistStore = () => {
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
fetch,
|
fetch,
|
||||||
|
update,
|
||||||
itemBought,
|
itemBought,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,35 @@
|
||||||
"text": "Gekauft"
|
"text": "Gekauft"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"wishlist-header": {
|
||||||
|
"main": {
|
||||||
|
"form": {
|
||||||
|
"title": {
|
||||||
|
"label": "Titel",
|
||||||
|
"error-requried": "Titel wird benötigt"
|
||||||
|
},
|
||||||
|
"public": {
|
||||||
|
"label": "Auf der Startseite anzeigen?"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"label": "Beschreibung",
|
||||||
|
"error-max": "Die maximale Länge beträgt 300 Zeichen"
|
||||||
|
},
|
||||||
|
"slug-text": {
|
||||||
|
"label": "URL Slug-Text",
|
||||||
|
"error-requried": "URL Slug-Text wird benötigt"
|
||||||
|
},
|
||||||
|
"image-src": {
|
||||||
|
"label": "Bild-URL",
|
||||||
|
"error-requried": "Bild-URL wird benötigt",
|
||||||
|
"error-url": "Bild-URL muss eine gültige URL sein"
|
||||||
|
},
|
||||||
|
"submit": {
|
||||||
|
"text": "Speichern"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"edit-mode": {
|
"edit-mode": {
|
||||||
"text": "Bearbeitungsmodus"
|
"text": "Bearbeitungsmodus"
|
||||||
|
|
|
@ -70,6 +70,33 @@
|
||||||
"text": "Bought"
|
"text": "Bought"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"wishlist-header": {
|
||||||
|
"main": {
|
||||||
|
"form": {
|
||||||
|
"title": {
|
||||||
|
"label": "Title"
|
||||||
|
},
|
||||||
|
"public": {
|
||||||
|
"label": "Show on startpage?"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"label": "Description",
|
||||||
|
"error-max": "The max. length is 300 chars."
|
||||||
|
},
|
||||||
|
"slug-text": {
|
||||||
|
"label": "URL Slug-Text"
|
||||||
|
},
|
||||||
|
"image-src": {
|
||||||
|
"label": "Image-URL",
|
||||||
|
"error-requried": "Image-URL is required",
|
||||||
|
"error-url": "Image-URL has to be a valid url"
|
||||||
|
},
|
||||||
|
"submit": {
|
||||||
|
"text": "Save"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"edit-mode": {
|
"edit-mode": {
|
||||||
"text": "Edit-Mode"
|
"text": "Edit-Mode"
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { WishlistItem as WishlistItemType } from '@/types'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useWishlistStore, useModal } from '@/composables'
|
import { useWishlistStore, useModal } from '@/composables'
|
||||||
import ImageTile from '@/components/ImageTile.vue'
|
|
||||||
import WishlistItem from '@/components/WishlistItem.vue'
|
import WishlistItem from '@/components/WishlistItem.vue'
|
||||||
|
import WishlistHeader from '@/components/WishlistHeader.vue'
|
||||||
import { IconNoGift } from '../components/icons'
|
import { IconNoGift } from '../components/icons'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
@ -37,19 +37,7 @@ const bought = async (item: WishlistItemType): Promise<void> => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="state !== null" class="h-full">
|
<div v-if="state !== null" class="h-full">
|
||||||
<div
|
<WishlistHeader v-model="state" />
|
||||||
class="relative flex flex-col items-center space-x-0 space-y-2 md:flex-row md:space-x-6 md:space-y-0"
|
|
||||||
>
|
|
||||||
<ImageTile :image-src="state.imageSrc" class="shrink-0"></ImageTile>
|
|
||||||
<div>
|
|
||||||
<h1 class="mb-2 text-center text-2xl font-bold md:text-left">
|
|
||||||
{{ state.title }}
|
|
||||||
</h1>
|
|
||||||
<p class="text-lg">
|
|
||||||
{{ state.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="notBoughtItems && notBoughtItems.length > 0"
|
v-if="notBoughtItems && notBoughtItems.length > 0"
|
||||||
class="flex flex-col space-y-14 py-10 md:space-y-8"
|
class="flex flex-col space-y-14 py-10 md:space-y-8"
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Form } from 'vee-validate'
|
||||||
import { object, string } from 'yup'
|
import { object, string } from 'yup'
|
||||||
import { useAuth } from '@/composables'
|
import { useAuth } from '@/composables'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
import TextInput from '@/components/TextInput.vue'
|
import InputText from '@/components/InputText.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { setToken } = useAuth()
|
const { setToken } = useAuth()
|
||||||
|
@ -38,7 +38,7 @@ const onSubmit = (values: any): void => {
|
||||||
v-slot="{ meta }"
|
v-slot="{ meta }"
|
||||||
class="w-full flex-col space-y-3"
|
class="w-full flex-col space-y-3"
|
||||||
>
|
>
|
||||||
<TextInput
|
<InputText
|
||||||
name="api-key"
|
name="api-key"
|
||||||
type="text"
|
type="text"
|
||||||
:label="t('pages.login-view.main.form.api-key.placeholder')"
|
:label="t('pages.login-view.main.form.api-key.placeholder')"
|
||||||
|
|
Loading…
Add table
Reference in a new issue