Skip to content

Instantly share code, notes, and snippets.

@iiic
Last active April 30, 2026 14:58
Show Gist options
  • Select an option

  • Save iiic/258708a2c40365194b5ac4903a2d1475 to your computer and use it in GitHub Desktop.

Select an option

Save iiic/258708a2c40365194b5ac4903a2d1475 to your computer and use it in GitHub Desktop.
nový cart-discount.js
/**
* revize 3 - minimum změn oproti produkci
*
* oproti produkci tam nejsou ty problémy s nutným refreshováním stránky či nepřidáním věcí do košíku při zadání kódu
* taktéž oproti produkci nevypisuje chvilkově tu chybu po zadání kódu (než se košík přenačte)
* není nutné podmínkově rozlišovat prohlížeče, chová se ve všech stejně
*
* ale oproti revizi 2 zase má stejný problém jako produkce, že to bere PATCHcokoliv, PATCHPOROTA, PATCHE CELÁ ZEMĚ a tak
*
*/
import { Component } from '@theme/component';
import { morphSection } from '@theme/section-renderer';
import { DiscountUpdateEvent } from '@theme/events';
import { fetchConfig } from '@theme/utilities';
import { cartPerformance } from '@theme/performance';
/**
* A custom element that applies a discount to the cart.
*
* @typedef {Object} CartDiscountComponentRefs
* @property {HTMLElement} cartDiscountError - The error element.
* @property {HTMLElement} cartDiscountErrorDiscountCode - The discount code error element.
* @property {HTMLElement} cartDiscountErrorShipping - The shipping error element.
*/
/**
* @extends {Component<CartDiscountComponentRefs>}
*/
class CartDiscount extends Component {
requiredRefs = ['cartDiscountError', 'cartDiscountErrorDiscountCode', 'cartDiscountErrorShipping'];
/** @type {AbortController | null} */
#activeFetch = null;
#createAbortController() {
if (this.#activeFetch) {
this.#activeFetch.abort();
}
const abortController = new AbortController();
this.#activeFetch = abortController;
return abortController;
}
/**
* Handles updates to the cart note.
* @param {SubmitEvent} event - The submit event on our form.
*/
applyDiscount = async (event) => {
const { cartDiscountError, cartDiscountErrorDiscountCode, cartDiscountErrorShipping } = this.refs;
event.preventDefault();
event.stopPropagation();
const form = event.target;
if (!(form instanceof HTMLFormElement)) return;
const discountCode = form.querySelector('input[name="discount"]');
if (!(discountCode instanceof HTMLInputElement) || typeof this.dataset.sectionId !== 'string') return;
const discountCodeValue = discountCode.value;
const abortController = this.#createAbortController();
try {
const existingDiscounts = this.#existingDiscounts();
if (existingDiscounts.includes(discountCodeValue)) return;
cartDiscountError.classList.add('hidden');
cartDiscountErrorDiscountCode.classList.add('hidden');
cartDiscountErrorShipping.classList.add('hidden');
const config = fetchConfig('json', {
body: JSON.stringify({
discount: [...existingDiscounts, discountCodeValue].join(','),
sections: [this.dataset.sectionId],
}),
});
const response = await fetch(Theme.routes.cart_update_url, {
...config,
signal: abortController.signal,
});
/** @type {Object} */
const data = await response.json();
if ( discountCodeValue.startsWith( "PATCH" ) ) {
console.log( "Detekoval jsem PATCH" );
const variantId = '57580469584197'; // ID dárku
// 2. Zkontrolovat, jestli produkt už v košíku je
const existingItem = data.items.find( item => item.variant_id == variantId );
const conflictItem = data.items.find( item => ( item.variant_id == '56990918050117' || item.variant_id == '57698293973317' ) ); //kontrola na přítomnost dvoubalení
const solutionItem = data.items.find( item => item.variant_id == '57411253272901' ); //kontrola na přítomnost kompenzačního předmětu
/** @type {{ items: Array }} */
const addIntoCartList = { items: [] };
if ( conflictItem && !solutionItem ) { //řeší problém s dvoubalením a dárkem, pokud je dvoubalení a není možné použít kód, tak to přidá prázdný předmět do košíku
addIntoCartList.items.push( { // prázdný item přidán!
id: 57411253272901,
quantity: 1
} );
console.log( 'prázdný item přidán!' );
}
if ( existingItem ) {
console.log( 'Produkt už je v košíku, množství:', existingItem.quantity );
// Nedělejte nic, nebo aktualizujte množství
} else {
/** @type {{ code: string; applicable: boolean; }} */
const currentDiscount = data.discount_codes.find( item => item.code == discountCodeValue );
currentDiscount.applicable = true; // nastavím uživatelem zadaný kód jako platný (tedy nevypisuje se chybová hláška o špatném kódu)
existingDiscounts.push( discountCodeValue ); // přidám kód do seznamu už použitých slevových kódů (prevence opakovaného vložení toho samého kódu)
addIntoCartList.items.push( { // Přidat produkt
id: variantId,
quantity: 1
} );
console.log( 'Dárek přidán!' );
}
if ( addIntoCartList.items.length ) { // je něco na přidání do košíku?
await fetch( '/cart/add.js', { // hromadné vložení předmětu (předmětů) do košíku, s čekáním na dokončení (!) // @todo : může vracet chybovou hlášku typu "out of stock" pokud dojdou přidávané předměty a tak, odchytit vypsat
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( addIntoCartList )
} );
}
}
if (
data.discount_codes.find((/** @type {{ code: string; applicable: boolean; }} */ discount) => {
return discount.code === discountCodeValue && discount.applicable === false;
})
) {
discountCode.value = '';
this.#handleDiscountError('discount_code');
return;
}
const newHtml = data.sections[this.dataset.sectionId];
const parsedHtml = new DOMParser().parseFromString(newHtml, 'text/html');
const section = parsedHtml.getElementById(`shopify-section-${this.dataset.sectionId}`);
const discountCodes = section?.querySelectorAll('.cart-discount__pill') || [];
if (section) {
const codes = Array.from(discountCodes)
.map((element) => (element instanceof HTMLLIElement ? element.dataset.discountCode : null))
.filter(Boolean);
// Before morphing, we need to check if the shipping discount is applicable in the UI
// we check the liquid logic compared to the cart payload to assess whether we leveraged
// a valid shipping discount code.
if (
codes.length === existingDiscounts.length &&
codes.every((/** @type {string} */ code) => existingDiscounts.includes(code)) &&
data.discount_codes.find((/** @type {{ code: string; applicable: boolean; }} */ discount) => {
return discount.code === discountCodeValue && discount.applicable === true;
})
) {
this.#handleDiscountError('shipping');
discountCode.value = '';
return;
}
}
document.dispatchEvent(new DiscountUpdateEvent(data, this.id));
morphSection(this.dataset.sectionId, newHtml);
} catch (error) {
} finally {
this.#activeFetch = null;
cartPerformance.measureFromEvent('discount-update:user-action', event);
}
};
/**
* Handles removing a discount from the cart.
* @param {MouseEvent | KeyboardEvent} event - The mouse or keyboard event in our pill.
*/
removeDiscount = async (event) => {
event.preventDefault();
event.stopPropagation();
if (
(event instanceof KeyboardEvent && event.key !== 'Enter') ||
!(event instanceof MouseEvent) ||
!(event.target instanceof HTMLElement) ||
typeof this.dataset.sectionId !== 'string'
) {
return;
}
const pill = event.target.closest('.cart-discount__pill');
if (!(pill instanceof HTMLLIElement)) return;
const discountCode = pill.dataset.discountCode;
if (!discountCode) return;
const existingDiscounts = this.#existingDiscounts();
const index = existingDiscounts.indexOf(discountCode);
if (index === -1) return;
existingDiscounts.splice(index, 1);
const abortController = this.#createAbortController();
try {
const config = fetchConfig('json', {
body: JSON.stringify({ discount: existingDiscounts.join(','), sections: [this.dataset.sectionId] }),
});
const response = await fetch(Theme.routes.cart_update_url, {
...config,
signal: abortController.signal,
});
const data = await response.json();
document.dispatchEvent(new DiscountUpdateEvent(data, this.id));
morphSection(this.dataset.sectionId, data.sections[this.dataset.sectionId]);
} catch (error) {
} finally {
this.#activeFetch = null;
}
};
/**
* Handles the discount error.
*
* @param {'discount_code' | 'shipping'} type - The type of discount error.
*/
#handleDiscountError(type) {
const { cartDiscountError, cartDiscountErrorDiscountCode, cartDiscountErrorShipping } = this.refs;
const target = type === 'discount_code' ? cartDiscountErrorDiscountCode : cartDiscountErrorShipping;
cartDiscountError.classList.remove('hidden');
target.classList.remove('hidden');
}
/**
* Returns an array of existing discount codes.
* @returns {string[]}
*/
#existingDiscounts() {
/** @type {string[]} */
const discountCodes = [];
const discountPills = this.querySelectorAll('.cart-discount__pill');
for (const pill of discountPills) {
if (pill instanceof HTMLLIElement && typeof pill.dataset.discountCode === 'string') {
discountCodes.push(pill.dataset.discountCode);
}
}
return discountCodes;
}
}
if (!customElements.get('cart-discount-component')) {
customElements.define('cart-discount-component', CartDiscount);
}
@iiic
Copy link
Copy Markdown
Author

iiic commented Apr 27, 2026

  • asi by stačilo říct že fetch() je asynchronní, nečeká se až se provede kód fetch() a pak se jde v kódu dále, začne se dělat fetch() a kód jede dále, bez ohledu na to kdy se fetch dokončí nabo jestli vůbec se dokončí. Ale řešení téhle věci není úplně triviální dá se řešit nekonečným zanořením kódu do then(), ale z toho je těžké se dostat a synchronizovat zbytek kódu, nebo pomocí klíčového slova await, ale to zase nejde použí v zanořených strukturách. Jediné čím se dá obalit je snad podmínka, ale jinak nic, ani v cyklu to nesmí být.
  • video jak se dá otestovat změna částí webu (třeba právě tohoto javascriptu) přímo na živém běžícím webu a jak se dá testovat různé rychlosti internetu: https://www.youtube.com/watch?v=0tIqqjje0S0

nový script:

  • slevový kód není možné zadat opakovaně… tedy nepřidá se další nášivka
  • žádné refreshe stránky nebo zavírání košíku, absolutně nikdy
  • je možné použít libovolné kódy, není nutný PATCH prefix a přesto není možné je vyčíst ze zdrojáku
  • ty slevové kódy jsou zde https://gist.github.com/iiic/258708a2c40365194b5ac4903a2d1475#file-cart-discount-js-L39 udává se hash, sha256(libovolnýText) kde libovolnýText je slevový kód, udělá to libovolný online nástroj či AIčko, sha256 nějakého textu vychází vždy stejně a není možné vrátit otisk zpět na text (pokud by to bylo možné, tak Bitcoin jako celek je v prdeli), dají se najít hashe kratšího textu čistě vložením do googlu (třeba) b6f8d434a847fb0f0c1a8d9b936b8ca952e224f205a55f4ba9b2c20f88fdc9e7
  • jsou tam sloučené přidání do košíku i více položek (patch a kompenzační předmět za korunu) se přidává v jednom POST požadavku, snížení datových přenostů, měne problémů s čekáním na dokončení fetch()
  • žádné umělé čekání, vždy jen a pouze na tu dobu než se provede fetch() dle té rychlosti internetu co kdo má

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment