feat: display product options
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Adapters\OcImageTool;
|
||||
use App\Adapters\OcModelCatalogProductAdapter;
|
||||
use App\ApplicationFactory;
|
||||
use Cart\Currency;
|
||||
use Cart\Tax;
|
||||
@@ -20,9 +22,8 @@ class Controllerextensiontgshophandle extends Controller
|
||||
{
|
||||
$app = ApplicationFactory::create([
|
||||
'oc_config_tax' => $this->config->get('config_tax'),
|
||||
'oc_currency' => $this->session->data['currency'],
|
||||
'oc_default_currency' => $this->config->get('config_currency'),
|
||||
'timezone' => $this->config->get('config_timezone', 'UTC'),
|
||||
'lang' => $this->config->get('config_admin_language'),
|
||||
'language_id' => (int)$this->config->get('config_language_id'),
|
||||
'shop_base_url' => HTTPS_SERVER,
|
||||
'dir_image' => DIR_IMAGE,
|
||||
@@ -42,18 +43,21 @@ class Controllerextensiontgshophandle extends Controller
|
||||
return $this->url;
|
||||
});
|
||||
|
||||
$app->bind(Currency::class, function () { return $this->currency; });
|
||||
$app->bind(Tax::class, function () { return $this->tax; });
|
||||
$app->bind(Currency::class, function () {
|
||||
return $this->currency;
|
||||
});
|
||||
$app->bind(Tax::class, function () {
|
||||
return $this->tax;
|
||||
});
|
||||
|
||||
$app->bind(OcModelCatalogProductAdapter::class, function () {
|
||||
$this->load->model('catalog/product');
|
||||
return new OcModelCatalogProductAdapter($this->model_catalog_product);
|
||||
});
|
||||
|
||||
$app->bind(OcImageTool::class, function () {
|
||||
$this->load->model('tool/image');
|
||||
$app->bind('image_resize', function () {
|
||||
return function ($path, $width, $height) {
|
||||
if (is_file(DIR_IMAGE . $path)) {
|
||||
return $this->model_tool_image->resize($path, $width, $height);
|
||||
}
|
||||
|
||||
return $this->model_tool_image->resize('no_image.png', $width, $height);
|
||||
};
|
||||
return new OcImageTool($this->model_tool_image);
|
||||
});
|
||||
|
||||
$this->load->model('checkout/order');
|
||||
@@ -62,6 +66,8 @@ class Controllerextensiontgshophandle extends Controller
|
||||
return $this->model_checkout_order;
|
||||
});
|
||||
|
||||
$this->session->data['language'] = $this->config->get('config_language');
|
||||
|
||||
$app->bootAndHandleRequest();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
namespace Openguru\OpenCartFramework\Collection;
|
||||
|
||||
use ArrayAccess;
|
||||
use InvalidArgumentException;
|
||||
use ArrayIterator;
|
||||
use IteratorAggregate;
|
||||
use Countable;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* A generic collection for managing objects of a specific type.
|
||||
*
|
||||
* @template T
|
||||
* @implements IteratorAggregate<T>
|
||||
*/
|
||||
class Collection implements IteratorAggregate, Countable, ArrayAccess, JsonSerializable
|
||||
{
|
||||
/** @var array<T> */
|
||||
protected array $items;
|
||||
|
||||
/** @var string|null */
|
||||
protected ?string $expectedClass;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $items Initial items for the collection.
|
||||
* @param string|null $expectedClass The expected class type for the items.
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $items = [], ?string $expectedClass = null)
|
||||
{
|
||||
$this->expectedClass = $expectedClass;
|
||||
|
||||
foreach ($items as $item) {
|
||||
$this->validateItem($item);
|
||||
}
|
||||
|
||||
$this->items = $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the collection.
|
||||
*
|
||||
* @param mixed $item
|
||||
* @return self
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function add($item): self
|
||||
{
|
||||
$this->validateItem($item);
|
||||
$this->items[] = $item;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all items.
|
||||
*
|
||||
* @return array<T>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items based on a callback.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return self
|
||||
*/
|
||||
public function filter(callable $callback): self
|
||||
{
|
||||
return new self(array_values(array_filter($this->items, $callback)), $this->expectedClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a callback to each item and return a new collection.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return array
|
||||
*/
|
||||
public function map(callable $callback): array
|
||||
{
|
||||
return array_map($callback, $this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the collection to a single value.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @param mixed $initial
|
||||
* @return mixed
|
||||
*/
|
||||
public function reduce(callable $callback, $initial)
|
||||
{
|
||||
return array_reduce($this->items, $callback, $initial);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an item by a specific property and value.
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @return T|null
|
||||
*/
|
||||
public function findByProperty(string $property, $value)
|
||||
{
|
||||
foreach ($this->items as $item) {
|
||||
if (is_object($item) && property_exists($item, $property) && $item->$property === $value) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item exists with a specific property and value.
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function hasValue(string $property, $value): bool
|
||||
{
|
||||
return $this->findByProperty($property, $value) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an item type.
|
||||
*
|
||||
* @param mixed $item
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function validateItem($item): void
|
||||
{
|
||||
if ($this->expectedClass !== null && !($item instanceof $this->expectedClass)) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Item must be an instance of %s, %s given.',
|
||||
$this->expectedClass,
|
||||
is_object($item) ? get_class($item) : gettype($item)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterator for the collection.
|
||||
*/
|
||||
public function getIterator(): ArrayIterator
|
||||
{
|
||||
return new ArrayIterator($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of items in the collection.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an offset exists.
|
||||
*
|
||||
* @param mixed $offset
|
||||
* @return bool
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->items[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item at a specific offset.
|
||||
*
|
||||
* @param mixed $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
return $this->items[$offset] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an item at a specific offset.
|
||||
*
|
||||
* @param mixed $offset
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
$this->validateItem($value);
|
||||
|
||||
if ($offset === null) {
|
||||
$this->items[] = $value;
|
||||
} else {
|
||||
$this->items[$offset] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset an item at a specific offset.
|
||||
*
|
||||
* @param mixed $offset
|
||||
*/
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->items[$offset]);
|
||||
}
|
||||
|
||||
public function getValueByProperty(string $property, $propertyValue)
|
||||
{
|
||||
$item = $this->findByProperty($property, $propertyValue);
|
||||
|
||||
return $item ? $item->getValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return T|null
|
||||
*/
|
||||
public function first()
|
||||
{
|
||||
return $this->items[0] ?? null;
|
||||
}
|
||||
|
||||
public function getFirstItemValue(string $field, $default = null)
|
||||
{
|
||||
if ($this->first() === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $this->first()->{$field};
|
||||
}
|
||||
|
||||
public function groupBy(callable $callback): array
|
||||
{
|
||||
$grouped = [];
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
$key = $callback($item);
|
||||
$grouped[$key][] = $item;
|
||||
}
|
||||
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
public function keyByField(callable $callback): array
|
||||
{
|
||||
$items = [];
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
$key = $callback($item);
|
||||
$items[$key] = $item;
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->all();
|
||||
}
|
||||
|
||||
public function pluck(string $field): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
if (is_object($item)) {
|
||||
$result[] = $item->$field;
|
||||
} else {
|
||||
$result[] = $item[$field];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->all();
|
||||
}
|
||||
|
||||
public function append(array $items): Collection
|
||||
{
|
||||
return new Collection(array_merge($this->items, $items), $this->expectedClass);
|
||||
}
|
||||
}
|
||||
30
module/oc_telegram_shop/upload/oc_telegram_shop/src/Adapters/OcImageTool.php
Executable file
30
module/oc_telegram_shop/upload/oc_telegram_shop/src/Adapters/OcImageTool.php
Executable file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Adapters;
|
||||
|
||||
use Proxy;
|
||||
use RuntimeException;
|
||||
|
||||
class OcImageTool
|
||||
{
|
||||
private Proxy $image;
|
||||
|
||||
public function __construct(Proxy $image)
|
||||
{
|
||||
$this->image = $image;
|
||||
}
|
||||
|
||||
public function resize(string $path, int $width, int $height, ?string $default = null): ?string
|
||||
{
|
||||
if ($path && is_file(DIR_IMAGE . $path)) {
|
||||
$image = $path;
|
||||
return $this->image->resize($image, $width, $height);
|
||||
}
|
||||
|
||||
if ($default && is_file(DIR_IMAGE . $default)) {
|
||||
return $this->image->resize($default, $width, $height);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Adapters;
|
||||
|
||||
use ModelCatalogProduct;
|
||||
use Proxy;
|
||||
|
||||
class OcModelCatalogProductAdapter
|
||||
{
|
||||
/** @var Proxy|ModelCatalogProduct */
|
||||
private Proxy $model;
|
||||
|
||||
public function __construct(Proxy $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
public function getProductOptions(int $productId): array
|
||||
{
|
||||
return $this->model->getProductOptions($productId);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use Closure;
|
||||
use Openguru\OpenCartFramework\Application;
|
||||
use App\Adapters\OcImageTool;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
||||
@@ -12,10 +11,12 @@ use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
||||
class CategoriesHandler
|
||||
{
|
||||
private Builder $queryBuilder;
|
||||
private OcImageTool $imageTool;
|
||||
|
||||
public function __construct(Builder $queryBuilder)
|
||||
public function __construct(Builder $queryBuilder, OcImageTool $ocImageTool)
|
||||
{
|
||||
$this->queryBuilder = $queryBuilder;
|
||||
$this->ocImageTool = $ocImageTool;
|
||||
}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
@@ -47,16 +48,9 @@ class CategoriesHandler
|
||||
->limit($perPage)
|
||||
->get();
|
||||
|
||||
/** @var Closure $resize */
|
||||
$resize = Application::getInstance()->get('image_resize');
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => array_map(static function ($category) use ($resize, $imageWidth, $imageHeight) {
|
||||
if ($category['image']) {
|
||||
$image = $resize($category['image'], $imageWidth, $imageHeight);
|
||||
} else {
|
||||
$image = $resize('no_image.png', $imageWidth, $imageHeight);
|
||||
}
|
||||
'data' => array_map(function ($category) use ($imageWidth, $imageHeight) {
|
||||
$image = $this->ocImageTool->resize($category['image'], $imageWidth, $imageHeight, 'no_image.png');
|
||||
|
||||
return [
|
||||
'id' => (int)$category['id'],
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Handlers;
|
||||
|
||||
use App\Adapters\OcImageTool;
|
||||
use App\Adapters\OcModelCatalogProductAdapter;
|
||||
use Cart\Currency;
|
||||
use Cart\Tax;
|
||||
use Closure;
|
||||
use Openguru\OpenCartFramework\Application;
|
||||
use Openguru\OpenCartFramework\Config\Settings;
|
||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||
use Openguru\OpenCartFramework\Http\Request;
|
||||
use Openguru\OpenCartFramework\Http\Response;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
||||
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
||||
use Openguru\OpenCartFramework\Support\Arr;
|
||||
@@ -19,13 +20,23 @@ class ProductsHandler
|
||||
private Currency $currency;
|
||||
private Tax $tax;
|
||||
private Settings $settings;
|
||||
private OcModelCatalogProductAdapter $ocModelCatalogProduct;
|
||||
private OcImageTool $ocImageTool;
|
||||
|
||||
public function __construct(Builder $queryBuilder, Currency $currency, Tax $tax, Settings $settings)
|
||||
{
|
||||
public function __construct(
|
||||
Builder $queryBuilder,
|
||||
Currency $currency,
|
||||
Tax $tax,
|
||||
Settings $settings,
|
||||
OcModelCatalogProductAdapter $ocModelCatalogProduct,
|
||||
OcImageTool $ocImageTool
|
||||
) {
|
||||
$this->queryBuilder = $queryBuilder;
|
||||
$this->currency = $currency;
|
||||
$this->tax = $tax;
|
||||
$this->settings = $settings;
|
||||
$this->ocModelCatalogProduct = $ocModelCatalogProduct;
|
||||
$this->ocImageTool = $ocImageTool;
|
||||
}
|
||||
|
||||
public function handle(Request $request): JsonResponse
|
||||
@@ -93,26 +104,24 @@ class ProductsHandler
|
||||
->get();
|
||||
}
|
||||
|
||||
|
||||
/** @var Closure $resize */
|
||||
$resize = Application::getInstance()->get('image_resize');
|
||||
|
||||
$productsImagesMap = [];
|
||||
foreach ($productsImages as $item) {
|
||||
$productsImagesMap[$item['product_id']][] = [
|
||||
'url' => $resize($item['image'], $imageWidth, $imageHeight),
|
||||
'url' => $this->ocImageTool->resize($item['image'], $imageWidth, $imageHeight, 'placeholder.png'),
|
||||
'alt' => 'Product Image',
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => array_map(function ($product) use ($resize, $productsImagesMap, $imageWidth, $imageHeight) {
|
||||
'data' => array_map(function ($product) use ($productsImagesMap, $imageWidth, $imageHeight) {
|
||||
$allImages = [];
|
||||
if ($product['product_image']) {
|
||||
$image = $resize($product['product_image'], $imageWidth, $imageHeight);
|
||||
} else {
|
||||
$image = $resize('placeholder.png', $imageWidth, $imageHeight);
|
||||
}
|
||||
|
||||
$image = $this->ocImageTool->resize(
|
||||
$product['product_image'],
|
||||
$imageWidth,
|
||||
$imageHeight,
|
||||
'placeholder.png'
|
||||
);
|
||||
|
||||
$allImages[] = [
|
||||
'url' => $image,
|
||||
@@ -125,7 +134,7 @@ class ProductsHandler
|
||||
$product['tax_class_id'],
|
||||
$this->settings->get('oc_config_tax'),
|
||||
),
|
||||
$this->settings->get('oc_currency'),
|
||||
$this->settings->get('oc_default_currency'),
|
||||
);
|
||||
|
||||
if (!empty($productsImagesMap[$product['product_id']])) {
|
||||
@@ -173,7 +182,7 @@ class ProductsHandler
|
||||
->where('product_description.language_id', '=', $languageId);
|
||||
}
|
||||
)
|
||||
->join(
|
||||
->leftJoin(
|
||||
db_table('manufacturer') . ' AS manufacturer',
|
||||
function (JoinClause $join) use ($languageId) {
|
||||
$join->on('products.manufacturer_id', '=', 'manufacturer.manufacturer_id');
|
||||
@@ -183,6 +192,10 @@ class ProductsHandler
|
||||
->limit(1)
|
||||
->firstOrNull();
|
||||
|
||||
if (!$product) {
|
||||
return new JsonResponse([], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$productsImages = $this->queryBuilder->newQuery()
|
||||
->select([
|
||||
'products_images.product_id' => 'product_id',
|
||||
@@ -193,17 +206,20 @@ class ProductsHandler
|
||||
->where('products_images.product_id', '=', $productId)
|
||||
->get();
|
||||
|
||||
/** @var Closure $resize */
|
||||
$resize = Application::getInstance()->get('image_resize');
|
||||
|
||||
$images = [];
|
||||
$images[] = [
|
||||
'url' => $resize($product['product_image'], $imageWidth, $imageHeight),
|
||||
'url' => $this->ocImageTool->resize(
|
||||
$product['product_image'],
|
||||
$imageWidth,
|
||||
$imageHeight,
|
||||
'placeholder.png'
|
||||
),
|
||||
'alt' => $product['product_name'],
|
||||
];
|
||||
|
||||
foreach ($productsImages as $item) {
|
||||
$images[] = [
|
||||
'url' => $resize($item['image'], $imageWidth, $imageHeight),
|
||||
'url' => $this->ocImageTool->resize($item['image'], $imageWidth, $imageHeight, 'placeholder.png'),
|
||||
'alt' => $product['product_name'],
|
||||
];
|
||||
}
|
||||
@@ -214,7 +230,7 @@ class ProductsHandler
|
||||
$product['tax_class_id'],
|
||||
$this->settings->get('oc_config_tax'),
|
||||
),
|
||||
$this->settings->get('oc_currency'),
|
||||
$this->settings->get('oc_default_currency'),
|
||||
);
|
||||
|
||||
$data = [
|
||||
@@ -224,10 +240,64 @@ class ProductsHandler
|
||||
'manufacturer' => $product['product_manufacturer'],
|
||||
'price' => $price,
|
||||
'images' => $images,
|
||||
'options' => $this->loadProductOptions($product),
|
||||
];
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
private function loadProductOptions($product): array
|
||||
{
|
||||
$result = [];
|
||||
$productId = $product['product_id'];
|
||||
$taxClassId = $product['tax_class_id'];
|
||||
|
||||
$options = $this->ocModelCatalogProduct->getProductOptions($productId);
|
||||
$ocConfigTax = $this->settings->get('oc_config_tax');
|
||||
$ocDefaultCurrency = $this->settings->get('oc_default_currency');
|
||||
|
||||
foreach ($options as $option) {
|
||||
$product_option_value_data = [];
|
||||
|
||||
foreach ($option['product_option_value'] as $option_value) {
|
||||
if (!$option_value['subtract'] || ($option_value['quantity'] > 0)) {
|
||||
if ((float)$option_value['price']) {
|
||||
$priceWithTax = $this->tax->calculate(
|
||||
$option_value['price'],
|
||||
$taxClassId,
|
||||
$ocConfigTax ? 'Р' : false,
|
||||
);
|
||||
|
||||
$price = $this->currency->format($priceWithTax, $ocDefaultCurrency);
|
||||
} else {
|
||||
$price = false;
|
||||
}
|
||||
|
||||
$product_option_value_data[] = [
|
||||
'product_option_value_id' => (int)$option_value['product_option_value_id'],
|
||||
'option_value_id' => (int)$option_value['option_value_id'],
|
||||
'name' => $option_value['name'],
|
||||
'image' => $this->ocImageTool->resize($option_value['image'], 50, 50),
|
||||
'price' => $price,
|
||||
'price_prefix' => $option_value['price_prefix'],
|
||||
'selected' => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'product_option_id' => (int)$option['product_option_id'],
|
||||
'values' => $product_option_value_data,
|
||||
'option_id' => (int)$option['option_id'],
|
||||
'name' => $option['name'],
|
||||
'type' => $option['type'],
|
||||
'value' => $option['value'],
|
||||
'required' => filter_var($option['required'], FILTER_VALIDATE_BOOLEAN),
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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