diff --git a/CLAUDE.md b/CLAUDE.md index 64527e51..7a6e88be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,3 +67,9 @@ When creating or modifying overrides, PrestaShop also needs to rebuild the class - Module DB tables use `_DB_PREFIX_` constant (typically `ps_`). - PrestaShop hooks are the integration point — prefer hooks over direct core edits. - The `admin658c34/` directory is the custom admin panel path (security through obscurity). + +## Custom Assistant Command + +- If the user writes `zapisz-changelog`, create or update monthly changelog file `changelog/YYYY-MM.md` (based on current date). +- Add an entry for the current day with a concise summary of code changes made in the current session. +- Include touched file paths and relevant line references where possible. diff --git a/changelog/2026-03.md b/changelog/2026-03.md new file mode 100644 index 00000000..d0201139 --- /dev/null +++ b/changelog/2026-03.md @@ -0,0 +1,51 @@ +# Changelog 2026-03 + +## 2026-03-20 + +### Zmiany funkcjonalne +- Podmieniono logikę modułu `ps_categoryproducts` na wyświetlanie produktów z tej samej cechy `Seria` (`id_feature = 8`) zamiast produktów z tej samej kategorii, gdy produkt ma ustawioną cechę serii. +- Dla produktów z serią włączono tryb restrykcyjny: brak fallbacku do kategorii, jeśli nie ma dopasowań po serii. +- Dodano i dopracowano karuzelę dla sekcji produktów powiązanych (nawigacja, responsywność, scroll poziomy). +- Zmieniono nagłówek sekcji na: `Produkty z tej samej serii`. + +### Najważniejsze modyfikacje w kodzie + +1. `modules/ps_categoryproducts/ps_categoryproducts.php` +- Stała cechy serii: + - `SERIES_FEATURE_ID = 8` ([modules/ps_categoryproducts/ps_categoryproducts.php:44](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\ps_categoryproducts.php:44)) +- Główna logika danych widgetu: + - pobieranie wszystkich wartości serii i filtrowanie po nich ([modules/ps_categoryproducts/ps_categoryproducts.php:213](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\ps_categoryproducts.php:213)) +- Rejestracja assetów karuzeli (z fallbackiem): + - `registerCarouselAssets()` ([modules/ps_categoryproducts/ps_categoryproducts.php:287](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\ps_categoryproducts.php:287)) +- Pobieranie wartości serii produktu: + - `getSeriesFeatureValueIds()` ([modules/ps_categoryproducts/ps_categoryproducts.php:381](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\ps_categoryproducts.php:381)) +- Pobieranie produktów z tej samej serii: + - `getProductsByFeatureValue()` ([modules/ps_categoryproducts/ps_categoryproducts.php:398](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\ps_categoryproducts.php:398)) +- Wersjonowanie cache bloku: + - `v4_strict_series_masterdb` ([modules/ps_categoryproducts/ps_categoryproducts.php:490](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\ps_categoryproducts.php:490)) + +2. `themes/InterBlue/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl` +- Nagłówek sekcji dla serii: + - `Produkty z tej samej serii` ([themes/InterBlue/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl:42](c:\visual studio code\projekty\interblue.pl\themes\InterBlue\modules\ps_categoryproducts\views\templates\hook\ps_categoryproducts.tpl:42)) +- Markup karuzeli i klasy JS: + - wrapper i nawigacja ([themes/InterBlue/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl:39](c:\visual studio code\projekty\interblue.pl\themes\InterBlue\modules\ps_categoryproducts\views\templates\hook\ps_categoryproducts.tpl:39), [themes/InterBlue/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl:54](c:\visual studio code\projekty\interblue.pl\themes\InterBlue\modules\ps_categoryproducts\views\templates\hook\ps_categoryproducts.tpl:54), [themes/InterBlue/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl:66](c:\visual studio code\projekty\interblue.pl\themes\InterBlue\modules\ps_categoryproducts\views\templates\hook\ps_categoryproducts.tpl:66)) +- Inline CSS/JS opakowany w `{literal}`: + - style ([themes/InterBlue/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl:25](c:\visual studio code\projekty\interblue.pl\themes\InterBlue\modules\ps_categoryproducts\views\templates\hook\ps_categoryproducts.tpl:25)) + - skrypt ([themes/InterBlue/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl:73](c:\visual studio code\projekty\interblue.pl\themes\InterBlue\modules\ps_categoryproducts\views\templates\hook\ps_categoryproducts.tpl:73)) + +3. `modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl` +- Analogiczne zmiany jak w override motywu: + - `{literal}` + markup karuzeli + JS ([modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl:25](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\views\templates\hook\ps_categoryproducts.tpl:25), [modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl:39](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\views\templates\hook\ps_categoryproducts.tpl:39), [modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl:77](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\views\templates\hook\ps_categoryproducts.tpl:77)) + +4. Dodatkowe pliki assetów karuzeli +- CSS: + - [modules/ps_categoryproducts/views/css/carousel.css](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\views\css\carousel.css) +- JS: + - [modules/ps_categoryproducts/views/js/carousel.js](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\views\js\carousel.js) +- Zabezpieczenie katalogów: + - [modules/ps_categoryproducts/views/css/index.php](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\views\css\index.php) + - [modules/ps_categoryproducts/views/js/index.php](c:\visual studio code\projekty\interblue.pl\modules\ps_categoryproducts\views\js\index.php) + +### Uwagi operacyjne +- Po wdrożeniu zmian wymagane było czyszczenie cache PrestaShop, aby wymusić przebudowę bloku `ps_categoryproducts`. + diff --git a/modules/ps_categoryproducts/ps_categoryproducts.php b/modules/ps_categoryproducts/ps_categoryproducts.php index 88bc1830..06d7e30f 100644 --- a/modules/ps_categoryproducts/ps_categoryproducts.php +++ b/modules/ps_categoryproducts/ps_categoryproducts.php @@ -41,6 +41,8 @@ use PrestaShop\PrestaShop\Core\Product\Search\SortOrder; class Ps_Categoryproducts extends Module implements WidgetInterface { + const SERIES_FEATURE_ID = 8; + protected $html; protected $templateFile; @@ -213,15 +215,33 @@ class Ps_Categoryproducts extends Module implements WidgetInterface $params = $this->getInformationFromConfiguration($configuration); if ($params) { + $products = array(); + $seriesFeatureValueIds = $this->getSeriesFeatureValueIds((int) $params['id_product']); + if (!empty($seriesFeatureValueIds)) { + $products = $this->getProductsByFeatureValue( + (int) $params['id_product'], + (int) self::SERIES_FEATURE_ID, + $seriesFeatureValueIds + ); + if (empty($products)) { + return false; + } - $products = $this->getCategoryProducts($params['id_product'], $params['id_category']); - - if (!empty($products)) { return array( 'products' => $products, + 'products_source' => 'series', ); } + $products = $this->getCategoryProducts($params['id_product'], $params['id_category']); + if (empty($products)) { + return false; + } + + return array( + 'products' => $products, + 'products_source' => 'category', + ); } return false; @@ -233,6 +253,7 @@ class Ps_Categoryproducts extends Module implements WidgetInterface if ($params) { if ((int)Configuration::get('CATEGORYPRODUCTS_DISPLAY_PRODUCTS') > 0) { + $this->registerCarouselAssets(); // Need variables only if this template isn't cached if (!$this->isCached($this->templateFile, $params['cache_id'])) { @@ -263,6 +284,39 @@ class Ps_Categoryproducts extends Module implements WidgetInterface return false; } + private function registerCarouselAssets() + { + if (empty($this->context->controller)) { + return; + } + + if (method_exists($this->context->controller, 'registerStylesheet')) { + $this->context->controller->registerStylesheet( + 'module-ps-categoryproducts-carousel', + 'modules/' . $this->name . '/views/css/carousel.css', + array( + 'media' => 'all', + 'priority' => 150, + ) + ); + } else { + $this->context->controller->addCSS($this->_path . 'views/css/carousel.css'); + } + + if (method_exists($this->context->controller, 'registerJavascript')) { + $this->context->controller->registerJavascript( + 'module-ps-categoryproducts-carousel', + 'modules/' . $this->name . '/views/js/carousel.js', + array( + 'position' => 'bottom', + 'priority' => 150, + ) + ); + } else { + $this->context->controller->addJS($this->_path . 'views/js/carousel.js'); + } + } + private function getCategoryProducts($idProduct, $idCategory) { $category = new Category($idCategory); @@ -324,6 +378,98 @@ class Ps_Categoryproducts extends Module implements WidgetInterface return $productsForTemplate; } + private function getSeriesFeatureValueIds($idProduct) + { + $sql = 'SELECT DISTINCT fp.`id_feature_value` + FROM `' . _DB_PREFIX_ . 'feature_product` fp + WHERE fp.`id_product` = ' . (int) $idProduct . ' + AND fp.`id_feature` = ' . (int) self::SERIES_FEATURE_ID . ' + AND fp.`id_feature_value` > 0 + ORDER BY fp.`id_feature_value` ASC'; + + $rows = Db::getInstance()->executeS($sql); + if (empty($rows)) { + return array(); + } + + return array_map('intval', array_column($rows, 'id_feature_value')); + } + + private function getProductsByFeatureValue($idProduct, $idFeature, array $idFeatureValues) + { + $nProducts = (int) Configuration::get('CATEGORYPRODUCTS_DISPLAY_PRODUCTS'); + if ($nProducts <= 0) { + return array(); + } + if (empty($idFeatureValues)) { + return array(); + } + + $idShop = (int) $this->context->shop->id; + $idLang = (int) $this->context->language->id; + if ($idShop <= 0) { + $idShop = (int) Configuration::get('PS_SHOP_DEFAULT'); + } + $idFeatureValues = array_filter(array_map('intval', $idFeatureValues)); + if (empty($idFeatureValues)) { + return array(); + } + + $sql = 'SELECT DISTINCT fp.`id_product` + FROM `' . _DB_PREFIX_ . 'feature_product` fp + INNER JOIN `' . _DB_PREFIX_ . 'product_shop` product_shop + ON product_shop.`id_product` = fp.`id_product` + AND product_shop.`id_shop` = ' . (int) $idShop . ' + INNER JOIN `' . _DB_PREFIX_ . 'product_lang` pl + ON pl.`id_product` = fp.`id_product` + AND pl.`id_shop` = ' . (int) $idShop . ' + AND pl.`id_lang` = ' . (int) $idLang . ' + WHERE fp.`id_feature` = ' . (int) $idFeature . ' + AND fp.`id_feature_value` IN (' . implode(',', $idFeatureValues) . ') + AND fp.`id_product` != ' . (int) $idProduct . ' + AND product_shop.`active` = 1 + AND product_shop.`visibility` IN ("both", "catalog") + ORDER BY RAND() + LIMIT ' . (int) $nProducts; + + $rows = Db::getInstance()->executeS($sql); + if (empty($rows)) { + return array(); + } + + $ids = array_map('intval', array_column($rows, 'id_product')); + if (empty($ids)) { + return array(); + } + + $showPrice = (bool) Configuration::get('CATEGORYPRODUCTS_DISPLAY_PRICE'); + $assembler = new ProductAssembler($this->context); + $presenterFactory = new ProductPresenterFactory($this->context); + $presentationSettings = $presenterFactory->getPresentationSettings(); + $presenter = new ProductListingPresenter( + new ImageRetriever( + $this->context->link + ), + $this->context->link, + new PriceFormatter(), + new ProductColorsRetriever(), + $this->context->getTranslator() + ); + + $presentationSettings->showPrices = $showPrice; + + $productsForTemplate = array(); + foreach ($ids as $id) { + $productsForTemplate[] = $presenter->present( + $presentationSettings, + $assembler->assembleProduct(array('id_product' => (int) $id)), + $this->context->language + ); + } + + return $productsForTemplate; + } + private function getInformationFromConfiguration($configuration) { if (empty($configuration['product'])) { @@ -341,7 +487,7 @@ class Ps_Categoryproducts extends Module implements WidgetInterface if (!empty($id_product) && !empty($id_category)) { - $cache_id = 'ps_categoryproducts|'.$id_product.'|'.$id_category; + $cache_id = 'ps_categoryproducts|v4_strict_series_masterdb|'.$id_product.'|'.$id_category; return array( 'id_product' => $id_product, diff --git a/modules/ps_categoryproducts/views/css/carousel.css b/modules/ps_categoryproducts/views/css/carousel.css new file mode 100644 index 00000000..ed0bd6b1 --- /dev/null +++ b/modules/ps_categoryproducts/views/css/carousel.css @@ -0,0 +1,93 @@ +.category-products .category-products-carousel-wrapper { + position: relative; +} + +.category-products .products.category-products-carousel { + display: flex !important; + flex-wrap: nowrap !important; + gap: 16px; + overflow-x: auto !important; + overflow-y: hidden !important; + scroll-behavior: smooth; + scroll-snap-type: x mandatory; + padding: 0; + margin: 0; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.category-products .products.category-products-carousel::-webkit-scrollbar { + display: none; +} + +.category-products .products.category-products-carousel > article.product-miniature { + flex: 0 0 calc(25% - 12px) !important; + max-width: calc(25% - 12px) !important; + scroll-snap-align: start; +} + +.category-products .category-products-carousel__nav { + position: absolute; + top: 42%; + transform: translateY(-50%); + z-index: 20; + width: 38px; + height: 38px; + border: 0; + border-radius: 50%; + background: #ffffff; + color: #112c50; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: opacity 0.2s ease, background-color 0.2s ease; +} + +.category-products .category-products-carousel__nav:hover { + background: #f2f7ff; +} + +.category-products .category-products-carousel__nav[disabled] { + opacity: 0.35; + cursor: default; +} + +.category-products .category-products-carousel__nav--prev { + left: -12px; +} + +.category-products .category-products-carousel__nav--next { + right: -12px; +} + +.category-products .category-products-carousel__nav span { + font-size: 28px; + line-height: 1; +} + +@media (max-width: 1199.98px) { + .category-products .products.category-products-carousel > article.product-miniature { + flex: 0 0 calc(33.3333% - 11px) !important; + max-width: calc(33.3333% - 11px) !important; + } +} + +@media (max-width: 767.98px) { + .category-products .products.category-products-carousel > article.product-miniature { + flex: 0 0 calc(50% - 8px) !important; + max-width: calc(50% - 8px) !important; + } + + .category-products .category-products-carousel__nav { + display: none; + } +} + +@media (max-width: 479.98px) { + .category-products .products.category-products-carousel > article.product-miniature { + flex: 0 0 100% !important; + max-width: 100% !important; + } +} diff --git a/modules/ps_categoryproducts/views/css/index.php b/modules/ps_categoryproducts/views/css/index.php new file mode 100644 index 00000000..92f23b95 --- /dev/null +++ b/modules/ps_categoryproducts/views/css/index.php @@ -0,0 +1,33 @@ + + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); +header('Location: ../'); +exit; diff --git a/modules/ps_categoryproducts/views/js/carousel.js b/modules/ps_categoryproducts/views/js/carousel.js new file mode 100644 index 00000000..102ffc2b --- /dev/null +++ b/modules/ps_categoryproducts/views/js/carousel.js @@ -0,0 +1,87 @@ +(function () { + 'use strict'; + + function getStep(track) { + var firstItem = track.querySelector('article.product-miniature'); + if (!firstItem) { + return Math.max(240, Math.floor(track.clientWidth * 0.8)); + } + + var style = window.getComputedStyle(track); + var gap = parseFloat(style.columnGap || style.gap || '0') || 0; + + return Math.ceil(firstItem.getBoundingClientRect().width + gap); + } + + function updateNavState(track, prevBtn, nextBtn) { + var maxScrollLeft = Math.max(0, track.scrollWidth - track.clientWidth); + var canScroll = maxScrollLeft > 8; + + if (!canScroll) { + prevBtn.disabled = true; + nextBtn.disabled = true; + return; + } + + prevBtn.disabled = track.scrollLeft <= 2; + nextBtn.disabled = track.scrollLeft >= maxScrollLeft - 2; + } + + function initCarousel(root) { + var track = root.querySelector('.js-category-products-track'); + var prevBtn = root.querySelector('.js-category-products-prev'); + var nextBtn = root.querySelector('.js-category-products-next'); + + if (!track || !prevBtn || !nextBtn) { + return; + } + + prevBtn.addEventListener('click', function () { + track.scrollBy({ + left: -getStep(track), + behavior: 'smooth' + }); + }); + + nextBtn.addEventListener('click', function () { + track.scrollBy({ + left: getStep(track), + behavior: 'smooth' + }); + }); + + var ticking = false; + track.addEventListener('scroll', function () { + if (ticking) { + return; + } + + ticking = true; + window.requestAnimationFrame(function () { + updateNavState(track, prevBtn, nextBtn); + ticking = false; + }); + }); + + window.addEventListener('resize', function () { + updateNavState(track, prevBtn, nextBtn); + }); + + updateNavState(track, prevBtn, nextBtn); + } + + function onReady() { + var carousels = document.querySelectorAll('.js-category-products-carousel'); + if (!carousels.length) { + return; + } + + carousels.forEach(initCarousel); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', onReady); + } else { + onReady(); + } +})(); diff --git a/modules/ps_categoryproducts/views/js/index.php b/modules/ps_categoryproducts/views/js/index.php new file mode 100644 index 00000000..92f23b95 --- /dev/null +++ b/modules/ps_categoryproducts/views/js/index.php @@ -0,0 +1,33 @@ + + * @copyright 2007-2016 PrestaShop SA + * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); +header('Location: ../'); +exit; diff --git a/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl b/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl index 818712b4..47df38a7 100644 --- a/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl +++ b/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl @@ -1,4 +1,4 @@ -{* +{* * 2007-2016 PrestaShop * * NOTICE OF LICENSE @@ -22,17 +22,58 @@ * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) * International Registered Trademark & Property of PrestaShop SA *} -
+{literal}{/literal} + +{literal}{/literal} diff --git a/themes/InterBlue/assets/img/cms/serie-simon10-tile-1.svg b/themes/InterBlue/assets/img/cms/serie-simon10-tile-1.svg new file mode 100644 index 00000000..5887ecf6 --- /dev/null +++ b/themes/InterBlue/assets/img/cms/serie-simon10-tile-1.svg @@ -0,0 +1,34 @@ + + SIMON 10 + Kafelek serii SIMON 10 z linkiem do oferty. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SIMON 10 + SKONTAKTUJ SIMON + diff --git a/themes/InterBlue/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl b/themes/InterBlue/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl index fcbe7dd6..3319e2de 100644 --- a/themes/InterBlue/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl +++ b/themes/InterBlue/modules/ps_categoryproducts/views/templates/hook/ps_categoryproducts.tpl @@ -22,22 +22,54 @@ * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) * International Registered Trademark & Property of PrestaShop SA *} -
+{literal}{/literal} + +{literal}{/literal} diff --git a/themes/InterBlue/templates/cms/page.tpl b/themes/InterBlue/templates/cms/page.tpl index bff349db..00090143 100644 --- a/themes/InterBlue/templates/cms/page.tpl +++ b/themes/InterBlue/templates/cms/page.tpl @@ -46,6 +46,39 @@ {/literal} {else} {block name='cms_content'} + {if $cms.id == 9} + + +
+ + SIMON 10 - skontaktuj SIMON + +
+ {/if} {$cms.content nofilter} {/block} {/if}