feat: display product options
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Adapters\OcImageTool;
|
||||||
|
use App\Adapters\OcModelCatalogProductAdapter;
|
||||||
use App\ApplicationFactory;
|
use App\ApplicationFactory;
|
||||||
use Cart\Currency;
|
use Cart\Currency;
|
||||||
use Cart\Tax;
|
use Cart\Tax;
|
||||||
@@ -20,9 +22,8 @@ class Controllerextensiontgshophandle extends Controller
|
|||||||
{
|
{
|
||||||
$app = ApplicationFactory::create([
|
$app = ApplicationFactory::create([
|
||||||
'oc_config_tax' => $this->config->get('config_tax'),
|
'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'),
|
'timezone' => $this->config->get('config_timezone', 'UTC'),
|
||||||
'lang' => $this->config->get('config_admin_language'),
|
|
||||||
'language_id' => (int)$this->config->get('config_language_id'),
|
'language_id' => (int)$this->config->get('config_language_id'),
|
||||||
'shop_base_url' => HTTPS_SERVER,
|
'shop_base_url' => HTTPS_SERVER,
|
||||||
'dir_image' => DIR_IMAGE,
|
'dir_image' => DIR_IMAGE,
|
||||||
@@ -42,18 +43,21 @@ class Controllerextensiontgshophandle extends Controller
|
|||||||
return $this->url;
|
return $this->url;
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->bind(Currency::class, function () { return $this->currency; });
|
$app->bind(Currency::class, function () {
|
||||||
$app->bind(Tax::class, function () { return $this->tax; });
|
return $this->currency;
|
||||||
|
});
|
||||||
|
$app->bind(Tax::class, function () {
|
||||||
|
return $this->tax;
|
||||||
|
});
|
||||||
|
|
||||||
$this->load->model('tool/image');
|
$app->bind(OcModelCatalogProductAdapter::class, function () {
|
||||||
$app->bind('image_resize', function () {
|
$this->load->model('catalog/product');
|
||||||
return function ($path, $width, $height) {
|
return new OcModelCatalogProductAdapter($this->model_catalog_product);
|
||||||
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);
|
$app->bind(OcImageTool::class, function () {
|
||||||
};
|
$this->load->model('tool/image');
|
||||||
|
return new OcImageTool($this->model_tool_image);
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->load->model('checkout/order');
|
$this->load->model('checkout/order');
|
||||||
@@ -62,6 +66,8 @@ class Controllerextensiontgshophandle extends Controller
|
|||||||
return $this->model_checkout_order;
|
return $this->model_checkout_order;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->session->data['language'] = $this->config->get('config_language');
|
||||||
|
|
||||||
$app->bootAndHandleRequest();
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ if (!function_exists('config')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! function_exists('dd')) {
|
if (!function_exists('dd')) {
|
||||||
function dd(): void
|
function dd(): void
|
||||||
{
|
{
|
||||||
Utils::dd(func_get_args());
|
Utils::dd(func_get_args());
|
||||||
|
|||||||
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;
|
namespace App\Handlers;
|
||||||
|
|
||||||
use Closure;
|
use App\Adapters\OcImageTool;
|
||||||
use Openguru\OpenCartFramework\Application;
|
|
||||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||||
use Openguru\OpenCartFramework\Http\Request;
|
use Openguru\OpenCartFramework\Http\Request;
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
||||||
@@ -12,10 +11,12 @@ use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
|||||||
class CategoriesHandler
|
class CategoriesHandler
|
||||||
{
|
{
|
||||||
private Builder $queryBuilder;
|
private Builder $queryBuilder;
|
||||||
|
private OcImageTool $imageTool;
|
||||||
|
|
||||||
public function __construct(Builder $queryBuilder)
|
public function __construct(Builder $queryBuilder, OcImageTool $ocImageTool)
|
||||||
{
|
{
|
||||||
$this->queryBuilder = $queryBuilder;
|
$this->queryBuilder = $queryBuilder;
|
||||||
|
$this->ocImageTool = $ocImageTool;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index(Request $request): JsonResponse
|
public function index(Request $request): JsonResponse
|
||||||
@@ -47,16 +48,9 @@ class CategoriesHandler
|
|||||||
->limit($perPage)
|
->limit($perPage)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
/** @var Closure $resize */
|
|
||||||
$resize = Application::getInstance()->get('image_resize');
|
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'data' => array_map(static function ($category) use ($resize, $imageWidth, $imageHeight) {
|
'data' => array_map(function ($category) use ($imageWidth, $imageHeight) {
|
||||||
if ($category['image']) {
|
$image = $this->ocImageTool->resize($category['image'], $imageWidth, $imageHeight, 'no_image.png');
|
||||||
$image = $resize($category['image'], $imageWidth, $imageHeight);
|
|
||||||
} else {
|
|
||||||
$image = $resize('no_image.png', $imageWidth, $imageHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int)$category['id'],
|
'id' => (int)$category['id'],
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Handlers;
|
namespace App\Handlers;
|
||||||
|
|
||||||
|
use App\Adapters\OcImageTool;
|
||||||
|
use App\Adapters\OcModelCatalogProductAdapter;
|
||||||
use Cart\Currency;
|
use Cart\Currency;
|
||||||
use Cart\Tax;
|
use Cart\Tax;
|
||||||
use Closure;
|
|
||||||
use Openguru\OpenCartFramework\Application;
|
|
||||||
use Openguru\OpenCartFramework\Config\Settings;
|
use Openguru\OpenCartFramework\Config\Settings;
|
||||||
use Openguru\OpenCartFramework\Http\JsonResponse;
|
use Openguru\OpenCartFramework\Http\JsonResponse;
|
||||||
use Openguru\OpenCartFramework\Http\Request;
|
use Openguru\OpenCartFramework\Http\Request;
|
||||||
|
use Openguru\OpenCartFramework\Http\Response;
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
||||||
use Openguru\OpenCartFramework\Support\Arr;
|
use Openguru\OpenCartFramework\Support\Arr;
|
||||||
@@ -19,13 +20,23 @@ class ProductsHandler
|
|||||||
private Currency $currency;
|
private Currency $currency;
|
||||||
private Tax $tax;
|
private Tax $tax;
|
||||||
private Settings $settings;
|
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->queryBuilder = $queryBuilder;
|
||||||
$this->currency = $currency;
|
$this->currency = $currency;
|
||||||
$this->tax = $tax;
|
$this->tax = $tax;
|
||||||
$this->settings = $settings;
|
$this->settings = $settings;
|
||||||
|
$this->ocModelCatalogProduct = $ocModelCatalogProduct;
|
||||||
|
$this->ocImageTool = $ocImageTool;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(Request $request): JsonResponse
|
public function handle(Request $request): JsonResponse
|
||||||
@@ -93,26 +104,24 @@ class ProductsHandler
|
|||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** @var Closure $resize */
|
|
||||||
$resize = Application::getInstance()->get('image_resize');
|
|
||||||
|
|
||||||
$productsImagesMap = [];
|
$productsImagesMap = [];
|
||||||
foreach ($productsImages as $item) {
|
foreach ($productsImages as $item) {
|
||||||
$productsImagesMap[$item['product_id']][] = [
|
$productsImagesMap[$item['product_id']][] = [
|
||||||
'url' => $resize($item['image'], $imageWidth, $imageHeight),
|
'url' => $this->ocImageTool->resize($item['image'], $imageWidth, $imageHeight, 'placeholder.png'),
|
||||||
'alt' => 'Product Image',
|
'alt' => 'Product Image',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'data' => array_map(function ($product) use ($resize, $productsImagesMap, $imageWidth, $imageHeight) {
|
'data' => array_map(function ($product) use ($productsImagesMap, $imageWidth, $imageHeight) {
|
||||||
$allImages = [];
|
$allImages = [];
|
||||||
if ($product['product_image']) {
|
|
||||||
$image = $resize($product['product_image'], $imageWidth, $imageHeight);
|
$image = $this->ocImageTool->resize(
|
||||||
} else {
|
$product['product_image'],
|
||||||
$image = $resize('placeholder.png', $imageWidth, $imageHeight);
|
$imageWidth,
|
||||||
}
|
$imageHeight,
|
||||||
|
'placeholder.png'
|
||||||
|
);
|
||||||
|
|
||||||
$allImages[] = [
|
$allImages[] = [
|
||||||
'url' => $image,
|
'url' => $image,
|
||||||
@@ -125,7 +134,7 @@ class ProductsHandler
|
|||||||
$product['tax_class_id'],
|
$product['tax_class_id'],
|
||||||
$this->settings->get('oc_config_tax'),
|
$this->settings->get('oc_config_tax'),
|
||||||
),
|
),
|
||||||
$this->settings->get('oc_currency'),
|
$this->settings->get('oc_default_currency'),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!empty($productsImagesMap[$product['product_id']])) {
|
if (!empty($productsImagesMap[$product['product_id']])) {
|
||||||
@@ -173,7 +182,7 @@ class ProductsHandler
|
|||||||
->where('product_description.language_id', '=', $languageId);
|
->where('product_description.language_id', '=', $languageId);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
->join(
|
->leftJoin(
|
||||||
db_table('manufacturer') . ' AS manufacturer',
|
db_table('manufacturer') . ' AS manufacturer',
|
||||||
function (JoinClause $join) use ($languageId) {
|
function (JoinClause $join) use ($languageId) {
|
||||||
$join->on('products.manufacturer_id', '=', 'manufacturer.manufacturer_id');
|
$join->on('products.manufacturer_id', '=', 'manufacturer.manufacturer_id');
|
||||||
@@ -183,6 +192,10 @@ class ProductsHandler
|
|||||||
->limit(1)
|
->limit(1)
|
||||||
->firstOrNull();
|
->firstOrNull();
|
||||||
|
|
||||||
|
if (!$product) {
|
||||||
|
return new JsonResponse([], Response::HTTP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
$productsImages = $this->queryBuilder->newQuery()
|
$productsImages = $this->queryBuilder->newQuery()
|
||||||
->select([
|
->select([
|
||||||
'products_images.product_id' => 'product_id',
|
'products_images.product_id' => 'product_id',
|
||||||
@@ -193,17 +206,20 @@ class ProductsHandler
|
|||||||
->where('products_images.product_id', '=', $productId)
|
->where('products_images.product_id', '=', $productId)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
/** @var Closure $resize */
|
|
||||||
$resize = Application::getInstance()->get('image_resize');
|
|
||||||
|
|
||||||
$images = [];
|
$images = [];
|
||||||
$images[] = [
|
$images[] = [
|
||||||
'url' => $resize($product['product_image'], $imageWidth, $imageHeight),
|
'url' => $this->ocImageTool->resize(
|
||||||
|
$product['product_image'],
|
||||||
|
$imageWidth,
|
||||||
|
$imageHeight,
|
||||||
|
'placeholder.png'
|
||||||
|
),
|
||||||
'alt' => $product['product_name'],
|
'alt' => $product['product_name'],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($productsImages as $item) {
|
foreach ($productsImages as $item) {
|
||||||
$images[] = [
|
$images[] = [
|
||||||
'url' => $resize($item['image'], $imageWidth, $imageHeight),
|
'url' => $this->ocImageTool->resize($item['image'], $imageWidth, $imageHeight, 'placeholder.png'),
|
||||||
'alt' => $product['product_name'],
|
'alt' => $product['product_name'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -214,7 +230,7 @@ class ProductsHandler
|
|||||||
$product['tax_class_id'],
|
$product['tax_class_id'],
|
||||||
$this->settings->get('oc_config_tax'),
|
$this->settings->get('oc_config_tax'),
|
||||||
),
|
),
|
||||||
$this->settings->get('oc_currency'),
|
$this->settings->get('oc_default_currency'),
|
||||||
);
|
);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
@@ -224,10 +240,64 @@ class ProductsHandler
|
|||||||
'manufacturer' => $product['product_manufacturer'],
|
'manufacturer' => $product['product_manufacturer'],
|
||||||
'price' => $price,
|
'price' => $price,
|
||||||
'images' => $images,
|
'images' => $images,
|
||||||
|
'options' => $this->loadProductOptions($product),
|
||||||
];
|
];
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'data' => $data,
|
'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>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<FullscreenViewport/>
|
<FullscreenViewport v-if="platform === 'ios' || platform === 'android'"/>
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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 Home from './views/Home.vue';
|
||||||
import Product from './views/Product.vue';
|
import Product from './views/Product.vue';
|
||||||
import CategoriesList from "./views/CategoriesList.vue";
|
import CategoriesList from "./views/CategoriesList.vue";
|
||||||
@@ -12,7 +12,7 @@ const routes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHistory('/image/catalog/tgshopspa/'),
|
history: createMemoryHistory('/image/catalog/tgshopspa/'),
|
||||||
routes,
|
routes,
|
||||||
scrollBehavior(to, from, savedPosition) {
|
scrollBehavior(to, from, savedPosition) {
|
||||||
if (savedPosition) {
|
if (savedPosition) {
|
||||||
|
|||||||
@@ -20,11 +20,14 @@
|
|||||||
<h3 class="text-sm font-medium">{{ product.manufacturer }}</h3>
|
<h3 class="text-sm font-medium">{{ product.manufacturer }}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Options -->
|
|
||||||
<div class="mt-4 lg:row-span-3 lg:mt-0">
|
<div class="mt-4 lg:row-span-3 lg:mt-0">
|
||||||
<p class="text-3xl tracking-tight">{{ product.price }}</p>
|
<p class="text-3xl tracking-tight">{{ product.price }}</p>
|
||||||
</div>
|
</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">
|
<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 -->
|
<!-- Description and details -->
|
||||||
<div>
|
<div>
|
||||||
@@ -34,7 +37,6 @@
|
|||||||
<p class="text-base" v-html="product.description"></p>
|
<p class="text-base" v-html="product.description"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,6 +49,7 @@ import {$fetch} from "ofetch";
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import {useHapticFeedback} from 'vue-tg';
|
import {useHapticFeedback} from 'vue-tg';
|
||||||
|
import ProductOptions from "../components/ProductOptions/ProductOptions.vue";
|
||||||
const hapticFeedback = useHapticFeedback();
|
const hapticFeedback = useHapticFeedback();
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
Reference in New Issue
Block a user