first commit
This commit is contained in:
100
bundles/KupShop/RecommendersBundle/Admin/Recommenders.php
Normal file
100
bundles/KupShop/RecommendersBundle/Admin/Recommenders.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Admin;
|
||||
|
||||
use KupShop\AdminBundle\Admin\ProductsFilter;
|
||||
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
||||
use KupShop\KupShopBundle\Util\System\BundleFinder;
|
||||
use KupShop\RecommendersBundle\Recommenders\RecommendersLocator;
|
||||
|
||||
class Recommenders extends \Window
|
||||
{
|
||||
protected $tableName = 'recommenders';
|
||||
protected $nameField = 'title';
|
||||
protected $uniques = ['label' => 'label'];
|
||||
|
||||
protected RecommendersLocator $recommendersLocator;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->recommendersLocator = ServiceContainer::getService(RecommendersLocator::class);
|
||||
}
|
||||
|
||||
public function get_vars()
|
||||
{
|
||||
$vars = parent::get_vars();
|
||||
|
||||
$recommenders = $this->recommendersLocator->getRecommenders();
|
||||
$vars['recommenders'] = array_map(fn ($r) => $r->getName(), $recommenders);
|
||||
$this->unserializeCustomData($vars['body']['data']);
|
||||
$this->getRecommendersData($vars['body']['data']);
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
$data = parent::getData();
|
||||
|
||||
$this->cleanData($data);
|
||||
if (!empty($data['data']['fallback'])) {
|
||||
$this->cleanData($data['data']['fallback']);
|
||||
}
|
||||
|
||||
if (getVal('Submit')) {
|
||||
$this->serializeCustomData($data);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function serializeCustomData(&$data)
|
||||
{
|
||||
if (empty($data['data']['cache']) && $data['data']['cache'] === '') {
|
||||
unset($data['data']['cache']);
|
||||
}
|
||||
|
||||
parent::serializeCustomData($data);
|
||||
}
|
||||
|
||||
protected function cleanData(&$data): void
|
||||
{
|
||||
$type = $data['type'] ?? null;
|
||||
if (($type == 'products_filter') && ($filter = $data['data']['filter'] ?? null)) {
|
||||
$data['data']['filter'] = ProductsFilter::cleanFilter($filter);
|
||||
}
|
||||
}
|
||||
|
||||
public function getRecommendersData(array &$data)
|
||||
{
|
||||
if ($type = $data['type'] ?? null) {
|
||||
$data['recommender'] = $recommender = $this->recommendersLocator->getRecommender($type);
|
||||
$data['configurationTemplate'] = $this->getConfigurationTemplate($recommender->getTemplate());
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getConfigurationTemplate(?string $template): ?string
|
||||
{
|
||||
$bundleFinder = ServiceContainer::getService(BundleFinder::class);
|
||||
|
||||
if ($existing = $bundleFinder->getExistingBundlesPath('Admin/templates/window/'.$template)) {
|
||||
$bundles = array_keys($existing);
|
||||
|
||||
return '['.reset($bundles).']/window/'.$template;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function hasRights($name = null)
|
||||
{
|
||||
return match ($name) {
|
||||
\Window::RIGHT_DELETE, \Window::RIGHT_DUPLICATE => isSuperuser(),
|
||||
default => parent::hasRights($name),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Recommenders::class;
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\RecommendersBundle\Admin\Tabs;
|
||||
|
||||
use KupShop\AdminBundle\Admin\WindowTab;
|
||||
|
||||
class RecommendersFallbackTab extends WindowTab
|
||||
{
|
||||
protected $title = 'flapRecommenderFallback';
|
||||
protected $template = 'window/recommenders.fallback.tpl';
|
||||
|
||||
public static function getTypes(): array
|
||||
{
|
||||
return [
|
||||
'Recommenders' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
public function getLabel()
|
||||
{
|
||||
return translate('flapRecommenderFallback', 'Recommenders');
|
||||
}
|
||||
|
||||
public function getVars($smarty_tpl_vars)
|
||||
{
|
||||
$fallback = $this->window->getCustomData()['fallback'] ?? [];
|
||||
if ($fallback && (($fallback['enabled'] ?? 'N') == 'Y')) {
|
||||
$this->window->getRecommendersData($fallback);
|
||||
}
|
||||
|
||||
return $fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
$txt_str['Recommenders'] = [
|
||||
'navigation' => 'Doporučené zboží',
|
||||
'toolbar_list' => 'Seznam doporučených produktů',
|
||||
'toolbar_add' => 'Přidat doporučené produkty',
|
||||
'titleEdit' => 'Upravit doporučené produkty',
|
||||
'titleAdd' => 'Přidat doporučené produkty',
|
||||
'flapRecommender' => 'Doporučené produkty',
|
||||
'flapRecommenderFallback' => 'Doplňovat produkty',
|
||||
|
||||
'activityEdited' => 'Upraven recommender: %s',
|
||||
'activityAdded' => 'Přidán recommender: %s',
|
||||
'activityDeleted' => 'Smazán recommender: %s',
|
||||
|
||||
'id' => 'ID',
|
||||
'position' => 'Pozice',
|
||||
'label' => 'Label',
|
||||
'products_count' => 'Počet produktů',
|
||||
'type' => 'Způsob doporučování produktů',
|
||||
'typeTooltip' => 'Doplňující nastavení se zobrazí až po uložení vašeho výběru v tomto poli.',
|
||||
'title' => 'Nadpis',
|
||||
'random' => 'Náhodný výběr',
|
||||
'randomTooltip' => 'Každý den se zobrazí jiné produkty, které splňují zvolená kritéria.',
|
||||
'related_type' => 'Typ souvisejícího zboží',
|
||||
];
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
$txt_str['Recommenders'] = [
|
||||
'navigation' => 'Recommenders',
|
||||
'toolbar_list' => 'Recommenders list',
|
||||
'toolbar_add' => 'Add recommender',
|
||||
'titleEdit' => 'Edit recommender',
|
||||
'titleAdd' => 'Add recommender',
|
||||
'flapRecommender' => 'Recommender',
|
||||
'flapRecommenderFallback' => 'Fallback',
|
||||
|
||||
'activityEdited' => 'Edited recommender: %s',
|
||||
'activityAdded' => 'Added recommender: %s',
|
||||
'activityDeleted' => 'Deleted recommender: %s',
|
||||
|
||||
'id' => 'ID',
|
||||
'position' => 'Position',
|
||||
'label' => 'Label',
|
||||
'products_count' => 'Products count',
|
||||
'type' => 'Recommending method',
|
||||
'typeTooltip' => 'Additional settings will be displayed after you save your selection in this field.',
|
||||
'title' => 'Title',
|
||||
'random' => 'Random selection',
|
||||
'randomTooltip' => 'Different products that meet the selected criteria will be displayed each day.',
|
||||
'related_type' => 'Related products type',
|
||||
];
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\RecommendersBundle\Admin\lists;
|
||||
|
||||
use KupShop\AdminBundle\AdminList\BaseList;
|
||||
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
||||
use KupShop\RecommendersBundle\Recommenders\RecommendersLocator;
|
||||
|
||||
class RecommendersList extends BaseList
|
||||
{
|
||||
protected $tableName = 'recommenders';
|
||||
protected ?string $tableAlias = 'rec';
|
||||
|
||||
protected $tableDef = [
|
||||
'id' => 'id',
|
||||
'fields' => [
|
||||
'ID' => ['field' => 'id', 'fieldType' => self::TYPE_INT, 'size' => 0.2],
|
||||
'position' => ['field' => 'position', 'translate' => true, 'fieldType' => self::TYPE_STRING],
|
||||
'label' => ['field' => 'label', 'translate' => true, 'fieldType' => self::TYPE_STRING, 'wpjAdmin' => true],
|
||||
'title' => ['field' => 'title', 'translate' => true, 'fieldType' => self::TYPE_STRING],
|
||||
'products_count' => ['field' => 'products_count', 'translate' => true, 'fieldType' => self::TYPE_INT],
|
||||
'type' => ['field' => 'type', 'translate' => true, 'fieldType' => self::TYPE_STRING, 'render' => 'renderType'],
|
||||
],
|
||||
];
|
||||
|
||||
protected RecommendersLocator $recommendersLocator;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->recommendersLocator = ServiceContainer::getService(RecommendersLocator::class);
|
||||
}
|
||||
|
||||
public function renderType($values, $column)
|
||||
{
|
||||
$type = $values['type'];
|
||||
|
||||
$recommender = $this->recommendersLocator->getRecommender($type);
|
||||
|
||||
return $recommender->getName(json_decode($values['data'] ?? '', true));
|
||||
}
|
||||
}
|
||||
|
||||
return RecommendersList::class;
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Admin\lists;
|
||||
|
||||
use KupShop\I18nBundle\Admin\lists\TranslateList;
|
||||
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
||||
use KupShop\RecommendersBundle\Translations\RecommendersTranslation;
|
||||
|
||||
class TranslateRecommendersList extends TranslateList
|
||||
{
|
||||
protected $template = 'list/translateRecommenders.tpl';
|
||||
|
||||
protected $listType = 'translateRecommenders';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->translation = ServiceContainer::getService(RecommendersTranslation::class);
|
||||
}
|
||||
}
|
||||
|
||||
return TranslateRecommendersList::class;
|
||||
@@ -0,0 +1,5 @@
|
||||
{extends file="[shared]../../bundles/KupShop/I18nBundle/Admin/templates/list/translate.tpl"}
|
||||
|
||||
{block translationObjectTitle}
|
||||
{$object.id} | <a title="Zobrazit detail" href="javascript:nw('Recommenders', '{$object.id}')">{$object.position}</a>
|
||||
{/block}
|
||||
@@ -0,0 +1,9 @@
|
||||
{extends file="[shared]/menu.tpl"}
|
||||
|
||||
{block name="menu-items"}
|
||||
<li class="nav-header"><i class="glyphicon {block list_icon}glyphicon-tags{/block}"></i><span>{translate_type type=$type}</span></li>
|
||||
<li><a href="javascript:nf('', 'launch.php?s=list.php&type={$type}');"><i class="glyphicon glyphicon-list"></i> <span>{'toolbar_list'|translate}</span></a></li>
|
||||
{if isSuperuser()}
|
||||
<li><a href="javascript:nw('{$type}', '0');"><i class="glyphicon glyphicon-plus"></i> <span>{'toolbar_add'|translate}</span> <i class="glyphicon glyphicon-flash" style="color: #aab2bd;" title="Vidí pouze superadmin"></i></a></li>
|
||||
{/if}
|
||||
{/block}
|
||||
@@ -0,0 +1,43 @@
|
||||
<div id="flapRecommenderFallback" class="tab-pane fade active in boxStatic">
|
||||
<div class="form-group wpj-form-group-flex">
|
||||
<div class="col-md-3">
|
||||
<div class="wpj-form-group">
|
||||
<div class="d-flex align-items-center">
|
||||
{print_toggle value=$tab.data.enabled name="data][fallback][enabled"}
|
||||
<label>{'flapRecommenderFallback'|translate}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="wpj-form-group">
|
||||
<label>{'type'|translate}</label>
|
||||
<select class="selecter" name="data[data][fallback][type]" {if $body.data.data.fallback.enabled != 'Y'}disabled{/if}>
|
||||
{foreach array_merge(['' => 'Vyberte možnost'], $recommenders) as $type => $name}
|
||||
<option value="{$type}" {if $type == $tab.data.type}selected{/if}>{$name}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{if $tab.data.configurationTemplate and !$tab.data.configurationTemplate|strstr:'/recommender.products_filter.tpl'}
|
||||
{include $tab.data.configurationTemplate name="data[data][fallback]" data=$tab.data}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{if $tab.data.configurationTemplate and $tab.data.configurationTemplate|strstr:'/recommender.products_filter.tpl'}
|
||||
{include $tab.data.configurationTemplate name="data[data][fallback]" data=$tab.data}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$("input[name='data[data][fallback][enabled]']").on("change", function () {
|
||||
const $inputs = $("select[name='data[data][fallback][type]']");
|
||||
$inputs.prop("disabled", !$(this).is(':checked'));
|
||||
$inputs.trigger('chosen:updated')
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
{extends file="[shared]/window.tpl"}
|
||||
|
||||
{block tabs}
|
||||
{windowTab id='flapRecommender' label=translate('flapRecommender')}
|
||||
{/block}
|
||||
|
||||
{block tabsContent}
|
||||
<div id="flapRecommender" class="tab-pane fade active in boxStatic">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<div class="wpj-form-group">
|
||||
<label>{'title'|translate}</label>
|
||||
<input class="form-control input-sm" type="text" name="data[title]" value="{$body.data.title}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-12">
|
||||
{if isSuperuser()}
|
||||
<div class="wpj-form-group">
|
||||
<label><span class="glyphicon glyphicon-flash" style="color: #aab2bd;" title="Vidí pouze superadmin"></span>{'label'|translate}</label>
|
||||
<input class="form-control input-sm" type="text" name="data[label]"
|
||||
value="{$body.data.label}" required>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-12">
|
||||
{if isSuperuser()}
|
||||
<div class="wpj-form-group">
|
||||
<label><span class="glyphicon glyphicon-flash" style="color: #aab2bd;" title="Vidí pouze superadmin"></span>Cache</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control input-sm" type="number" name="data[data][cache]"
|
||||
value="{$body.data.data.cache}" placeholder="3600">
|
||||
<span class="input-group-addon">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-xs-12">
|
||||
<div class="wpj-form-group">
|
||||
<label>{'position'|translate}</label>
|
||||
<input class="form-control input-sm" type="text" name="data[position]"
|
||||
value="{$body.data.position}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-12">
|
||||
<div class="wpj-form-group">
|
||||
<label>{'products_count'|translate}</label>
|
||||
<input class="form-control input-sm" type="number" name="data[products_count]"
|
||||
value="{$body.data.products_count}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-12">
|
||||
<div class="wpj-form-group">
|
||||
<label>
|
||||
{'type'|translate}
|
||||
<a class="help-tip" data-toggle="tooltip" title=""
|
||||
data-original-title="{'typeTooltip'|translate}">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</label>
|
||||
<select class="selecter" name="data[type]">
|
||||
{foreach $recommenders as $type => $name}
|
||||
<option value="{$type}" {if $type == $body.data.type}selected{/if}>{$name}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-12">
|
||||
{if $body.data.configurationTemplate and !$body.data.configurationTemplate|strstr:'/recommender.products_filter.tpl'}
|
||||
{include $body.data.configurationTemplate name="data" data=$body.data}
|
||||
{/if}
|
||||
|
||||
{if $body.data.recommender && $body.data.recommender->getRandom()}
|
||||
<div class="wpj-form-group">
|
||||
<label class="nowrap">
|
||||
{'random'|translate}
|
||||
<a class="help-tip" data-toggle="tooltip" title=""
|
||||
data-original-title="{'randomTooltip'|translate}">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</label>
|
||||
{$random = ['N' => 'Ne', 'Y' => 'Ano', 'very_random' => 'very_random']}
|
||||
{$random = ['N' => 'Ne', 'Y' => 'Ano']}
|
||||
{print_select name="data[data][random]" var=$random selected=$body.data.data.random}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{if $body.data.configurationTemplate and $body.data.configurationTemplate|strstr:'/recommender.products_filter.tpl'}
|
||||
{include $body.data.configurationTemplate name="data" data=$body.data}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/block}
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="wpj-form-group">
|
||||
<label class="nowrap">v kategorii</label>
|
||||
<select data-autocomplete="categories" data-preload="sections"
|
||||
name="{$name}[data][section][]" multiple='multiple'
|
||||
class="selecter" id="category_select" data-type="categories"
|
||||
data-autocomplete-params="visible=0&allow_empty=1&empty_choice=[Všechny kategorie]"
|
||||
data-placeholder="{'selectCategories'|translate:"productsFilter"}">
|
||||
{foreach $data.data.section as $item}
|
||||
<option value="{$item}" selected>{$item}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="wpj-form-group">
|
||||
{include 'block.productsFilter.tpl' filter_size=2 filter=$data.data.filter filterName=$name filterInputName=$name|cat:'[data][filter]'}
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
{if findModule(Modules::PRODUCTS_RELATED, Modules::SUB_TYPES)}
|
||||
<div class="wpj-form-group">
|
||||
<label>{'related_type'|translate}</label>
|
||||
<select data-autocomplete="productsRelatedTypes" data-preload="productsRelatedTypes"
|
||||
name="{$name}[data][related_type]" class="required selecter small" required="required">
|
||||
<option value="{$data.data.related_type}" selected></option>
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\AdminRegister;
|
||||
|
||||
use KupShop\AdminBundle\AdminRegister\AdminRegister;
|
||||
use KupShop\AdminBundle\AdminRegister\IAdminRegisterDynamic;
|
||||
use KupShop\AdminBundle\AdminRegister\IAdminRegisterStatic;
|
||||
|
||||
class RecommendersAdminRegister extends AdminRegister implements IAdminRegisterDynamic, IAdminRegisterStatic
|
||||
{
|
||||
public function getDynamicMenu(): array
|
||||
{
|
||||
$dynamicMenu = [
|
||||
static::createMenuItem('productsMenu',
|
||||
[
|
||||
'name' => 'Recommenders',
|
||||
'title' => translate('navigation', 'Recommenders'),
|
||||
'left' => 's=menu.php&type=Recommenders',
|
||||
'right' => 's=list.php&type=Recommenders',
|
||||
]),
|
||||
];
|
||||
|
||||
if (findModule(\Modules::TRANSLATIONS)) {
|
||||
$dynamicMenu[] = static::createMenuItem('translate/translateProducts',
|
||||
[
|
||||
'name' => 'translate.translateRecommenders',
|
||||
'right' => 's=list.php&type=translateRecommenders',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $dynamicMenu;
|
||||
}
|
||||
|
||||
public static function getPermissions(): array
|
||||
{
|
||||
return [
|
||||
static::createPermissions('Recommenders', [], ['RECOMMENDERS']),
|
||||
static::createPermissions('translateRecommenders', [], ['RECOMMENDERS']),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Recommenders;
|
||||
|
||||
use KupShop\CatalogBundle\ProductList\ProductCollection;
|
||||
|
||||
abstract class AbstractRecommender implements IRecommender
|
||||
{
|
||||
protected static string $type;
|
||||
protected static string $name;
|
||||
protected static string $context = 'default';
|
||||
protected static bool $random = false;
|
||||
protected static ?string $in_store = 'supplier'; // in_store, supplier, all...
|
||||
|
||||
public function __construct(protected RecommendersLocator $recommendersLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public static function getName(array $data = []): string
|
||||
{
|
||||
return static::$name;
|
||||
}
|
||||
|
||||
public static function getType(): string
|
||||
{
|
||||
return static::$type;
|
||||
}
|
||||
|
||||
public function getTemplate(): string
|
||||
{
|
||||
return 'recommenders/recommender.'.static::$type.'.tpl';
|
||||
}
|
||||
|
||||
public static function getRandom(): bool
|
||||
{
|
||||
return static::$random;
|
||||
}
|
||||
|
||||
public static function getInStore(): ?string
|
||||
{
|
||||
return static::$in_store;
|
||||
}
|
||||
|
||||
public function getConfigurationVariables(array $data): array
|
||||
{
|
||||
$config = ($data['data'] ?? []);
|
||||
|
||||
if (is_string($config)) {
|
||||
$config = json_decode($config, true);
|
||||
}
|
||||
|
||||
if (!is_array($config) && !is_object($config)) {
|
||||
$config = [];
|
||||
}
|
||||
|
||||
if (!empty($data['products_count'])) {
|
||||
$config['count'] = $data['products_count'];
|
||||
}
|
||||
if ($this->getInStore()) {
|
||||
$config['in_store'] = $config['in_store'] ?? $this->getInStore();
|
||||
}
|
||||
if ($fallback = $config['fallback'] ?? null) {
|
||||
unset($config['fallback']);
|
||||
if (!empty($fallback['type']) && (($fallback['enabled'] ?? 'N') == 'Y')) {
|
||||
$recommender = $this->recommendersLocator->getRecommender($fallback['type']);
|
||||
$fallbackConfig = $recommender->getConfigurationVariables($fallback);
|
||||
$config['fallback'] = [array_merge($fallbackConfig, ['type' => $fallback['type']])];
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function execute(array $data, array $config): ProductCollection
|
||||
{
|
||||
$params = array_merge($config, $data, ['type' => $this::$type]);
|
||||
|
||||
return (new \InsertProductsTypes())->getProducts($params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Recommenders;
|
||||
|
||||
class AlsoBoughtRecommender extends AbstractRecommender implements IRecommender
|
||||
{
|
||||
protected static string $type = 'cart_product';
|
||||
protected static string $name = 'Zákazníci koupili také';
|
||||
protected static string $context = 'cart';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Recommenders;
|
||||
|
||||
class BestsellingRecommender extends AbstractRecommender implements IRecommender
|
||||
{
|
||||
protected static string $type = 'bestselling_product';
|
||||
protected static string $name = 'Nejprodávanější produkty';
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Recommenders;
|
||||
|
||||
use KupShop\CatalogBundle\ProductList\ProductCollection;
|
||||
|
||||
interface IRecommender
|
||||
{
|
||||
public static function getName(array $data): string;
|
||||
|
||||
public static function getType(): string;
|
||||
|
||||
public function getTemplate(): ?string;
|
||||
|
||||
public function getConfigurationVariables(array $data): array;
|
||||
|
||||
public function execute(array $data, array $config): ProductCollection;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Recommenders;
|
||||
|
||||
use KupShop\CatalogBundle\ProductList\ProductCollection;
|
||||
|
||||
class LastVisitedProductsRecommender extends AbstractRecommender implements IRecommender
|
||||
{
|
||||
protected static string $type = 'last_visited';
|
||||
protected static string $typeStateless = 'last_visited_stateless';
|
||||
|
||||
protected static string $name = 'Naposledy prohlížené';
|
||||
protected static ?string $in_store = 'all';
|
||||
|
||||
public function execute(array $data, array $config): ProductCollection
|
||||
{
|
||||
$params = array_merge($config, $data);
|
||||
|
||||
if (isset($data['lastVisited'])) {
|
||||
$params['type'] = $this::$typeStateless;
|
||||
$params['products'] = $data['lastVisited'] ?: [];
|
||||
} else {
|
||||
$params['type'] = $this::$type;
|
||||
}
|
||||
|
||||
return (new \InsertProductsTypes())->getProducts($params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Recommenders;
|
||||
|
||||
class NewestProductsRecommender extends AbstractRecommender implements IRecommender
|
||||
{
|
||||
protected static string $type = 'newest_product';
|
||||
protected static string $name = 'Novinky';
|
||||
protected static bool $random = true;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Recommenders;
|
||||
|
||||
class ProductsFilterRecommender extends AbstractRecommender implements IRecommender
|
||||
{
|
||||
protected static string $type = 'products_filter';
|
||||
protected static string $name = 'Produktový filtr';
|
||||
protected static bool $random = true;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\RecommendersBundle\Recommenders;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
|
||||
class RecommendersLocator
|
||||
{
|
||||
private $recommenders;
|
||||
|
||||
public function __construct(iterable $recommenders)
|
||||
{
|
||||
$this->recommenders = $recommenders;
|
||||
}
|
||||
|
||||
public function getTypes(): array
|
||||
{
|
||||
return array_keys($this->getRecommenders());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IRecommender[]
|
||||
*/
|
||||
public function getRecommenders(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->recommenders as $recommender) {
|
||||
$result[$recommender::getType()] = $recommender;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getRecommender(string $type): IRecommender
|
||||
{
|
||||
if ($recommender = $this->getRecommenders()[$type] ?? null) {
|
||||
return $recommender;
|
||||
}
|
||||
|
||||
throw new ServiceNotFoundException($type, msg: "You have requested a non-existent recommender \"{$type}\".");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Recommenders;
|
||||
|
||||
class RelatedProductsRecommender extends AbstractRecommender implements IRecommender
|
||||
{
|
||||
protected static string $type = 'related_product';
|
||||
protected static string $name = 'Související zboží';
|
||||
protected static string $context = 'product';
|
||||
protected static array $relatedTypes;
|
||||
protected static bool $random = true;
|
||||
|
||||
public static function getName(array $data = []): string
|
||||
{
|
||||
if (!empty($data['related_type'])) {
|
||||
return static::$name.' - '.self::getRelatedTypes()[$data['related_type']] ?? $data['related_type'];
|
||||
}
|
||||
|
||||
return static::$name;
|
||||
}
|
||||
|
||||
protected static function getRelatedTypes(): array
|
||||
{
|
||||
if (!isset(self::$relatedTypes)) {
|
||||
self::$relatedTypes = sqlQueryBuilder()->select('*')
|
||||
->from('products_related_types', 'prt')
|
||||
->execute()->fetchAllKeyValue();
|
||||
}
|
||||
|
||||
return self::$relatedTypes;
|
||||
}
|
||||
}
|
||||
18
bundles/KupShop/RecommendersBundle/RecommendersBundle.php
Normal file
18
bundles/KupShop/RecommendersBundle/RecommendersBundle.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle;
|
||||
|
||||
use KupShop\RecommendersBundle\Recommenders\IRecommender;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class RecommendersBundle extends Bundle
|
||||
{
|
||||
public function build(ContainerBuilder $container)
|
||||
{
|
||||
$container->registerForAutoconfiguration(IRecommender::class)
|
||||
->addTag('kupshop.recommender')
|
||||
->setPublic(false)
|
||||
->setAutowired(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
twig_component:
|
||||
defaults:
|
||||
KupShop\RecommendersBundle\Twig\Components\: '@Recommenders/components/'
|
||||
|
||||
services:
|
||||
_defaults:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
KupShop\RecommendersBundle\:
|
||||
resource: ../../{Twig}
|
||||
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
_defaults:
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
||||
KupShop\RecommendersBundle\:
|
||||
resource: ../../{Admin/Tabs,AdminRegister,Recommenders,Translations,Util}
|
||||
|
||||
KupShop\RecommendersBundle\Recommenders\RecommendersLocator:
|
||||
class: KupShop\RecommendersBundle\Recommenders\RecommendersLocator
|
||||
arguments: [ !tagged kupshop.recommender ]
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* @param Smarty_Internal_Template $smarty
|
||||
*/
|
||||
function smarty_function_insert_recommender($params, $smarty)
|
||||
{
|
||||
/*
|
||||
Examples
|
||||
category bestsellers:
|
||||
{insert_recommender label='bestsellers' data=[section => $body.category.id] template='block.products.related.tpl'}
|
||||
cart also bought:
|
||||
{insert_recommender label='cart-alsobought' data=[products => $body.products]}
|
||||
Luigi's Box Recommenders:
|
||||
category recommended:
|
||||
{insert_recommender label='luigisbox-trends' data=[category => $body.category.id] template='block.products.related.tpl'}
|
||||
cart cross-sell:
|
||||
{insert_recommender label='luigisbox-basket' data=[products => $body.products]}
|
||||
last visited:
|
||||
{insert_recommender label='luigisbox-last-visited' data=['count' => 10, 'image' => 'product_gallery']}
|
||||
*/
|
||||
|
||||
if (empty($params['label'])) {
|
||||
throw new InvalidArgumentException('Missing required parameter \'label\'');
|
||||
}
|
||||
|
||||
$default = [
|
||||
'image' => 'product_catalog',
|
||||
];
|
||||
|
||||
$data = $params['data'] ?? [];
|
||||
if (!empty($data['products'])) {
|
||||
if ($data['products'] instanceof \KupShop\CatalogBundle\ProductList\ProductCollection) {
|
||||
$data['products'] = $data['products']->toArray();
|
||||
}
|
||||
$id_products = array_map(function ($x) {
|
||||
return $x['id'] ?? $x;
|
||||
}, $data['products']);
|
||||
$data['products'] = $id_products;
|
||||
}
|
||||
|
||||
$params['data'] = array_replace_recursive($default, $data);
|
||||
|
||||
$smarty->assign(['data' => json_encode($params)]);
|
||||
|
||||
echo $smarty->fetch('block.recommender.tpl');
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{if isset($luigisbox)}
|
||||
{* TODO: remove, temporary log *}
|
||||
<!-- {$luigisbox|var_dump} -->
|
||||
{/if}
|
||||
{if $products|count}
|
||||
<div class="products-recommender" {if $lbx_recommender}data-lbx-recommender="{$lbx_recommender}"{/if} style="margin-bottom: 50px;">
|
||||
{if !isset($title)}{$title = $recommender.title}{/if}
|
||||
{if $title}<h3 class="text-center">{$title}</h3>{/if}
|
||||
{include "block.products.tpl" listType="recommender" listId=$recommender.label}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1 @@
|
||||
<div class='recommender'><div data-recommender='{$data}'></div></div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="cs">
|
||||
<file id="products.cs">
|
||||
<unit id="734Z29u" name="recommender.show">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:79</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:175</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:83</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:182</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>recommender.show</source>
|
||||
<target>Další nejprodávanější</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="RKUqxW2" name="recommender.hide">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:171</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:178</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>recommender.hide</source>
|
||||
<target>Skrýt nejprodávanější</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de">
|
||||
<file id="products.de">
|
||||
<unit id="734Z29u" name="recommender.show">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:79</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:175</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:83</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:182</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>recommender.show</source>
|
||||
<target>Andere Bestseller</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="RKUqxW2" name="recommender.hide">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:171</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:178</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>recommender.hide</source>
|
||||
<target>Bestseller ausblenden</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="en">
|
||||
<file id="products.en">
|
||||
<unit id="734Z29u" name="recommender.show">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:79</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:175</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:83</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:182</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>recommender.show</source>
|
||||
<target>Other best sellers</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="RKUqxW2" name="recommender.hide">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:171</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:178</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>recommender.hide</source>
|
||||
<target>Hide best sellers</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="sk">
|
||||
<file id="products.sk">
|
||||
<unit id="734Z29u" name="recommender.show">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:79</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:175</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:83</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:182</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>recommender.show</source>
|
||||
<target>Ďalšie najpredávanejšie</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="RKUqxW2" name="recommender.hide">
|
||||
<notes>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/EditableContent/Recommender.php:171</note>
|
||||
<note category="file-source" priority="1">engine/bundles/KupShop/RecommendersBundle/Twig/Components/Recommender.php:178</note>
|
||||
</notes>
|
||||
<segment>
|
||||
<source>recommender.hide</source>
|
||||
<target>Skryť najpredávanejšie</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Resources\upgrade;
|
||||
|
||||
class RecommendersUpgrade extends \UpgradeNew
|
||||
{
|
||||
public function check_recommenders()
|
||||
{
|
||||
return $this->checkTableExists('recommenders');
|
||||
}
|
||||
|
||||
/** Create table 'recommenders' */
|
||||
public function upgrade_recommenders()
|
||||
{
|
||||
sqlQuery('
|
||||
create table recommenders (
|
||||
id int auto_increment primary key,
|
||||
position varchar(100) not null,
|
||||
label varchar(100) not null,
|
||||
products_count int not null,
|
||||
type varchar(100) not null,
|
||||
data text null
|
||||
)'
|
||||
);
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_RecommendersTitleColumn()
|
||||
{
|
||||
return $this->checkColumnExists('recommenders', 'title');
|
||||
}
|
||||
|
||||
/** Add title column to recommenders table */
|
||||
public function upgrade_RecommendersTitleColumn()
|
||||
{
|
||||
sqlQuery('ALTER TABLE recommenders ADD COLUMN title VARCHAR(100) DEFAULT NULL');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_RecommendersTranslationsTable(): bool
|
||||
{
|
||||
return findModule(\Modules::TRANSLATIONS) && $this->checkTableExists('recommenders_translations');
|
||||
}
|
||||
|
||||
/** Create recommenders_translations table */
|
||||
public function upgrade_RecommendersTranslationsTable()
|
||||
{
|
||||
sqlQuery('CREATE TABLE IF NOT EXISTS recommenders_translations
|
||||
(
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
id_recommender INT(11) NOT NULL,
|
||||
id_language VARCHAR(2) NOT NULL,
|
||||
id_admin INT DEFAULT NULL,
|
||||
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated DATETIME DEFAULT NULL,
|
||||
title VARCHAR(100) DEFAULT NULL,
|
||||
FOREIGN KEY (id_recommender)
|
||||
REFERENCES recommenders(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (id_language)
|
||||
REFERENCES languages(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
FOREIGN KEY (id_admin)
|
||||
REFERENCES admins(id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE UNIQUE INDEX recommenders_translations_id_recommender_id_language_uindex
|
||||
ON recommenders_translations (id_recommender, id_language);
|
||||
');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_RecommendersUniqueLabel()
|
||||
{
|
||||
return $this->checkConstraintExists('recommenders', 'unique_label');
|
||||
}
|
||||
|
||||
/** Add recommenders unique constraints on label */
|
||||
public function upgrade_RecommendersUniqueLabel()
|
||||
{
|
||||
sqlQuery('ALTER TABLE recommenders ADD CONSTRAINT unique_label UNIQUE (label);');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<div {{ attributes.defaults(stimulus_controller(this.shortName, {lazy: this.lazy})).defaults({
|
||||
class: this.container ? 'container' : ''
|
||||
}) }}>
|
||||
{% if not this.lazy and this.recommender %}
|
||||
{% if (this.title or this.recommender.title) and this.productList.count %}
|
||||
<p class="{{ this.title_class }}">{{ this.title ?: this.recommender.title }}</p>
|
||||
{% endif %}
|
||||
<div class="products {% if this.isCarousel %}carousel-wrapper{% endif %}">
|
||||
{% for item in this.productList %}
|
||||
{% if loop.index <= this.limit %}
|
||||
<twig:block name="content">
|
||||
<twig:ProductList:SquareItem id_product="{{ item.id }}"/>
|
||||
</twig:block>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if (this.productList|length > this.limit or this.productList|length > this.defaultLimit) and this.showBtn %}
|
||||
<div class="more-products">
|
||||
<button type="button" class="btn btn-more" data-action="live#action" data-live-action-param="toggleAction">{{ this.toggleText }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if outerScope.this.identifier is defined %}
|
||||
<style>
|
||||
{% if this.productList|length %}
|
||||
[data-target="#tabs-{{ outerScope.this.identifier }}"]::after {
|
||||
content: '{{ this.productList|length }}';
|
||||
}
|
||||
|
||||
{% else %}
|
||||
[data-target="#tabs-{{ outerScope.this.identifier }}"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
{% endif %}
|
||||
</style>
|
||||
{% endif %}
|
||||
<twig:Utils:GTM:ProductDataPrinter/>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
@use "sass:map";
|
||||
@use "@twig/scss/_global" as global;
|
||||
@use "@twig/scss/variables/_variables-components" as components;
|
||||
|
||||
$params: (
|
||||
"breakpoint": global.$md,
|
||||
"carousel-resp-margin": 0,
|
||||
"carousel-resp-gap": 0,
|
||||
"carousel-scrollbar-height": 0,
|
||||
"products-gap": global.$gap-width 30px,
|
||||
|
||||
"more-btn-padding": 10px 12px,
|
||||
"btn-transform-y": -9px,
|
||||
);
|
||||
|
||||
@if global-variable-exists(c-editablecontent-recommender, components) {
|
||||
$keys: map.keys(components.$c-editablecontent-recommender);
|
||||
@each $name in $keys {
|
||||
@if not map.get($params, $name) {
|
||||
@warn "Neexistující proměnná '#{$name}' v komponentě '$#{component("EditableContent:Recommender", "class")}'.";
|
||||
}
|
||||
}
|
||||
|
||||
$params: map.merge($params, components.$c-editablecontent-recommender);
|
||||
}
|
||||
|
||||
.#{component("EditableContent:Recommender", "class")} {
|
||||
width: 100%;
|
||||
margin-bottom: map.get($params, "margin-bottom");
|
||||
margin-top: map.get($params, "margin-bottom");
|
||||
container-type: inline-size;
|
||||
position: relative;
|
||||
|
||||
.products {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: map.get($params, "products-gap");
|
||||
}
|
||||
|
||||
.more-products {
|
||||
text-align: center;
|
||||
transform: translateY(map.get($params, "btn-transform-y"));
|
||||
|
||||
> button {
|
||||
padding: map.get($params, "more-btn-padding");
|
||||
|
||||
@include global.btn-alt-outline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@container (inline-size < #{map.get($params, "breakpoint")}) {
|
||||
@include global.container-query-selector(".carousel-wrapper") {
|
||||
flex-wrap: nowrap;
|
||||
overflow-y: auto;
|
||||
gap: map.get($params, "carousel-resp-gap");
|
||||
margin: map.get($params, "carousel-resp-margin");
|
||||
|
||||
&::-webkit-scrollbar,
|
||||
::-webkit-scrollbar {
|
||||
height: map.get($params, "carousel-scrollbar-height");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wpj-slider-wrapper {
|
||||
.#{component("EditableContent:Recommender", "class")} {
|
||||
@container (inline-size < #{map.get($params, "breakpoint")}) {
|
||||
@include global.container-query-selector(".carousel-wrapper") {
|
||||
overflow-y: unset;
|
||||
gap: map.get($params, "products-gap");
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
import { useIntersection } from 'stimulus-use';
|
||||
import { Component, getComponent } from '@symfony/ux-live-component';
|
||||
import { recommenderSetLazyInitData } from '../../Recommender/Recommender.1';
|
||||
|
||||
export default class extends Controller<HTMLDivElement> {
|
||||
static values = {
|
||||
lazy: Boolean,
|
||||
}
|
||||
|
||||
private component: Component;
|
||||
|
||||
declare lazyValue: boolean;
|
||||
declare observe: () => void;
|
||||
declare unobserve: () => void;
|
||||
|
||||
async initialize() {
|
||||
this.component = await getComponent(this.element);
|
||||
}
|
||||
|
||||
connect() {
|
||||
const [observe, unobserve] = useIntersection(this, { rootMargin: '300px 0px 300px 0px' })
|
||||
this.observe = observe
|
||||
this.unobserve = unobserve
|
||||
}
|
||||
|
||||
appear(entry, observer) {
|
||||
if(this.component === undefined){
|
||||
this.initialize().then(() => this.lazyRender());
|
||||
} else {
|
||||
this.lazyRender();
|
||||
}
|
||||
}
|
||||
|
||||
async lazyRender() {
|
||||
if (this.lazyValue && this.component) {
|
||||
this.component.set('lazy', false);
|
||||
recommenderSetLazyInitData(this.component);
|
||||
await this.component.render();
|
||||
const renderedEvent = new CustomEvent('wpj-recommender-rendered', {
|
||||
detail: {
|
||||
recommender: this.element,
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(renderedEvent);
|
||||
this.unobserve();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{% cache this.cacheKey ttl(this.cacheTTL) %}
|
||||
<div {{ attributes.defaults(stimulus_controller(this.shortName, {lazy: this.lazy})).defaults({
|
||||
class: this.additional_class ~ ' ' ~ (this.container ? 'container' : ''),
|
||||
'data-tracking-default': this.trackingData,
|
||||
}) }}>
|
||||
{% if not this.lazy and this.recommender and this.productList.count %}
|
||||
{% if (this.title or this.recommender.title) and this.productList.count %}
|
||||
<p class="{{ this.title_class }}">{{ this.title ?: this.recommender.title }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="wpj-slider-wrapper" data-slide-settings="{{ {buttons: true}|json_encode }}">
|
||||
<div class="wpj-slider-carousel" data-slide-init="true">
|
||||
<div class="products">
|
||||
{% for item in this.productList %}
|
||||
{% if not this.limit or loop.index <= this.limit %}
|
||||
<twig:block name="content">
|
||||
<twig:ProductList:SquareItem id_product="{{ item.id }}"/>
|
||||
</twig:block>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wpj-slider-controls">
|
||||
<button
|
||||
data-slide-controls="prev"
|
||||
class="btn">
|
||||
</button>
|
||||
<button
|
||||
data-slide-controls="next"
|
||||
class="btn">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<twig:Utils:GTM:ProductDataPrinter/>
|
||||
{% endif %}
|
||||
|
||||
{% if outerScope.this.identifier is defined and this.useOuterscopeIdentifier %}
|
||||
<style>
|
||||
{% if this.productList|length %}
|
||||
[data-target="#tabs-{{ outerScope.this.identifier }}"]::after {
|
||||
content: '{{ this.productList|length }}';
|
||||
}
|
||||
|
||||
{% else %}
|
||||
[data-target="#tabs-{{ outerScope.this.identifier }}"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
{% endif %}
|
||||
</style>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endcache %}
|
||||
@@ -0,0 +1,59 @@
|
||||
@use "sass:map";
|
||||
@use "@twig/scss/_global" as global;
|
||||
@use "@twig/scss/variables/_variables-components" as components;
|
||||
|
||||
$params: (
|
||||
"breakpoint-md": 500px,
|
||||
"breakpoint-lg": 810px,
|
||||
|
||||
"gap": global.$gap-width,
|
||||
"margin-top": global.$spacer,
|
||||
"margin-bottom": global.$spacer,
|
||||
|
||||
"item-size": calc(400px * .65),
|
||||
"item-size-md": calc(400px * .85),
|
||||
"item-size-lg": 400px,
|
||||
);
|
||||
|
||||
@if global-variable-exists(c-recommender-carousel, components) {
|
||||
$keys: map.keys(components.$c-recommender-carousel);
|
||||
@each $name in $keys {
|
||||
@if not map.get($params, $name) {
|
||||
@warn "Neexistující proměnná '#{$name}' v komponentě '$#{component("Recommender:Carousel", "class")}'.";
|
||||
}
|
||||
}
|
||||
|
||||
$params: map.merge($params, components.$c-recommender-carousel);
|
||||
}
|
||||
|
||||
.#{component("Recommender:Carousel", "class")} {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-top: map.get($params, "margin-top");
|
||||
margin-bottom: map.get($params, "margin-bottom");
|
||||
|
||||
.products {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: map.get($params, "gap");
|
||||
|
||||
.#{component("ProductList:SquareItem", "class")} {
|
||||
flex-basis: map.get($params, "item-size");
|
||||
max-width: map.get($params, "item-size");
|
||||
|
||||
@container (inline-size > #{map.get($params, "breakpoint-md")}) {
|
||||
@include global.container-query-selector("&") {
|
||||
flex-basis: map.get($params, "item-size-md");
|
||||
max-width: map.get($params, "item-size-md");
|
||||
}
|
||||
}
|
||||
|
||||
@container (inline-size > #{map.get($params, "breakpoint-lg")}) {
|
||||
@include global.container-query-selector("&") {
|
||||
flex-basis: map.get($params, "item-size-lg");
|
||||
max-width: map.get($params, "item-size-lg");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
import { useIntersection } from 'stimulus-use';
|
||||
import { Component, getComponent } from '@symfony/ux-live-component';
|
||||
|
||||
export default class extends Controller<HTMLDivElement> {
|
||||
static values = {
|
||||
lazy: Boolean
|
||||
};
|
||||
|
||||
private component: Component;
|
||||
|
||||
declare lazyValue: boolean;
|
||||
declare observe: () => void;
|
||||
declare unobserve: () => void;
|
||||
|
||||
async initialize() {
|
||||
this.component = await getComponent(this.element);
|
||||
}
|
||||
|
||||
connect() {
|
||||
const [observe, unobserve] = useIntersection(this, { rootMargin: '300px 0px 300px 0px' });
|
||||
this.observe = observe;
|
||||
this.unobserve = unobserve;
|
||||
}
|
||||
|
||||
appear(entry, observer) {
|
||||
if (this.component === undefined) {
|
||||
this.initialize().then(() => this.lazyRender());
|
||||
} else {
|
||||
this.lazyRender();
|
||||
}
|
||||
}
|
||||
|
||||
lazyRender() {
|
||||
if (this.lazyValue && this.component) {
|
||||
this.component.set('lazy', false);
|
||||
recommenderSetLazyInitData(this.component);
|
||||
this.component.render().then(() => {
|
||||
wpj.blocekRuntime.initializeSliders();
|
||||
});
|
||||
this.unobserve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function recommenderSetLazyInitData(component: Component) {
|
||||
if (component.getData('label') == 'last-visited') {
|
||||
const data = component.getData('data');
|
||||
const lastVisited = window.wpj.jsShop.lastVisitedProducts?.get(data.product ?? 0);
|
||||
component.set('data', { ...data, lastVisited });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{% cache this.cacheKey ttl(this.cacheTTL) %}
|
||||
|
||||
<div {{ attributes.defaults(stimulus_controller(this.shortName, {lazy: this.lazy})).defaults({
|
||||
class: this.additional_class ~ ' ' ~ (this.container ? 'container' : ''),
|
||||
'data-tracking-default': this.trackingData,
|
||||
}) }}>
|
||||
{% if not this.lazy and this.recommender and this.productList.count %}
|
||||
<div class="recommender-inner">
|
||||
{% if (this.title or this.recommender.title) and this.productList.count %}
|
||||
<p class="{{ this.title_class }}">{{ this.title ?: this.recommender.title }}</p>
|
||||
{% endif %}
|
||||
<div class="products {% if this.isCarousel %}carousel-wrapper{% endif %} {% if this.isSquareItemCarousel %}square-item-carousel{% endif %}">
|
||||
{% for item in this.productList %}
|
||||
{% if not this.limit or loop.index <= this.limit %}
|
||||
<twig:block name="content">
|
||||
<twig:ProductList:SquareItem id_product="{{ item.id }}"/>
|
||||
</twig:block>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if this.limit and (this.productList|length > this.limit or this.productList|length > this.defaultLimit) and this.showBtn %}
|
||||
<div class="more-products">
|
||||
<button type="button" class="btn btn-more" data-action="live#action" data-live-action-param="toggleAction">{{ this.toggleText }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<twig:Utils:GTM:ProductDataPrinter/>
|
||||
{% endif %}
|
||||
|
||||
{% if outerScope.this.identifier is defined and this.useOuterscopeIdentifier %}
|
||||
<style>
|
||||
{% if this.productList|length %}
|
||||
[data-target="#tabs-{{ outerScope.this.identifier }}"]::after {
|
||||
content: '{{ this.productList|length }}';
|
||||
}
|
||||
|
||||
{% else %}
|
||||
[data-target="#tabs-{{ outerScope.this.identifier }}"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
{% endif %}
|
||||
</style>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endcache %}
|
||||
@@ -0,0 +1,86 @@
|
||||
@use "sass:map";
|
||||
@use "@twig/scss/_global" as global;
|
||||
@use "@twig/scss/variables/_variables-components" as components;
|
||||
|
||||
$params: (
|
||||
"margin-top": 0,
|
||||
"margin-bottom": global.$spacer,
|
||||
|
||||
"carousel-resp-margin": 0,
|
||||
"carousel-scrollbar-height": 0,
|
||||
"products-gap": global.$gap-width 30px,
|
||||
|
||||
"square-item-carousel-resp-margin": 0 -31px 0 0,
|
||||
"square-item-carousel-resp-gap": 10px,
|
||||
|
||||
"more-btn-padding": 10px 12px,
|
||||
"more-btn-font-size": global.$font-size-smaller,
|
||||
"btn-transform-y": -9px,
|
||||
|
||||
"carousel-item-width": 1 0 75%,
|
||||
"carousel-item-border-bottom": none,
|
||||
);
|
||||
|
||||
@if global-variable-exists(c-recommender, components) {
|
||||
$keys: map.keys(components.$c-recommender);
|
||||
@each $name in $keys {
|
||||
@if not map.get($params, $name) {
|
||||
@warn "Neexistující proměnná '#{$name}' v komponentě '$#{component("Recommender", "class")}'.";
|
||||
}
|
||||
}
|
||||
|
||||
$params: map.merge($params, components.$c-recommender);
|
||||
}
|
||||
|
||||
.#{component("Recommender", "class")} {
|
||||
container-type: inline-size;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-top: map.get($params, "margin-top");
|
||||
margin-bottom: map.get($params, "margin-bottom");
|
||||
|
||||
.products {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: map.get($params, "products-gap");
|
||||
}
|
||||
|
||||
.more-products {
|
||||
text-align: center;
|
||||
transform: translateY(map.get($params, "btn-transform-y"));
|
||||
|
||||
> button {
|
||||
font-size: map.get($params, "more-btn-font-size");
|
||||
padding: map.get($params, "more-btn-padding");
|
||||
|
||||
@include global.btn-alt-outline;
|
||||
}
|
||||
}
|
||||
|
||||
@container (inline-size < #{global.$md}) {
|
||||
@include global.container-query-selector(".carousel-wrapper") {
|
||||
flex-wrap: nowrap;
|
||||
overflow-y: auto;
|
||||
margin: map.get($params, "carousel-resp-margin");
|
||||
|
||||
&::-webkit-scrollbar,
|
||||
::-webkit-scrollbar {
|
||||
height: map.get($params, "carousel-scrollbar-height");
|
||||
}
|
||||
|
||||
.#{component("ProductList:LineItem", "class")} {
|
||||
flex: map.get($params, "carousel-item-width");
|
||||
border-bottom: map.get($params, "carousel-item-border-bottom");
|
||||
}
|
||||
}
|
||||
|
||||
@include global.container-query-selector(".square-item-carousel") {
|
||||
margin: map.get($params, "square-item-carousel-resp-margin");
|
||||
gap: map.get($params, "square-item-carousel-resp-gap");
|
||||
}
|
||||
|
||||
button:not(.#{component("Product:AddToCartButton", "class")}) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
import { useIntersection } from 'stimulus-use';
|
||||
import { Component, getComponent } from '@symfony/ux-live-component';
|
||||
|
||||
export default class extends Controller<HTMLDivElement> {
|
||||
static values = {
|
||||
lazy: Boolean
|
||||
};
|
||||
|
||||
private component: Component;
|
||||
|
||||
declare lazyValue: boolean;
|
||||
declare observe: () => void;
|
||||
declare unobserve: () => void;
|
||||
|
||||
async initialize() {
|
||||
this.component = await getComponent(this.element);
|
||||
}
|
||||
|
||||
connect() {
|
||||
const [observe, unobserve] = useIntersection(this, { rootMargin: '300px 0px 300px 0px' });
|
||||
this.observe = observe;
|
||||
this.unobserve = unobserve;
|
||||
}
|
||||
|
||||
appear(entry, observer) {
|
||||
if (this.component === undefined) {
|
||||
this.initialize().then(() => this.lazyRender());
|
||||
} else {
|
||||
this.lazyRender();
|
||||
}
|
||||
}
|
||||
|
||||
lazyRender() {
|
||||
if (this.lazyValue && this.component) {
|
||||
this.component.set('lazy', false);
|
||||
recommenderSetLazyInitData(this.component);
|
||||
this.component.render();
|
||||
this.unobserve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function recommenderSetLazyInitData(component: Component) {
|
||||
if (component.getData('label') == 'last-visited') {
|
||||
const data = component.getData('data');
|
||||
const lastVisited = window.wpj.jsShop.lastVisitedProducts?.get(data.product ?? 0);
|
||||
component.set('data', { ...data, lastVisited });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends '@Components/storybook/preview.html.twig' %}
|
||||
|
||||
{% block component %}
|
||||
<twig:Recommender lazy="0" {{ ...args }}>
|
||||
{% if (args.type ?? null) == 'LineItem' %}
|
||||
<twig:ProductList:LineItem id_product="{{ item.id }}" show_buyform="1"/>
|
||||
{% elseif (args.type ?? null) == 'PreviewItem' %}
|
||||
<twig:ProductList:PreviewItem id_product="{{ item.id }}"/>
|
||||
{% else %}
|
||||
<twig:ProductList:SquareItem id_product="{{ item.id }}"/>
|
||||
{% endif %}
|
||||
</twig:Recommender>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,87 @@
|
||||
export default {
|
||||
title: "Recommender",
|
||||
parameters: {
|
||||
server: {
|
||||
id: "Recommender"
|
||||
},
|
||||
viewport: {
|
||||
viewports: {
|
||||
default: { name: "default", styles: { height: "100%", width: "1480px" }, type: "desktop" },
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
label: {
|
||||
control: "select",
|
||||
options: {
|
||||
"home-news (Úvod: novinky)": "home-news",
|
||||
"category-bestsellers (Nejprodávanější)": "category-bestsellers",
|
||||
"product-accessories (Produkt: příslušenství)": "product-accessories",
|
||||
"product-alternatives (Produkt: alternativy)": "product-alternatives",
|
||||
}
|
||||
},
|
||||
type: {
|
||||
control: "select",
|
||||
options: ["SquareItem", "LineItem", "PreviewItem"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Recommender = {
|
||||
name: "Novinky",
|
||||
args: {
|
||||
title: "Novinky",
|
||||
label: "home-news",
|
||||
type: "SquareItem",
|
||||
}
|
||||
};
|
||||
|
||||
export const Recommender2 = {
|
||||
name: "Nejprodávanější",
|
||||
args: {
|
||||
title: "Nejprodávanější",
|
||||
label: "category-bestsellers",
|
||||
type: "SquareItem",
|
||||
}
|
||||
};
|
||||
|
||||
export const Recommender3 = {
|
||||
name: "Nejprodávanější v kategorii",
|
||||
args: {
|
||||
title: "Nejprodávanější Capsle",
|
||||
label: "category-bestsellers",
|
||||
type: "LineItem",
|
||||
id_category: 436,
|
||||
}
|
||||
};
|
||||
|
||||
export const Recommender4 = {
|
||||
name: "Produkt - příslušenství",
|
||||
args: {
|
||||
title: "Příslušenství",
|
||||
label: "product-accessories",
|
||||
type: "LineItem",
|
||||
id_product: 6313,
|
||||
},
|
||||
parameters: {
|
||||
viewport: {
|
||||
defaultViewport: "tablet",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export const Recommender5 = {
|
||||
name: "Produkt - alternativy",
|
||||
args: {
|
||||
title: "Mohlo by se vám líbit",
|
||||
label: "product-alternatives",
|
||||
id_product: 6313,
|
||||
type: "LineItem",
|
||||
},
|
||||
parameters: {
|
||||
viewport: {
|
||||
defaultViewport: "mobile2",
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
974
bundles/KupShop/RecommendersBundle/Tests/RecommendersTest.json
Normal file
974
bundles/KupShop/RecommendersBundle/Tests/RecommendersTest.json
Normal file
@@ -0,0 +1,974 @@
|
||||
{
|
||||
"producers" : [
|
||||
{
|
||||
"id" : 90,
|
||||
"id_block" : null,
|
||||
"name" : "wpj",
|
||||
"top" : "Y",
|
||||
"photo" : "p90.png",
|
||||
"web" : "http://wpj.cz",
|
||||
"active" : "Y",
|
||||
"position" : 33,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"orderby" : "date",
|
||||
"orderdir" : "ASC",
|
||||
"filter_url" : "wpj",
|
||||
"title" : null,
|
||||
"data" : "{\"id_slider\":\"\"}",
|
||||
"date_updated" : "2024-02-12 17:02:38"
|
||||
},
|
||||
{
|
||||
"id" : 100,
|
||||
"id_block" : null,
|
||||
"name" : "Harmonia",
|
||||
"top" : "Y",
|
||||
"photo" : "",
|
||||
"web" : "http://",
|
||||
"active" : "Y",
|
||||
"position" : 42,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"orderby" : "date",
|
||||
"orderdir" : "ASC",
|
||||
"filter_url" : "harmonia",
|
||||
"title" : null,
|
||||
"data" : "{\"id_slider\":\"\"}",
|
||||
"date_updated" : "2024-02-12 17:02:38"
|
||||
}
|
||||
],
|
||||
"products" : [
|
||||
{
|
||||
"id" : 53,
|
||||
"id_block" : null,
|
||||
"title" : "Triko Classic Creator Black&blue UNI",
|
||||
"code" : "WY189",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "<p>Kolekce wpj 2021</p>",
|
||||
"parameters" : "",
|
||||
"price" : 156.1983,
|
||||
"price_for_discount" : 156.1983,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 25.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 500,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 11,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "D",
|
||||
"updated" : "2023-04-25 09:49:30",
|
||||
"date_added" : "2021-03-17 09:17:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"3\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 54,
|
||||
"id_block" : null,
|
||||
"title" : "Triko Classic Creator Black&black UNI",
|
||||
"code" : "SR086",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "<p>Kolekce wpj 2021</p>",
|
||||
"parameters" : "",
|
||||
"price" : 156.1983,
|
||||
"price_for_discount" : 156.1983,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 25.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 499,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 14,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "D",
|
||||
"updated" : "2023-08-29 11:31:00",
|
||||
"date_added" : "2021-03-17 09:17:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"3\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 56,
|
||||
"id_block" : null,
|
||||
"title" : "Mikina Classic Cruiser Black&white UNI",
|
||||
"code" : "NH294",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "<p>Kolekce wpj 2021</p>",
|
||||
"parameters" : "",
|
||||
"price" : 635.5372,
|
||||
"price_for_discount" : 444.8760,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 5.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 500,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 8,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "",
|
||||
"updated" : "2024-02-01 08:14:40",
|
||||
"date_added" : "2021-03-17 09:19:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : 10,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"50\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 58,
|
||||
"id_block" : null,
|
||||
"title" : "Množstevní sleva",
|
||||
"code" : "AI317",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "<p>Kolekce wpj 2021</p>",
|
||||
"parameters" : "",
|
||||
"price" : 635.5372,
|
||||
"price_for_discount" : 635.5372,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 0.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 399,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 30,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "",
|
||||
"updated" : "2023-12-19 08:40:28",
|
||||
"date_added" : "2021-03-17 09:20:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : 1,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"78\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 59,
|
||||
"id_block" : null,
|
||||
"title" : "Mikina Classic Cruiser Gray&black UNI",
|
||||
"code" : "MT119",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "<p>Kolekce wpj 2021</p>",
|
||||
"parameters" : "",
|
||||
"price" : 635.5372,
|
||||
"price_for_discount" : 635.5372,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 30.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 400,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 12,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "",
|
||||
"updated" : "2024-03-26 09:40:22",
|
||||
"date_added" : "2021-03-17 09:20:00",
|
||||
"figure" : "N",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"50\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 71,
|
||||
"id_block" : null,
|
||||
"title" : "Mikina Classic Cruiser Gray&black W",
|
||||
"code" : "XA840",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "<p>Kolekce wpj 2021</p>",
|
||||
"parameters" : "",
|
||||
"price" : 635.5372,
|
||||
"price_for_discount" : 444.8760,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 0.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 600,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 5,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "",
|
||||
"updated" : "2023-11-28 01:22:47",
|
||||
"date_added" : "2021-03-17 11:15:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"50\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 81,
|
||||
"id_block" : null,
|
||||
"title" : "Triko Mountains krátký rukáv",
|
||||
"code" : "VP497",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "",
|
||||
"parameters" : "",
|
||||
"price" : 238.8430,
|
||||
"price_for_discount" : 238.8430,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 0.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 300,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 6,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "L",
|
||||
"updated" : "2023-04-25 09:49:30",
|
||||
"date_added" : "2021-10-04 13:21:00",
|
||||
"figure" : "O",
|
||||
"show_raw_price" : "N",
|
||||
"position" : 0,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"50\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 82,
|
||||
"id_block" : null,
|
||||
"title" : "Mikina Mountains černá (s kapucí)",
|
||||
"code" : "TY733",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "",
|
||||
"parameters" : "",
|
||||
"price" : 676.8595,
|
||||
"price_for_discount" : 676.8595,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 30.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 480,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 31,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "",
|
||||
"updated" : "2023-06-29 14:16:11",
|
||||
"date_added" : "2021-10-04 13:22:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : 1,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"50\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 92,
|
||||
"id_block" : null,
|
||||
"title" : "Triko Stripes černé krátký rukáv",
|
||||
"code" : "SC424",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "",
|
||||
"parameters" : "",
|
||||
"price" : 172.7273,
|
||||
"price_for_discount" : 172.7273,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 0.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 400,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 15,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "L",
|
||||
"updated" : "2023-04-25 09:49:30",
|
||||
"date_added" : "2021-10-04 14:16:00",
|
||||
"figure" : "O",
|
||||
"show_raw_price" : "N",
|
||||
"position" : 2,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"50\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 93,
|
||||
"id_block" : null,
|
||||
"title" : "Triko Stripes bílé krátký rukáv",
|
||||
"code" : "PB660",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "",
|
||||
"parameters" : "",
|
||||
"price" : 172.7273,
|
||||
"price_for_discount" : 172.7273,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 20.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 12,
|
||||
"in_store" : 400,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 10,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "L",
|
||||
"updated" : "2024-03-17 22:00:24",
|
||||
"date_added" : "2021-10-04 14:17:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"3\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 100,
|
||||
"id_block" : null,
|
||||
"title" : "Funkční triko Stripes dlouhý rukáv",
|
||||
"code" : "ZW344",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "",
|
||||
"parameters" : "",
|
||||
"price" : 271.9008,
|
||||
"price_for_discount" : 286.0870,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 0.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 700,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 22,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "L",
|
||||
"updated" : "2023-04-25 09:49:30",
|
||||
"date_added" : "2021-10-04 14:47:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"50\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 6142,
|
||||
"id_block" : null,
|
||||
"title" : "Mikina Classic Cruiser Grey UNI",
|
||||
"code" : "HP768",
|
||||
"ean" : null,
|
||||
"short_descr" : "Šedá pánská mikina s kapucí Celio Squid Game",
|
||||
"long_descr" : "<p>Mikina z limitované edice Squid Game. Klasická černá mikina s drobnýmnápisem na rukávu. Kapuce se stahovací šňůrkou. Klokaní kapsa a dlouhérukávy s náplety.</p>",
|
||||
"parameters" : "",
|
||||
"price" : 1071.4286,
|
||||
"price_for_discount" : 730.4348,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 2,
|
||||
"discount" : 30.00000000,
|
||||
"producer" : 90,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 500,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 5,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "D",
|
||||
"updated" : "2023-12-05 06:08:28",
|
||||
"date_added" : "2022-05-26 08:17:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"recycling_fee\":\"\",\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"50\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : 10,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 6195,
|
||||
"id_block" : null,
|
||||
"title" : "ZK1",
|
||||
"code" : "WB813",
|
||||
"ean" : null,
|
||||
"short_descr" : "Anotace",
|
||||
"long_descr" : "<p>Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží Popis zboží </p>",
|
||||
"parameters" : "<p>Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis Rozšiřující popis </p>",
|
||||
"price" : 8928.5715,
|
||||
"price_for_discount" : 7826.0870,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 2,
|
||||
"discount" : 10.00000000,
|
||||
"producer" : 100,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 300,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 1,
|
||||
"delivery_time" : 30,
|
||||
"campaign" : "",
|
||||
"updated" : "2023-12-05 06:08:28",
|
||||
"date_added" : "2022-09-30 15:13:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"recycling_fee\":\"\",\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"13\"}",
|
||||
"weight" : 15.000000,
|
||||
"note" : "Nutná pomoc řidiči",
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 6206,
|
||||
"id_block" : null,
|
||||
"title" : "Arsenal FC dres 2022/23",
|
||||
"code" : "CZ234",
|
||||
"ean" : 2257,
|
||||
"short_descr" : "Velice hezký dres",
|
||||
"long_descr" : "<h3>CHLADIVÝ DRES Z FUNKČNÍCH MATERIÁLŮ INSPIROVANÝ KLASIKOU.</h3>\r\n\r\n<p>Bílé panely a modré detaily odkazují na nejúspěšnější éru Arsenalu. Tento autentický fotbalový dres adidas umožní hráčům špičkové výkony na hřišti. Je vyrobený z měkkého materiálu s chladivou technologií HEAT.RDY. Nažehlený klubový znak drží hmotnost dresu na minimu. Tento model je vyrobený z funkčních recyklovaných materiálů Primegreen.</p>",
|
||||
"parameters" : "<p>Bukayo Sakánek</p>",
|
||||
"price" : 2000.0000,
|
||||
"price_for_discount" : 1569.4215,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 2,
|
||||
"discount" : 5.05000000,
|
||||
"producer" : 90,
|
||||
"guarantee" : 6,
|
||||
"in_store" : 480,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 420,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "L,A,PS",
|
||||
"updated" : "2024-04-09 14:51:09",
|
||||
"date_added" : "2023-05-09 14:34:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : "Arsenal dres domácí 2022/23",
|
||||
"meta_description" : "??",
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"recycling_fee\":\"\",\"generate_coupon\":\"N\"}",
|
||||
"weight" : 0.100000,
|
||||
"note" : "#COYG",
|
||||
"price_buy" : 1900.0000,
|
||||
"date_stock_in" : "2023-05-09 23:00:00",
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : 95.0000,
|
||||
"height" : 84.0000,
|
||||
"depth" : 94.0000
|
||||
},
|
||||
{
|
||||
"id" : 6207,
|
||||
"id_block" : null,
|
||||
"title" : "Mikina Classic Cruiser Black&black W",
|
||||
"code" : "JF294",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "<p>Kolekce wpj 2021</p>",
|
||||
"parameters" : "",
|
||||
"price" : 635.5372,
|
||||
"price_for_discount" : 444.8760,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 30.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 99,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 14,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "PS,MS",
|
||||
"updated" : "2024-04-04 12:07:43",
|
||||
"date_added" : "2023-05-09 15:16:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"50\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 6208,
|
||||
"id_block" : null,
|
||||
"title" : "Kraťasy Arsenal FC 2022/23",
|
||||
"code" : "WQ240",
|
||||
"ean" : null,
|
||||
"short_descr" : "Stylové kraťáky",
|
||||
"long_descr" : "<p>Nejlepší na léto ale v zimě také dobrý ?</p>",
|
||||
"parameters" : "<p>Mám plesnivou korunní flašku a v tom lesní plody šťávičku<img alt=\"\" src=\"https://www.perplexity.ai/?__cf_chl_tk=sbMqBAdWegM7EJsZRbYt8A4SFcEfA8FmXYiOmhVZMug-1683711030-0-gaNycGzNDGU\" style=\"height:15px; width:15px\" /></p>",
|
||||
"price" : 1238.8430,
|
||||
"price_for_discount" : 1073.5537,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 13.34220000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 480,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 420,
|
||||
"delivery_time" : 3,
|
||||
"campaign" : "D,A,PS",
|
||||
"updated" : "2024-04-09 14:51:09",
|
||||
"date_added" : "2023-05-10 11:32:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : 1,
|
||||
"meta_title" : null,
|
||||
"meta_description" : "Sportovní kratasky, velice hezké, velice stylové, nejlepší tým v PL. Hráli zde legendy jako Tomáš Rosický, Petr Čech, Thierry Henry",
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"recycling_fee\":\"50\",\"generate_coupon\":\"N\"}",
|
||||
"weight" : 0.300000,
|
||||
"note" : "Trenýrky bílé Arsenal 2022/23 (fakt hezké)",
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : "2023-05-10 22:50:00",
|
||||
"bonus_points" : 1899,
|
||||
"show_in_search" : "Y",
|
||||
"width" : 50.0000,
|
||||
"height" : 55.0000,
|
||||
"depth" : 73.0000
|
||||
},
|
||||
{
|
||||
"id" : 6221,
|
||||
"id_block" : null,
|
||||
"title" : "Testovací produkt - triko",
|
||||
"code" : "PD552",
|
||||
"ean" : null,
|
||||
"short_descr" : "gherúhtrhúphotrhúo",
|
||||
"long_descr" : "<h2>Testovací produkt</h2>\r\n\r\n<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Cras pede libero, dapibus nec, pretium sit amet, tempor quis. Duis bibendum, lectus ut viverra rhoncus, dolor nunc faucibus libero, eget facilisis enim ipsum id lacus. Nunc tincidunt ante vitae massa. Morbi imperdiet, mauris ac auctor dictum, nisl ligula egestas nulla, et sollicitudin sem purus in lacus. In enim a arcu imperdiet malesuada. Morbi leo mi, nonummy eget tristique non, rhoncus non leo. Nulla turpis magna, cursus sit amet, suscipit a, interdum id, felis. Morbi leo mi, nonummy eget tristique non, rhoncus non leo. Mauris dictum facilisis augue. Etiam neque. Aliquam ornare wisi eu metus. Vestibulum fermentum tortor id mi. In rutrum. Aliquam erat volutpat. Maecenas fermentum, sem in pharetra pellentesque, velit turpis volutpat ante, in pharetra metus odio a lectus. Fusce tellus odio, dapibus id fermentum quis, suscipit id erat.</p>\r\n\r\n<p>Morbi scelerisque luctus velit. Nulla accumsan, elit sit amet varius semper, nulla mauris mollis quam, tempor suscipit diam nulla vel leo. Et harum quidem rerum facilis est et expedita distinctio. Morbi scelerisque luctus velit. Aliquam ante. Quisque tincidunt scelerisque libero. Nullam at arcu a est sollicitudin euismod. Vestibulum fermentum tortor id mi. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Maecenas sollicitudin. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Etiam bibendum elit eget erat. Fusce wisi. Maecenas libero. Donec vitae arcu. Donec quis nibh at felis congue commodo.</p>\r\n\r\n<h3><strong>Vlastnosti:</strong></h3>\r\n\r\n<ul>\r\n\t<li>ergergergerg</li>\r\n\t<li>gergerkgerúg</li>\r\n\t<li>gerkgerúgergkerú</li>\r\n\t<li>erkgúerúgkúerkú</li>\r\n\t<li>gergkergkúre</li>\r\n</ul>",
|
||||
"parameters" : "<p>jwpgwgoopgrjpgrjpgrjpogrp</p>\r\n\r\n<p>gerjgerjopgerjopgerjpogjerogjopergjp</p>",
|
||||
"price" : 0.0000,
|
||||
"price_for_discount" : null,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 0.00000000,
|
||||
"producer" : 90,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 600,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 0,
|
||||
"delivery_time" : -2,
|
||||
"campaign" : "PS",
|
||||
"updated" : "2024-02-16 13:45:06",
|
||||
"date_added" : "2023-05-22 12:51:00",
|
||||
"figure" : "N",
|
||||
"show_raw_price" : "N",
|
||||
"position" : null,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"recycling_fee\":\"\",\"generate_coupon\":\"N\"}",
|
||||
"weight" : 1.000000,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
},
|
||||
{
|
||||
"id" : 6266,
|
||||
"id_block" : null,
|
||||
"title" : "Předplatné expresso",
|
||||
"code" : "ZQ198",
|
||||
"ean" : null,
|
||||
"short_descr" : "",
|
||||
"long_descr" : "<p>Kolekce wpj 2021</p>",
|
||||
"parameters" : "",
|
||||
"price" : 635.5372,
|
||||
"price_for_discount" : 635.5372,
|
||||
"price_common" : 0.0000,
|
||||
"vat" : 1,
|
||||
"discount" : 0.00000000,
|
||||
"producer" : null,
|
||||
"guarantee" : 24,
|
||||
"in_store" : 0,
|
||||
"unit" : 1,
|
||||
"pieces_sold" : 0,
|
||||
"delivery_time" : 0,
|
||||
"campaign" : "",
|
||||
"updated" : "2023-09-05 00:49:18",
|
||||
"date_added" : "2023-08-25 12:47:00",
|
||||
"figure" : "Y",
|
||||
"show_raw_price" : "N",
|
||||
"position" : 1,
|
||||
"meta_title" : null,
|
||||
"meta_description" : null,
|
||||
"meta_keywords" : null,
|
||||
"show_in_feed" : "Y",
|
||||
"max_cpc" : 0,
|
||||
"in_store_min" : null,
|
||||
"data" : "{\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"78\",\"recycling_fee\":\"\"}",
|
||||
"weight" : null,
|
||||
"note" : null,
|
||||
"price_buy" : null,
|
||||
"date_stock_in" : null,
|
||||
"bonus_points" : null,
|
||||
"show_in_search" : "Y",
|
||||
"width" : null,
|
||||
"height" : null,
|
||||
"depth" : null
|
||||
}
|
||||
],
|
||||
"products_related_types" : [
|
||||
{
|
||||
"id" : 1,
|
||||
"name" : "Příslušenství"
|
||||
},
|
||||
{
|
||||
"id" : 5,
|
||||
"name" : "Alternativy"
|
||||
}
|
||||
],
|
||||
"products_related_dynamic" : [
|
||||
{
|
||||
"id" : 8,
|
||||
"id_products_related_types" : 5,
|
||||
"name" : "Mikiny",
|
||||
"data" : "{\"filter_selection\":{\"categories\":[\"394\"]},\"filter_match\":{\"products\":[\"6211\",\"6213\",\"6208\"],\"products_invert\":\"invert\",\"categories\":[\"394\"]}}",
|
||||
"created_at" : "2024-06-12 11:10:14",
|
||||
"updated_at" : "2024-06-12 11:10:19"
|
||||
},
|
||||
{
|
||||
"id" : 9,
|
||||
"id_products_related_types" : 1,
|
||||
"name" : "Mikiny (tricka)",
|
||||
"data" : "{\"filter_selection\":{\"categories\":[\"394\"]},\"filter_match\":{\"categories\":[\"395\"]}}",
|
||||
"created_at" : "2024-06-12 11:11:58",
|
||||
"updated_at" : "2024-06-12 11:12:04"
|
||||
}
|
||||
],
|
||||
"products_related" : [
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 53,
|
||||
"position" : 1000,
|
||||
"type" : 1,
|
||||
"id_products_related_dynamic" : 9
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 54,
|
||||
"position" : 1000,
|
||||
"type" : 1,
|
||||
"id_products_related_dynamic" : 9
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 56,
|
||||
"position" : 1000,
|
||||
"type" : 5,
|
||||
"id_products_related_dynamic" : 8
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 58,
|
||||
"position" : 1000,
|
||||
"type" : 5,
|
||||
"id_products_related_dynamic" : 8
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 59,
|
||||
"position" : 1000,
|
||||
"type" : 5,
|
||||
"id_products_related_dynamic" : 8
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 71,
|
||||
"position" : 1,
|
||||
"type" : 1,
|
||||
"id_products_related_dynamic" : null
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 71,
|
||||
"position" : 1000,
|
||||
"type" : 5,
|
||||
"id_products_related_dynamic" : 8
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 81,
|
||||
"position" : 1000,
|
||||
"type" : 1,
|
||||
"id_products_related_dynamic" : 9
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 82,
|
||||
"position" : 1000,
|
||||
"type" : 5,
|
||||
"id_products_related_dynamic" : 8
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 92,
|
||||
"position" : 1000,
|
||||
"type" : 1,
|
||||
"id_products_related_dynamic" : 9
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 93,
|
||||
"position" : 1000,
|
||||
"type" : 1,
|
||||
"id_products_related_dynamic" : 9
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 100,
|
||||
"position" : 1000,
|
||||
"type" : 1,
|
||||
"id_products_related_dynamic" : 9
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 6142,
|
||||
"position" : 1000,
|
||||
"type" : 5,
|
||||
"id_products_related_dynamic" : 8
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 6195,
|
||||
"position" : 1000,
|
||||
"type" : 1,
|
||||
"id_products_related_dynamic" : 9
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 6206,
|
||||
"position" : 1000,
|
||||
"type" : 1,
|
||||
"id_products_related_dynamic" : 9
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 6208,
|
||||
"position" : 1000,
|
||||
"type" : 1,
|
||||
"id_products_related_dynamic" : 9
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 6221,
|
||||
"position" : 1000,
|
||||
"type" : 1,
|
||||
"id_products_related_dynamic" : 9
|
||||
},
|
||||
{
|
||||
"id_top_product" : 6207,
|
||||
"id_rel_product" : 6266,
|
||||
"position" : 1000,
|
||||
"type" : 5,
|
||||
"id_products_related_dynamic" : 8
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\RecommendersBundle\Tests;
|
||||
|
||||
use KupShop\RecommendersBundle\Recommenders\RecommendersLocator;
|
||||
use KupShop\RecommendersBundle\Recommenders\RelatedProductsRecommender;
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
|
||||
class RecommendersTest extends \DatabaseTestCase
|
||||
{
|
||||
protected RecommendersLocator $recommendersLocator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->recommendersLocator = $this->get(RecommendersLocator::class);
|
||||
}
|
||||
|
||||
public function testRecommendersLocator(): void
|
||||
{
|
||||
$recommender = $this->recommendersLocator->getRecommender('related_product');
|
||||
$this->assertInstanceOf(RelatedProductsRecommender::class, $recommender);
|
||||
|
||||
$this->expectException(ServiceNotFoundException::class);
|
||||
$this->recommendersLocator->getRecommender('nesmysl');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideCases
|
||||
*/
|
||||
public function testRecommenders(string $type, array $data, array $config, array $expected): void
|
||||
{
|
||||
$recommender = $this->recommendersLocator->getRecommender($type);
|
||||
|
||||
$result = $recommender->execute($data, $config);
|
||||
$this->assertEquals($expected, $result->getProductIds());
|
||||
}
|
||||
|
||||
public function provideCases(): \Generator
|
||||
{
|
||||
yield 'recommender newest_products limit 5' => [
|
||||
'type' => 'newest_product',
|
||||
'data' => [],
|
||||
'config' => ['count' => 5],
|
||||
'result' => [6266, 6208, 6207, 6206, 6195],
|
||||
];
|
||||
yield 'recommender newest_products no limit' => [
|
||||
'type' => 'newest_product',
|
||||
'data' => [],
|
||||
'config' => [],
|
||||
'result' => [6266, 6208, 6207, 6206, 6195, 6142, 100, 93, 82, 71, 58, 56, 53, 54],
|
||||
];
|
||||
|
||||
yield 'recommender related_products type 1' => [
|
||||
'type' => 'related_product',
|
||||
'data' => ['product' => 6207],
|
||||
'config' => ['count' => 5, 'related_type' => 1],
|
||||
'result' => [71, 53, 54, 93, 100],
|
||||
];
|
||||
yield 'recommender related_products type 5' => [
|
||||
'type' => 'related_product',
|
||||
'data' => ['product' => ['id' => 6207]],
|
||||
'config' => ['count' => 5, 'related_type' => 5],
|
||||
'result' => [56, 58, 71, 82, 6142],
|
||||
];
|
||||
yield 'recommender related_products no related' => [
|
||||
'type' => 'related_product',
|
||||
'data' => ['product' => ['id' => 53]],
|
||||
'config' => ['count' => 5],
|
||||
'result' => [],
|
||||
];
|
||||
yield 'recommender related_products no related symmetric' => [
|
||||
'type' => 'related_product',
|
||||
'data' => ['product' => ['id' => 53]],
|
||||
'config' => ['count' => 5, 'symmetric' => true],
|
||||
'result' => [6207],
|
||||
];
|
||||
yield 'recommender related_products products' => [
|
||||
'type' => 'related_product',
|
||||
'data' => ['products' => [53, 100, 6207]],
|
||||
'config' => ['count' => 5, 'related_type' => 1, 'symmetric' => true],
|
||||
'result' => [6207, 71, 53, 54, 93],
|
||||
];
|
||||
}
|
||||
|
||||
public function getDataSet(): \IteratorAggregate
|
||||
{
|
||||
return $this->getJsonDataSetFromFile();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Translations;
|
||||
|
||||
use KupShop\I18nBundle\Translations\BaseTranslation;
|
||||
|
||||
class RecommendersTranslation extends BaseTranslation
|
||||
{
|
||||
protected $columns = [
|
||||
'title' => ['alias' => 'Nadpis', 'maxlength' => 100],
|
||||
];
|
||||
|
||||
protected $tableName = 'recommenders';
|
||||
|
||||
protected $tableAlias = 're';
|
||||
|
||||
protected $nameColumn = 'title';
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\RecommendersBundle\Twig\Components\EditableContent;
|
||||
|
||||
use KupShop\CatalogBundle\ProductList\ProductCollection;
|
||||
use KupShop\ComponentsBundle\Attributes\Blocek;
|
||||
use KupShop\ComponentsBundle\Attributes\BlocekAttribute;
|
||||
use KupShop\ComponentsBundle\Attributes\Component;
|
||||
use KupShop\ComponentsBundle\Attributes\Version;
|
||||
use KupShop\ComponentsBundle\GTM\PostMount\GTMDefaultData;
|
||||
use KupShop\ComponentsBundle\Interfaces\Microdata;
|
||||
use KupShop\ComponentsBundle\Twig\BaseComponent;
|
||||
use KupShop\ComponentsBundle\Twig\DataProvider\LiveComponentRenderTrait;
|
||||
use KupShop\ComponentsBundle\Twig\DataProvider\MicrodataProvider;
|
||||
use KupShop\ContentBundle\Util\BlocekTypes;
|
||||
use KupShop\RecommendersBundle\Util\RecommendersUtil;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveAction;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveListener;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveProp;
|
||||
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
||||
use Symfony\UX\TwigComponent\Attribute\PostMount;
|
||||
use Symfony\UX\TwigComponent\Attribute\PreMount;
|
||||
|
||||
#[AsLiveComponent(template: '@Recommenders/components/EditableContent/Recommender/Recommender.1.html.twig', method: 'get')]
|
||||
#[Component(1, [
|
||||
new Version(1),
|
||||
])]
|
||||
#[Blocek(title: 'Recommender')]
|
||||
class Recommender extends BaseComponent implements Microdata
|
||||
{
|
||||
use DefaultActionTrait;
|
||||
use LiveComponentRenderTrait;
|
||||
use GTMDefaultData;
|
||||
|
||||
#[Required]
|
||||
public \KupShop\ComponentsBundle\GTM\Events\Products $gtmClass;
|
||||
|
||||
#[LiveProp]
|
||||
public bool $isCarousel = true;
|
||||
#[LiveProp]
|
||||
public bool $showBtn = false;
|
||||
#[LiveProp]
|
||||
#[BlocekAttribute(default: 4, title: 'Počet produktů')]
|
||||
public int $limit = 4;
|
||||
#[LiveProp]
|
||||
public ?int $defaultLimit;
|
||||
#[LiveProp]
|
||||
public bool $isHidden = true;
|
||||
#[LiveProp]
|
||||
public string $toggleText;
|
||||
#[LiveProp]
|
||||
#[BlocekAttribute(default: 'last-visited', title: 'Label', type: BlocekTypes::SELECT, options: 'getSelectValues')]
|
||||
public string $label;
|
||||
#[LiveProp]
|
||||
public array $data = [];
|
||||
#[LiveProp]
|
||||
#[BlocekAttribute(title: 'Title')]
|
||||
public string $title = '';
|
||||
#[LiveProp(writable: true)]
|
||||
public bool $lazy = false;
|
||||
#[LiveProp]
|
||||
#[BlocekAttribute(default: true)]
|
||||
public bool $container = true;
|
||||
#[LiveProp]
|
||||
public string $title_class = 'h4';
|
||||
|
||||
protected array $recommender;
|
||||
protected ProductCollection $productList;
|
||||
|
||||
public function __construct(protected RecommendersUtil $recommendersUtil, private readonly TranslatorInterface $translator)
|
||||
{
|
||||
$this->toggleText = $this->translator->trans('recommender.show', [], 'products');
|
||||
}
|
||||
|
||||
#[PostMount]
|
||||
public function postMount(): void
|
||||
{
|
||||
$this->defaultLimit = $this->limit;
|
||||
}
|
||||
|
||||
public function __invoke(Request $request): ?Response
|
||||
{
|
||||
$data = json_decode($this->getRecommender()['data'] ?? '', true);
|
||||
$cache = intval($data['cache'] ?? 3600);
|
||||
|
||||
if ($cache > 0) {
|
||||
return $this->createResponse($request, $cache);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#[PreMount]
|
||||
public function preMount(array $data): array
|
||||
{
|
||||
if ($id_product = ($data['id_product'] ?? null)) {
|
||||
$data['data']['product'] = $id_product;
|
||||
unset($data['id_product']);
|
||||
}
|
||||
if ($id_category = ($data['id_category'] ?? null)) {
|
||||
$data['data']['section'] = (array) $id_category;
|
||||
unset($data['id_category']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getRecommender(): array
|
||||
{
|
||||
if (!isset($this->recommender)) {
|
||||
$this->recommender = $this->recommendersUtil->getRecommenderByLabel($this->label) ?: [];
|
||||
}
|
||||
|
||||
return $this->recommender;
|
||||
}
|
||||
|
||||
public function getDataRecommender()
|
||||
{
|
||||
return json_encode([
|
||||
'label' => $this->label,
|
||||
'data' => $this->data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCount(): ?int
|
||||
{
|
||||
if ($this->lazy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getProductList()->count();
|
||||
}
|
||||
|
||||
public function getProductList(): ?ProductCollection
|
||||
{
|
||||
if ($this->lazy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset($this->productList)) {
|
||||
$data = ['label' => $this->label, 'data' => $this->data, 'count' => $this->limit];
|
||||
$this->productList = $this->recommendersUtil->getRecommendersProducts($data);
|
||||
}
|
||||
|
||||
return $this->productList;
|
||||
}
|
||||
|
||||
#[LiveListener('recommenderLoad')]
|
||||
public function load()
|
||||
{
|
||||
$this->lazy = false;
|
||||
}
|
||||
|
||||
public function initMicrodata(): array
|
||||
{
|
||||
// Aby to mělo svůj scope
|
||||
return [MicrodataProvider::RECOMMENDER => []];
|
||||
}
|
||||
|
||||
#[LiveAction]
|
||||
public function toggleAction(): void
|
||||
{
|
||||
if ($this->isHidden) {
|
||||
$this->toggleText = $this->translator->trans('recommender.hide', [], 'products');
|
||||
$this->isHidden = false;
|
||||
$this->limit = 100;
|
||||
} else {
|
||||
$this->toggleText = $this->translator->trans('recommender.show', [], 'products');
|
||||
$this->isHidden = true;
|
||||
$this->limit = $this->defaultLimit;
|
||||
}
|
||||
}
|
||||
|
||||
public function getGTMPlaceholders(): array
|
||||
{
|
||||
return ['item_list_name' => 'recommender', 'item_list_id' => $this->getRecommender()['id'] ?? ''];
|
||||
}
|
||||
|
||||
public function getTrackingData(): string
|
||||
{
|
||||
if ($this->getProductList()) {
|
||||
return htmlentities(json_encode($this->gtmClass->getPushData($this)));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public static function getSelectValues(): array
|
||||
{
|
||||
return sqlQueryBuilder()
|
||||
->select('position, label')
|
||||
->from('recommenders')
|
||||
->execute()
|
||||
->fetchAllKeyValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\RecommendersBundle\Twig\Components;
|
||||
|
||||
use KupShop\CatalogBundle\ProductList\ProductCollection;
|
||||
use KupShop\ComponentsBundle\Attributes\Component;
|
||||
use KupShop\ComponentsBundle\Attributes\Version;
|
||||
use KupShop\ComponentsBundle\GTM\PostMount\GTMPlaceholdersData;
|
||||
use KupShop\ComponentsBundle\Interfaces\Microdata;
|
||||
use KupShop\ComponentsBundle\Twig\BaseComponent;
|
||||
use KupShop\ComponentsBundle\Twig\DataProvider\LiveComponentRenderTrait;
|
||||
use KupShop\ComponentsBundle\Twig\DataProvider\MicrodataProvider;
|
||||
use KupShop\KupShopBundle\Context\CacheContext;
|
||||
use KupShop\KupShopBundle\Util\Contexts;
|
||||
use KupShop\KupShopBundle\Util\StringUtil;
|
||||
use KupShop\RecommendersBundle\Util\RecommendersUtil;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveAction;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveListener;
|
||||
use Symfony\UX\LiveComponent\Attribute\LiveProp;
|
||||
use Symfony\UX\LiveComponent\DefaultActionTrait;
|
||||
use Symfony\UX\TwigComponent\Attribute\PostMount;
|
||||
use Symfony\UX\TwigComponent\Attribute\PreMount;
|
||||
|
||||
#[AsLiveComponent(template: '@Recommenders/components/Recommender/Recommender.1.html.twig', method: 'get')]
|
||||
#[Component(1, [
|
||||
new Version(1),
|
||||
])]
|
||||
class Recommender extends BaseComponent implements Microdata
|
||||
{
|
||||
use DefaultActionTrait;
|
||||
use LiveComponentRenderTrait;
|
||||
use GTMPlaceholdersData;
|
||||
|
||||
#[Required]
|
||||
public \KupShop\ComponentsBundle\GTM\Events\Products $gtmClass;
|
||||
|
||||
#[LiveProp]
|
||||
public bool $isCarousel = true;
|
||||
#[LiveProp]
|
||||
public bool $isSquareItemCarousel = false;
|
||||
|
||||
#[LiveProp]
|
||||
public bool $showBtn = false;
|
||||
#[LiveProp]
|
||||
public ?int $limit = null;
|
||||
#[LiveProp]
|
||||
public ?int $defaultLimit = null;
|
||||
#[LiveProp]
|
||||
public bool $isHidden = true;
|
||||
#[LiveProp]
|
||||
public string $toggleText;
|
||||
#[LiveProp]
|
||||
public string $label;
|
||||
#[LiveProp(writable: true)]
|
||||
public array $data = [];
|
||||
#[LiveProp]
|
||||
public string $title = '';
|
||||
#[LiveProp(writable: true)]
|
||||
public bool $lazy = true;
|
||||
#[LiveProp]
|
||||
public bool $container = true;
|
||||
#[LiveProp]
|
||||
public string $title_class = 'h4';
|
||||
#[LiveProp]
|
||||
public string $additional_class = '';
|
||||
#[LiveProp]
|
||||
public bool $useOuterscopeIdentifier = true;
|
||||
|
||||
protected array $recommender;
|
||||
public ?string $propsCacheKey = null;
|
||||
|
||||
protected ProductCollection $productList;
|
||||
|
||||
public function __construct(protected RecommendersUtil $recommendersUtil, private readonly TranslatorInterface $translator)
|
||||
{
|
||||
$this->toggleText = $this->translator->trans('recommender.show', [], 'products');
|
||||
}
|
||||
|
||||
#[PostMount]
|
||||
public function postMount(): void
|
||||
{
|
||||
if ($this->limit) {
|
||||
$this->defaultLimit = $this->limit;
|
||||
}
|
||||
}
|
||||
|
||||
public function __invoke(Request $request): ?Response
|
||||
{
|
||||
$cache = $this->getRecommenderTTL();
|
||||
|
||||
if ($cache > 0) {
|
||||
return $this->createResponse($request, $cache);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#[PreMount]
|
||||
public function preMount(array $data): array
|
||||
{
|
||||
if ($id_product = ($data['id_product'] ?? null)) {
|
||||
$data['data']['product'] = $id_product;
|
||||
unset($data['id_product']);
|
||||
}
|
||||
if ($id_category = ($data['id_category'] ?? null)) {
|
||||
$data['data']['section'] = (array) $id_category;
|
||||
unset($data['id_category']);
|
||||
}
|
||||
|
||||
$data['propsCacheKey'] ??= serialize($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getRecommender(): array
|
||||
{
|
||||
if (!isset($this->recommender)) {
|
||||
$this->recommender = $this->recommendersUtil->getRecommenderByLabel($this->label) ?: [];
|
||||
}
|
||||
|
||||
return $this->recommender;
|
||||
}
|
||||
|
||||
public function getDataRecommender()
|
||||
{
|
||||
return json_encode([
|
||||
'label' => $this->label,
|
||||
'data' => $this->data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCount(): ?int
|
||||
{
|
||||
if ($this->lazy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getProductList()->count();
|
||||
}
|
||||
|
||||
public function getProductList(): ?ProductCollection
|
||||
{
|
||||
if ($this->lazy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset($this->productList)) {
|
||||
$data = ['label' => $this->label, 'data' => $this->data];
|
||||
$this->productList = $this->recommendersUtil->getRecommendersProducts($data);
|
||||
}
|
||||
|
||||
return $this->productList;
|
||||
}
|
||||
|
||||
#[LiveListener('recommenderLoad')]
|
||||
public function load()
|
||||
{
|
||||
$this->lazy = false;
|
||||
}
|
||||
|
||||
public function initMicrodata(): array
|
||||
{
|
||||
// Aby to mělo svůj scope
|
||||
return [MicrodataProvider::RECOMMENDER => []];
|
||||
}
|
||||
|
||||
#[LiveAction]
|
||||
public function toggleAction(): void
|
||||
{
|
||||
if ($this->isHidden) {
|
||||
$this->toggleText = $this->translator->trans('recommender.hide', [], 'products');
|
||||
$this->isHidden = false;
|
||||
$this->limit = 100;
|
||||
} else {
|
||||
$this->toggleText = $this->translator->trans('recommender.show', [], 'products');
|
||||
$this->isHidden = true;
|
||||
$this->limit = $this->defaultLimit;
|
||||
}
|
||||
}
|
||||
|
||||
public function getGTMPlaceholders(): array
|
||||
{
|
||||
return ['item_list_name' => 'recommender', 'item_list_id' => $this->getRecommender()['id'] ?? ''];
|
||||
}
|
||||
|
||||
public function getTrackingData(): string
|
||||
{
|
||||
if ($this->getProductList()) {
|
||||
return htmlentities(json_encode($this->gtmClass->getPushData($this), JSON_NUMERIC_CHECK | JSON_HEX_APOS));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function getRecommenderTTL(): int
|
||||
{
|
||||
$data = json_decode($this->getRecommender()['data'] ?? '', true);
|
||||
|
||||
return intval($data['cache'] ?? 3600);
|
||||
}
|
||||
|
||||
public function getCacheKey(): string
|
||||
{
|
||||
if ($this->lazy || !$this->propsCacheKey) {
|
||||
return '_skip';
|
||||
}
|
||||
|
||||
$cacheKey = [
|
||||
Contexts::get(CacheContext::class)->getKey([
|
||||
CacheContext::TYPE_TEXT,
|
||||
CacheContext::TYPE_PRICE,
|
||||
CacheContext::TYPE_AVAILABILITY,
|
||||
]),
|
||||
static::class,
|
||||
$this->version,
|
||||
$this->propsCacheKey,
|
||||
];
|
||||
|
||||
return StringUtil::slugify(implode('_', $cacheKey));
|
||||
}
|
||||
|
||||
public function getCacheTTL(): int
|
||||
{
|
||||
return $this->lazy || !$this->propsCacheKey ? 0 : $this->getRecommenderTTL();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\RecommendersBundle\Twig\Components\Recommender;
|
||||
|
||||
use KupShop\ComponentsBundle\Attributes\Component;
|
||||
use KupShop\ComponentsBundle\Attributes\Version;
|
||||
use KupShop\RecommendersBundle\Twig\Components\Recommender;
|
||||
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
|
||||
|
||||
#[AsLiveComponent(template: '@Recommenders/components/Recommender/Carousel/Carousel.1.html.twig')]
|
||||
#[Component(1, [
|
||||
new Version(1),
|
||||
])]
|
||||
class Carousel extends Recommender
|
||||
{
|
||||
}
|
||||
93
bundles/KupShop/RecommendersBundle/Util/RecommendersUtil.php
Normal file
93
bundles/KupShop/RecommendersBundle/Util/RecommendersUtil.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\RecommendersBundle\Util;
|
||||
|
||||
use KupShop\CatalogBundle\ProductList\ProductCollection;
|
||||
use KupShop\ComponentsBundle\Twig\DataProvider\ProductDataProvider;
|
||||
use KupShop\LuigisBoxBundle\Recommenders\LuigisBoxRecommender;
|
||||
use KupShop\RecommendersBundle\Recommenders\RecommendersLocator;
|
||||
use KupShop\RecommendersBundle\Translations\RecommendersTranslation;
|
||||
use Query\Operator;
|
||||
use Query\Translation;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Twig\Environment;
|
||||
|
||||
class RecommendersUtil
|
||||
{
|
||||
public function __construct(
|
||||
protected RecommendersLocator $recommendersLocator,
|
||||
protected ?ProductDataProvider $productDataProvider,
|
||||
protected Environment $twig,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRecommenderByLabel(string $label)
|
||||
{
|
||||
return sqlQueryBuilder()->select('re.*')->from('recommenders', 're')
|
||||
->where(Operator::equals(['label' => $label]))
|
||||
->andWhere(Translation::coalesceTranslatedFields(RecommendersTranslation::class))
|
||||
->execute()->fetchAssociative();
|
||||
}
|
||||
|
||||
public function getRecommendersProducts(array &$data): ProductCollection
|
||||
{
|
||||
if (empty($data['label'])) {
|
||||
throw new NotFoundHttpException('Empty label');
|
||||
}
|
||||
|
||||
$labelRecommender = $this->getRecommenderByLabel($data['label']);
|
||||
if ($type = $labelRecommender['type'] ?? null) {
|
||||
$recommender = $this->recommendersLocator->getRecommender($type);
|
||||
$config = $recommender->getConfigurationVariables($labelRecommender);
|
||||
|
||||
$productList = $recommender->execute($data['data'] ?? [], $config);
|
||||
if (findModule(\Modules::COMPONENTS)) {
|
||||
$this->productDataProvider->addProducts($productList->toArray());
|
||||
}
|
||||
if ($recommender instanceof LuigisBoxRecommender) {
|
||||
$data['luigisbox'] = $recommender->getLogData();
|
||||
}
|
||||
|
||||
return $productList;
|
||||
} else {
|
||||
throw new NotFoundHttpException("Recommender doesn't exist");
|
||||
}
|
||||
}
|
||||
|
||||
public function renderRecommendersProducts(array $data): string
|
||||
{
|
||||
$recommender = $this->getRecommenderByLabel($data['label']);
|
||||
try {
|
||||
$productList = $this->getRecommendersProducts($data);
|
||||
} catch (\Throwable $e) {
|
||||
if (getAdminUser()) {
|
||||
// show error to admin
|
||||
return "Recommender \"{$data['label']}\" error: ".$e->getMessage();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
if (findModule(\Modules::COMPONENTS)) {
|
||||
return $this->twig->createTemplate('{{ component(name, data) }}')
|
||||
->render([
|
||||
'name' => 'ProductList',
|
||||
'data' => [
|
||||
'productList' => $productList,
|
||||
'list_show' => $data['list_show'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$smarty = createSmarty(false, true);
|
||||
$smarty->assign($data['data'] ?? '');
|
||||
$smarty->assign('products', $productList);
|
||||
$smarty->assign('recommender', $recommender);
|
||||
if (isset($data['luigisbox'])) {
|
||||
$smarty->assign('luigisbox', $data['luigisbox']);
|
||||
$smarty->assign('lbx_recommender', $data['luigisbox']['response'][0]['recommender'] ?? null);
|
||||
}
|
||||
|
||||
return $smarty->fetch($data['template'] ?? 'block.products.recommender.tpl');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user