feat: display product options
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<FullscreenViewport/>
|
||||
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'"/>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
20
spa/src/components/ProductOptions/ProductOptions.vue
Normal file
20
spa/src/components/ProductOptions/ProductOptions.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div v-for="option in options" :key="option.product_option_id" class="mt-3">
|
||||
<OptionRadio v-if="option.type === 'radio'" :modelValue="option"/>
|
||||
<OptionCheckbox v-else-if="option.type === 'checkbox'" :modelValue="option"/>
|
||||
<OptionText v-else-if="option.type === 'text'" :modelValue="option"/>
|
||||
<OptionTextarea v-else-if="option.type === 'textarea'" :modelValue="option"/>
|
||||
<OptionSelect v-else-if="option.type === 'select'" :modelValue="option"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OptionRadio from "./Types/OptionRadio.vue";
|
||||
import OptionCheckbox from "./Types/OptionCheckbox.vue";
|
||||
import OptionText from "./Types/OptionText.vue";
|
||||
import OptionTextarea from "./Types/OptionTextarea.vue";
|
||||
import OptionSelect from "./Types/OptionSelect.vue";
|
||||
|
||||
const options = defineModel();
|
||||
|
||||
</script>
|
||||
41
spa/src/components/ProductOptions/Types/OptionCheckbox.vue
Normal file
41
spa/src/components/ProductOptions/Types/OptionCheckbox.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<OptionTemplate :name="model.name" :required="model.required">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label
|
||||
v-for="value in model.values"
|
||||
class="group relative flex items-center justify-center rounded-md border border-gray-300 bg-white p-2 has-checked:border-indigo-600 has-checked:bg-indigo-600 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-indigo-600 has-disabled:border-gray-400 has-disabled:bg-gray-200 has-disabled:opacity-25">
|
||||
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="value.product_option_value_id"
|
||||
:checked="value.selected"
|
||||
@change="select(value)"
|
||||
class="absolute inset-0 appearance-none focus:outline-none disabled:cursor-not-allowed"
|
||||
/>
|
||||
|
||||
<span class="text-xs font-medium group-has-checked:text-white">
|
||||
{{ value.name }}<span v-if="value.price"> ({{ value.price_prefix }}{{ value.price }})</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</OptionTemplate>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OptionTemplate from "./OptionTemplate.vue";
|
||||
|
||||
const model = defineModel();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
function select(toggledValue) {
|
||||
model.value.values.forEach(value => {
|
||||
if (value === toggledValue) {
|
||||
value.selected = !value.selected;
|
||||
}
|
||||
});
|
||||
|
||||
emit('update:modelValue', model.value);
|
||||
}
|
||||
</script>
|
||||
38
spa/src/components/ProductOptions/Types/OptionRadio.vue
Normal file
38
spa/src/components/ProductOptions/Types/OptionRadio.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<OptionTemplate :name="model.name" :required="model.required">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label
|
||||
v-for="value in model.values"
|
||||
class="group relative flex items-center justify-center rounded-md border border-gray-300 bg-base-200 p-2 has-checked:border-indigo-600 has-checked:bg-primary has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-indigo-600 has-disabled:border-gray-400 has-disabled:bg-gray-200 has-disabled:opacity-25">
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
:name="`option-${model.product_option_id}`"
|
||||
:value="value.product_option_value_id"
|
||||
:checked="value.selected"
|
||||
@change="select(value)"
|
||||
class="absolute inset-0 appearance-none focus:outline-none disabled:cursor-not-allowed"
|
||||
/>
|
||||
|
||||
<span class="text-xs font-medium group-has-checked:text-white">
|
||||
{{ value.name }}<span v-if="value.price"> ({{ value.price_prefix }}{{ value.price }})</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</OptionTemplate>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OptionTemplate from "./OptionTemplate.vue";
|
||||
|
||||
const model = defineModel();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
function select(selectedValue) {
|
||||
model.value.values.forEach(value => {
|
||||
value.selected = (value === selectedValue);
|
||||
});
|
||||
|
||||
emit('update:modelValue', model);
|
||||
}
|
||||
</script>
|
||||
35
spa/src/components/ProductOptions/Types/OptionSelect.vue
Normal file
35
spa/src/components/ProductOptions/Types/OptionSelect.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<OptionTemplate :name="model.name" :required="model.required">
|
||||
<select
|
||||
:name="`option-${model.product_option_id}`"
|
||||
class="select"
|
||||
@change="onChange"
|
||||
>
|
||||
<option
|
||||
v-for="value in model.values"
|
||||
:key="value.product_option_value_id"
|
||||
:value="value.product_option_value_id"
|
||||
:selected="value.selected"
|
||||
>
|
||||
{{ value.name }}<span v-if="value.price"> ({{ value.price_prefix }}{{ value.price }})</span>
|
||||
</option>
|
||||
</select>
|
||||
</OptionTemplate>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OptionTemplate from "./OptionTemplate.vue";
|
||||
|
||||
const model = defineModel();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
function onChange(event) {
|
||||
const selectedId = Number(event.target.value);
|
||||
|
||||
model.value.values.forEach(value => {
|
||||
value.selected = (value.product_option_value_id === selectedId);
|
||||
});
|
||||
|
||||
emit('update:modelValue', model.value);
|
||||
}
|
||||
</script>
|
||||
25
spa/src/components/ProductOptions/Types/OptionTemplate.vue
Normal file
25
spa/src/components/ProductOptions/Types/OptionTemplate.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 class="text-sm mb-2">
|
||||
{{ name }} <span v-if="required" class="text-red-500">*</span>
|
||||
</h3>
|
||||
|
||||
<fieldset>
|
||||
<slot></slot>
|
||||
</fieldset>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
</script>
|
||||
23
spa/src/components/ProductOptions/Types/OptionText.vue
Normal file
23
spa/src/components/ProductOptions/Types/OptionText.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<OptionTemplate :name="model.name" :required="model.required">
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="model.name"
|
||||
:value="model.value"
|
||||
@input="input(model, $event.target.value)"
|
||||
/>
|
||||
</OptionTemplate>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OptionTemplate from "./OptionTemplate.vue";
|
||||
|
||||
const model = defineModel();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
function input(model, newValue) {
|
||||
model.value = newValue;
|
||||
emit('update:modelValue', model);
|
||||
}
|
||||
</script>
|
||||
23
spa/src/components/ProductOptions/Types/OptionTextarea.vue
Normal file
23
spa/src/components/ProductOptions/Types/OptionTextarea.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<OptionTemplate :name="model.name" :required="model.required">
|
||||
<textarea
|
||||
type="text"
|
||||
class="textarea"
|
||||
:placeholder="model.name"
|
||||
v-text="model.value"
|
||||
@input="input(model, $event.target.value)"
|
||||
/>
|
||||
</OptionTemplate>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OptionTemplate from "./OptionTemplate.vue";
|
||||
|
||||
const model = defineModel();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
function input(model, newValue) {
|
||||
model.value = newValue;
|
||||
emit('update:modelValue', model);
|
||||
}
|
||||
</script>
|
||||
@@ -1,4 +1,4 @@
|
||||
import {createRouter, createWebHistory} from 'vue-router';
|
||||
import {createMemoryHistory, createRouter} from 'vue-router';
|
||||
import Home from './views/Home.vue';
|
||||
import Product from './views/Product.vue';
|
||||
import CategoriesList from "./views/CategoriesList.vue";
|
||||
@@ -12,7 +12,7 @@ const routes = [
|
||||
];
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory('/image/catalog/tgshopspa/'),
|
||||
history: createMemoryHistory('/image/catalog/tgshopspa/'),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
|
||||
@@ -20,11 +20,14 @@
|
||||
<h3 class="text-sm font-medium">{{ product.manufacturer }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="mt-4 lg:row-span-3 lg:mt-0">
|
||||
<p class="text-3xl tracking-tight">{{ product.price }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="product.options && product.options.length" class="mt-4">
|
||||
<ProductOptions v-model="product.options"/>
|
||||
</div>
|
||||
|
||||
<div class="py-10 lg:col-span-2 lg:col-start-1 lg:border-r lg:border-gray-200 lg:pt-6 lg:pr-8 lg:pb-16">
|
||||
<!-- Description and details -->
|
||||
<div>
|
||||
@@ -34,7 +37,6 @@
|
||||
<p class="text-base" v-html="product.description"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,6 +49,7 @@ import {$fetch} from "ofetch";
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {useHapticFeedback} from 'vue-tg';
|
||||
import ProductOptions from "../components/ProductOptions/ProductOptions.vue";
|
||||
const hapticFeedback = useHapticFeedback();
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
Reference in New Issue
Block a user