feat(new-layout): add-to-cart handler + piece configurator (Phase 02 plans 01-02)

Plan 02-01 (piece/crop configurator, complete):
- #piece reuse z shared partial product-cover-thumbnails.tpl
- 8 hidden inputs (is_crop, crop_pos_x/y, crop_width/height, piece_bg_top/left, is_reflection) w formie #add-to-cart-or-refresh
- Defensive setup w custom.js: setTimeout(600) init, no-op override totalpriceinfospecific/prod, DOM stubs
- CSS scope pod body#product .product-size-data .product-size-data--new

Plan 02-02 (add-to-cart submission, PARTIAL):
- Capture-phase native addEventListener (useCapture=true) blokuje PS core crash
  (button poza formą w nowym layoucie — closest('form') zwracało 0)
- Manualny AJAX POST: form.serialize() + qty + add=1&action=update do /pl/koszyk
- Fancybox-blocker port z custom.js:327 (nie odpalał się bo selector 0 matches)
- Manual sync is_crop/crop_width/height przed POST (obejście crash checkedHandler)
- prestashop.emit('updatedCart') + defensive blockcart refresh fetch
- Loading spinner + success flash CSS
- Inline handler mirror w product.tpl z idempotency guard (window.__p02p02Bound)
  — cache-buster dla browser cachowanego custom.js

Deferred do Plan 02-03 (customization + modal blocker dla production):
- Customization nie zapisuje się (squaremeter hook gate'owany discretion=on + brak dimension fields)
- Success modal (wymaga POST do /module/ps_shoppingcart/ajax)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 23:33:45 +02:00
parent 161c090ef0
commit 7ac795ba3f
24 changed files with 5447 additions and 2569 deletions

View File

@@ -48,7 +48,9 @@
{/if}
{/block}
{if $smarty.server.REMOTE_ADDR != '91.189.216.43'}
{if $smarty.server.REMOTE_ADDR != '89.69.31.86'}
{block name='content'}
<section id="main" itemscope itemtype="https://schema.org/Product">
@@ -520,7 +522,8 @@
<div class="piece-left-positon hidden">10</div>
<div class="piece-top-positon hidden">10</div>
{/block}
{else}
{/if}
{if $smarty.server.REMOTE_ADDR == '89.69.31.86'}
{block name='content'}
<section id="main" itemscope itemtype="https://schema.org/Product">
<meta itemprop="url" content="{$product.url}">
@@ -550,6 +553,7 @@
{block name='product_cover_thumbnails'}
{include file='catalog/_partials/product-cover-thumbnails.tpl'}
{/block}
{* #piece jest wewnatrz product-cover-thumbnails.tpl (rendered inside .product-images li) — NIE duplikowac tutaj *}
</div>
</div>
</div>
@@ -598,9 +602,24 @@
{block name='product_variants'}
{$product_variant_mode = 2}
<div class="product-box product-variants-data">
<div class="product-box product-variants-data product-variants-data--new">
<h4 class="block-title">Wybierz wersję kolorystyczną</h4>
{include file='catalog/_partials/product-variants.tpl'}
<form action="{$urls.pages.cart}" method="post" id="add-to-cart-or-refresh">
<input type="hidden" name="token" value="{$static_token}">
<input type="hidden" name="id_product" value="{$product.id}" id="product_page_product_id">
<input type="hidden" name="id_customization" value="{$product.id_customization}" id="product_customization_id" class="js-product-customization-id">
<input type="hidden" name="is_crop" value="0" id="product_is_crop">
<input type="hidden" name="is_reflection" value="0" id="product_is_reflection">
<input type="hidden" name="crop_pos_x" value="0" id="product_crop_pos_x">
<input type="hidden" name="crop_pos_y" value="0" id="product_crop_pos_y">
<input type="hidden" name="crop_width" value="0" id="product_crop_width">
<input type="hidden" name="crop_height" value="0" id="product_crop_height">
<input type="hidden" name="piece_bg_top" id="piece_bg_top" value="">
<input type="hidden" name="piece_bg_left" id="piece_bg_left" value="">
<div class="product-variants-grid">
{include file='catalog/_partials/product-variants.tpl'}
</div>
</form>
</div>
{/block}
@@ -612,6 +631,26 @@
</a>
</div>
<div class="product-box--data">
<div class="product-size-data--new">
<a rel="nofollow" href="javascript:void(0);" class="fancybox-size-controls piece-summary">
<span id="piece-size-view" class="strong">Wybierz rozmiar</span>
<span class="piece-hint">&mdash; kliknij aby zmienić</span>
</a>
<div id="button-mirror-reflection">
<div class="product-bar-icon rotate-icon">
<img src="/themes/ayon/assets/images/odbicie-iustrzane.png" alt="">
</div>
<div class="product-bar-box">
<p class="button-mirror-reflection-label">Odbicie lustrzane</p>
</div>
</div>
{* Hidden state trzymane w DOM — istniejące handlery w custom.js bindują się po ID. *}
<input type="checkbox" id="checkbox-piece" checked style="display:none;">
<input type="number" min="50" max="500" value="100" id="piece-width" style="display:none;">
<input type="number" min="50" max="300" value="100" id="piece-height" style="display:none;">
<div class="piece-left-positon" style="display:none;">10</div>
<div class="piece-top-positon" style="display:none;">10</div>
</div>
</div>
</div>
{/block}
@@ -715,5 +754,107 @@
</div>
</section>
{/block}
{* =========================================================================
Phase 02 Plan 02-02: inline add-to-cart handler (cache-buster).
Powod: <script src="custom.js"> w tym temacie jest serwowany bez wersji
i browser cache'uje stara wersje przy kolejnych iteracjach. Ten inline
<script> jest czescia HTML response wiec ZAWSZE jest swiezy. Idempotentny
guard `window.__p02p02Bound` zapobiega double-register jesli custom.js
tez jest aktualny (happy path z hard-reload).
Kod IDENTYCZNY z custom.js:994-1113 — zmiany wprowadzaj w OBU miejscach
do czasu dodania systemowego cache-bustera (Plan 02-03+).
========================================================================= *}
<script type="text/javascript">
(function() {
if (window.__p02p02Bound) return;
window.__p02p02Bound = true;
document.addEventListener('click', function(e) {
var btn = e.target.closest ? e.target.closest('[data-button-action=add-to-cart]') : null;
if (!btn) return;
if (!document.querySelector('.product-variants-data--new')) return;
var $form = jQuery('#add-to-cart-or-refresh');
if (!$form.length) return;
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
if (!jQuery('#checkbox-piece').is(':checked')) {
jQuery.fancybox({
minWidth: 800, maxWidth: 1000, padding: 30, height: 100,
content: 'Proszę wybrać rozmiar i wycinek tapety przed dodaniem jej do koszyka.'
});
return;
}
if (jQuery('#product_is_crop').val() === '0' || !jQuery('#product_crop_width').val() || jQuery('#product_crop_width').val() === '0') {
jQuery('#product_is_crop').val('1');
var pw = parseInt(jQuery('#piece-width').val(), 10) || 100;
var ph = parseInt(jQuery('#piece-height').val(), 10) || 100;
jQuery('#product_crop_width').val(pw);
jQuery('#product_crop_height').val(ph);
}
var $btn = jQuery(btn);
$btn.prop('disabled', true).addClass('loading');
var qty = parseInt(jQuery('#quantity_wanted').val(), 10) || 1;
var payload = $form.serialize() + '&qty=' + encodeURIComponent(qty) + '&add=1&action=update';
var actionUrl = $form.attr('action') || window.location.href;
jQuery.ajax({
url: actionUrl,
type: 'POST',
data: payload,
dataType: 'json',
headers: { 'Accept': 'application/json' },
success: function(resp) {
var hasError = !resp || resp.hasError === true || resp.success === false ||
(resp.errors && (Array.isArray(resp.errors) ? resp.errors.length : Object.keys(resp.errors).length));
if (hasError) {
var errs = resp && resp.errors;
var msg = '';
if (Array.isArray(errs)) msg = errs.join('<br>');
else if (errs && typeof errs === 'object') msg = Object.values(errs).join('<br>');
else msg = 'Nie udało się dodać produktu do koszyka. Spróbuj ponownie.';
jQuery.fancybox({
minWidth: 400, maxWidth: 800, padding: 30, height: 100,
content: msg
});
return;
}
if (window.prestashop && typeof prestashop.emit === 'function') {
prestashop.emit('updatedCart', { resp: resp, reason: { linkAction: 'add-to-cart' } });
}
jQuery(document).trigger('updatedCart', [resp]);
if (window.prestashop && prestashop.urls && prestashop.urls.pages && prestashop.urls.pages.cart) {
jQuery.get(prestashop.urls.pages.cart, { action: 'refresh', ajax: 1 }, null, 'json')
.done(function(cartResp) {
if (cartResp && cartResp.preview) {
jQuery('.blockcart').replaceWith(cartResp.preview);
}
});
}
$btn.addClass('added-flash');
setTimeout(function() { $btn.removeClass('added-flash'); }, 1200);
},
error: function() {
jQuery.fancybox({
minWidth: 400, maxWidth: 800, padding: 30, height: 100,
content: 'Błąd połączenia z serwerem. Spróbuj ponownie za chwilę.'
});
},
complete: function() {
$btn.prop('disabled', false).removeClass('loading');
}
});
}, true);
})();
</script>
{/if}