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'],
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
public: { type: 'boolean' },
|
||||
imageSrc: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
slugUrlText: { type: 'string' },
|
||||
|
@ -40,6 +41,7 @@ export const wishlistResponseSchema = {
|
|||
properties: {
|
||||
id: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
public: { type: 'boolean' },
|
||||
imageSrc: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
slugUrlText: { type: 'string' },
|
||||
|
|
|
@ -37,7 +37,7 @@ import { useAuth, useEditMode } from '@/composables/'
|
|||
|
||||
const { t } = useI18n()
|
||||
const { isAuthenticated, setToken } = useAuth()
|
||||
const { editMode, toggle } = useEditMode()
|
||||
const { state: editMode, toggle } = useEditMode()
|
||||
|
||||
const toggleDark = useToggle(useDark())
|
||||
</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 activate = (): void => {
|
||||
|
@ -14,9 +16,12 @@ const toggle = (): void => {
|
|||
state.value = !state.value
|
||||
}
|
||||
|
||||
const isActive = computed(() => state.value && isAuthenticated.value)
|
||||
|
||||
export const useEditMode = () => {
|
||||
return {
|
||||
editMode: readonly(state),
|
||||
state: readonly(state),
|
||||
isActive,
|
||||
activate,
|
||||
deactivate,
|
||||
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> => {
|
||||
await client.post(`/wishlist/${item.wishlistId}/item/${item.id}/bought`)
|
||||
item.bought = true
|
||||
|
@ -25,6 +44,7 @@ export const useWishlistStore = () => {
|
|||
return {
|
||||
state,
|
||||
fetch,
|
||||
update,
|
||||
itemBought,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,35 @@
|
|||
"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": {
|
||||
"edit-mode": {
|
||||
"text": "Bearbeitungsmodus"
|
||||
|
|
|
@ -70,6 +70,33 @@
|
|||
"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": {
|
||||
"edit-mode": {
|
||||
"text": "Edit-Mode"
|
||||
|
|
|
@ -4,8 +4,8 @@ import { WishlistItem as WishlistItemType } from '@/types'
|
|||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useWishlistStore, useModal } from '@/composables'
|
||||
import ImageTile from '@/components/ImageTile.vue'
|
||||
import WishlistItem from '@/components/WishlistItem.vue'
|
||||
import WishlistHeader from '@/components/WishlistHeader.vue'
|
||||
import { IconNoGift } from '../components/icons'
|
||||
|
||||
const route = useRoute()
|
||||
|
@ -37,19 +37,7 @@ const bought = async (item: WishlistItemType): Promise<void> => {
|
|||
|
||||
<template>
|
||||
<div v-if="state !== null" class="h-full">
|
||||
<div
|
||||
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>
|
||||
<WishlistHeader v-model="state" />
|
||||
<div
|
||||
v-if="notBoughtItems && notBoughtItems.length > 0"
|
||||
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 { useAuth } from '@/composables'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import TextInput from '@/components/TextInput.vue'
|
||||
import InputText from '@/components/InputText.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const { setToken } = useAuth()
|
||||
|
@ -38,7 +38,7 @@ const onSubmit = (values: any): void => {
|
|||
v-slot="{ meta }"
|
||||
class="w-full flex-col space-y-3"
|
||||
>
|
||||
<TextInput
|
||||
<InputText
|
||||
name="api-key"
|
||||
type="text"
|
||||
:label="t('pages.login-view.main.form.api-key.placeholder')"
|
||||
|
|
Loading…
Add table
Reference in a new issue