feat: display product options

This commit is contained in:
Nikita Kiselev
2025-07-15 19:52:04 +03:00
parent 08d2453df9
commit f47bb46751
17 changed files with 678 additions and 53 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<FullscreenViewport/>
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'"/>
<router-view />
</div>
</template>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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) {

View File

@@ -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()