1/* eslint-disable import/no-cycle, camelcase, max-classes-per-file, 2react/no-unused-state, react/destructuring-assignment */ 3import { 4 h, Component, Fragment, render, 5} from '../preact.js'; 6import htm from '../htm.js'; 7 8import { store } from './api.js'; 9import { loadCSS } from '../aem.js'; 10import { 11 getCart, 12 removeItemFromCart, 13} from './cart.js'; 14 15const html = htm.bind(h); 16let cartVisible = false; 17 18function ConfirmDeletionOverlay(props) { 19 const { close, confirm } = props; 20 21 return html`<div class="overlay-background"> 22 <div class="overlay"> 23 <button class="close" onclick=${close}>Close</button> 24 <div class="content">Are you sure you would like to remove this item from the shopping cart?</div> 25 <div class="actions"> 26 <button onclick=${close}>Cancel</button> 27 <button onclick=${confirm}>OK</button> 28 </div> 29 </div> 30 </div>`; 31} 32 33class ProductCard extends Component { 34 constructor(props) { 35 super(); 36 this.state = { 37 quantity: props.item.quantity, 38 quantityValid: true, 39 confirmDelete: false, 40 }; 41 } 42 43 componentDidUpdate(prevProps) { 44 // Check if quantity props have changed 45 if (prevProps.item.quantity !== this.props.item.quantity) { 46 this.setState({ quantity: this.props.item.quantity, quantityValid: true }); 47 } 48 } 49 50 static renderImage = (item) => { 51 let image; 52 if (item.configured_variant?.thumbnail?.url) { 53 image = item.configured_variant.thumbnail.url; 54 } else if (item.product?.thumbnail?.url) { 55 image = item.product.thumbnail.url; 56 } 57 58 const url = new URL(image); 59 url.search = ''; 60 61 return html`<picture> 62 <source type="image/webp" srcset="${url}?fit=bounds&height=131&width=105&bg-color=255,255,255&format=webply&optimize=medium" /> 63 <img class="product-image-photo" src="${url}?fit=bounds&height=131&width=105&quality=100&bg-color=255,255,255" max-width="105" max-height="131" alt=${item.product.name} /> 64 </picture>`; 65 }; 66 67 render(props, state) { 68 const { item, index, formatter } = props; 69 const { 70 product, prices, configurable_options, 71 } = props.item; 72 73 return html`<li> 74 <div class="minicart-product"> 75 <div class="image"> 76 <a href=${`/products/${product.url_key}/${product.sku.toLowerCase()}`}>${ProductCard.renderImage(item)}</a> 77 </div> 78 <div class="info"> 79 <div class="name"><a href=${`/products/${product.url_key}/${product.sku.toLowerCase()}`} dangerouslySetInnerHTML=${{ __html: product.name }} /></div> 80 ${configurable_options && (html`<div class="options"> 81 <input type="checkbox" id="see-options-${index}" /> 82 <label for="see-options-${index}">See Details</label> 83 <dl> 84 ${configurable_options.map(({ option_label, value_label }) => html`<${Fragment}> 85 <dt>${option_label}:</dt> 86 <dd>${value_label}</dd> 87 <//>`)} 88 </dl> 89 </div>`)} 90 <div class="price">${formatter.format(prices.price.value)}</div> 91 <div class="quantity"> 92 QTY: ${state.quantity} 93 </div> 94 </div> 95 <div class="actions"> 96 <button>Edit</button> 97 <button onclick=${() => this.setState({ confirmDelete: true })}>Remove</button> 98 </div> 99 ${state.confirmDelete && html`<${ConfirmDeletionOverlay} close=${() => this.setState({ confirmDelete: false })} confirm=${() => this.props.api.removeItemFromCart(item.uid, 'Cart Quick View')} />`} 100 </div> 101 </li>`; 102 } 103} 104 105export class Minicart extends Component { 106 constructor() { 107 super(); 108 this.state = { 109 loading: true, 110 cart: {}, 111 }; 112 this.formatter = new Intl.NumberFormat('en-US', { 113 style: 'currency', 114 currency: 'USD', 115 }); 116 } 117 118 componentDidMount() { 119 // Subscribe to store changes 120 this.props.api.store.subscribe((cart) => { 121 this.setState({ cart, loading: false }); 122 }); 123 } 124 125 render(props, state) { 126 if (!props.visible) { 127 return null; 128 } 129 130 const { close } = props.api; 131 132 const { cart } = state; 133 if (!cart.items || cart.items.length === 0) { 134 return html`<div class="minicart-panel empty"> 135 <div class="minicart-header"> 136 <button class="close" onClick=${() => close(false)}>Close</button> 137 </div>
138 <div className="cart-empty">You have no shopping items in your cart.</div> 139 </div>`; 140 } 141 142 return html`<div class="minicart-panel"> 143 <div class="minicart-header"> 144 <div class="title">Added to your bag</div> 145 <div class="subtotal">Sub-Total: <span class="price">${this.formatter.format(cart.totals.subtotal_excluding_tax.value)}</span></div> 146 <button class="close" onClick=${() => close(false)}>Close</button> 147 </div> 148 <ul class="minicart-list"> 149 ${state.cart.items.map((item, index) => html`<${ProductCard} index=${index} item=${item} formatter=${this.formatter} api=${props.api} />`)} 150 </ul> 151 <div class="minicart-actions"> 152 <button onClick=${() => { window.location.href = '/cart'; }}>Go to cart</button> 153 </div> 154 </div>`; 155 } 156} 157 158export async function toggle(refetch = true) { 159 if (!cartVisible) { 160 // Load CSS 161 await loadCSS('/styles/minicart.css'); 162 163 if (refetch && store.getCartId()) { 164 await getCart(); 165 } 166 } 167 168 cartVisible = !cartVisible; 169 170 const app = html`<${Minicart} visible=${cartVisible} api=${{ 171 store, 172 removeItemFromCart, 173 close: toggle, 174 }} />`; 175 render(app, document.querySelector('.minicart-wrapper')); 176} 177 178export async function showCart() { 179 if (!cartVisible) { 180 await toggle(false); 181 } 182}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.