PageSourceSearch

https://www.gamersgate.com/static/scripts/app.js?1782748914

js gamersgate.com collected 2026-09-24 20:12:10 UTC 150,051 bytes, 4,421 lines download raw bytes

1function csrfSafeMethod(method) {
2  // these HTTP methods do not require CSRF protection
3  return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method))
4}
5
6function sleep(ms) {
7  return new Promise(resolve => setTimeout(resolve, ms));
8}
9
10
11function getCookie(name) {
12  let cookieValue = null
13  if (document.cookie && document.cookie != '') {
14    let cookies = document.cookie.split(';')
15    for (let i = 0; i < cookies.length; i++) {
16      let cookie = cookies[i].trim()
17      if (cookie.substring(0, name.length + 1) == (name + '=')) {
18        cookieValue = decodeURIComponent(cookie.substring(name.length + 1))
19        break
20      }
21    }
22  }
23  return cookieValue
24}
25
26function setCookie(key, value, options) { // options = {expires: '', path: '', domain: '', secure: ''}
27  if (typeof value != 'undefined') {
28    options = options || {}
29    if (value === null) {
30      value = ''
31      options.expires = -1
32    }
33    let expires = ''
34    if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
35      let date
36      if (typeof options.expires == 'number') {
37        date = new Date()
38        date.setTime(date.getTime() + (options.expires * 1000))
39      } else {
40        date = options.expires
41      }
42      expires = '; expires=' + date.toUTCString()
43    }
44    const path = options.path ? '; path=' + (options.path) : ''
45    const domain = options.domain ? '; domain=' + (options.domain) : ''
46    const secure = options.secure ? '; secure' : ''
47    const same_site = options.same_site ? '; samesite=' + (options.same_site) : ''
48    document.cookie = [key, '=', value, expires, path, domain, secure, same_site].join('')
49  }
50}
51
52function escapeHtml(text) {
53  const map = {
54    '&': '&amp;',
55    '<': '&lt;',
56    '>': '&gt;',
57    '"': '&quot;',
58    "'": '&#039;'
59  };
60
61  return text.replace(/[&<>"']/g, function(m) { return map[m]; });
62}
63
64function onlyUnique(value, index, self) {
65  return self.indexOf(value) === index
66}
67
68function recentlyViewed(id) {
69  let data = getCookie('gg_rv')
70  data = data ? data.split(' ') : []
71  data.push(id.toString())
72  data = data.filter(onlyUnique).slice(-5)
73  setCookie('gg_rv', data.join(' '), {'expires': 365 * 24 * 3600, 'path': '/'})
74}
75
76function checkStatus(response) {
77  if (response.status >= 200 && response.status < 300) {
78    return response
79  } else {
80    let error = new Error(response.statusText)
81    error.response = response
82    throw error
83  }
84}
85
86function parseJSON(response) {
87  return response.json()
88}
89
90function get_url_with_locale(url, locale_code) {
91  const host = window.location.hostname;
92  const prefix_language = window.location.hostname === 'www.gamersgate.es' ? 'es' : 'en';
93  const prefix = locale_code == prefix_language ? '' : '/' + locale_code
94  return prefix + url
95}
96
97function timestamp_to_datetime_format(timestamp, locale) {
98  const date = new Date(timestamp*1000)
99  return date.toLocaleDateString(locale) + ' ' +  gettext('at') + ' ' + date.toLocaleTimeString(locale, {'hour12': false, 'hour': '2-digit', 'minute': '2-digit'})
100}
101
102function validatorEmail(email) {
103  let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
104  return email !== '' && re.test(String(email).toLowerCase())
105}
106
107function validatorRepeat(value, repeat) {
108  return repeat !== '' && !(repeat.length > 0 && repeat !== value)
109}
110
111function validatorEmpty(value) {
112  return value !== ''
113}
114
115function validatorPasswordLength(value) {
116  return value !== '' && value.length >= 8
117}
118
119function validatorPasswordChar(value) {
120  let re = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,34}/
121  return value !== '' && re.test(String(value))
122}
123
124String.prototype.format = function() {
125  a = this;
126  for (k in arguments) {
127    a = a.replace("{" + k + "}", arguments[k])
128  }
129  return a
130}
131
132class Cart {
133  constructor(cart_items, language) {
134    this.url = '/api/cart/'
135    this.cart_items = cart_items
136    this.language = language
137    this.max = 3
138    this.min = 1
139    this.wait = false
140    this.AddItemHandlerInstance = this.addItemHandler.bind(this)
141    this.render()
142    this.listen()
143    this.updateCartItemsInfo(null)
144    return this
145  }
146  
147  render() {
148    this.updateCartItemsInfo(this.cart_items.size)
149    this.cart_items.forEach(function(id) {
150      const elements = document.querySelectorAll('.cart--add-button[data-id="' + id + '"]')
151      for (let index = 0; index < elements.length; index++) {
152        elements[index].addEventListener('click', function() {top.location.href='/cart/'})
153        elements[index].innerHTML = gettext('In cart')
154        elements[index].classList.add('item_in_cart')
155      }
156    })
157  }
158  
159  listen() {
160    this.listenCatalog()
161
162    document.querySelectorAll('.cart--remove-button').forEach(function(elem) {
163      elem.addEventListener('click', function(event) {
164        event.preventDefault()
165        this.remove(event.target.dataset.id)
166      }.bind(this))
167    }.bind(this))
168
169    document.querySelectorAll('.cart--update-button a').forEach(function(elem) {
170      elem.addEventListener('click', function(event) {
171        this.update(elem.dataset.id, elem.dataset.value)
172      }.bind(this))
173    }.bind(this))
174
175    document.addEventListener('catalog-refresh', function() {
176      this.render()
177      this.listenCatalog()
178    }.bind(this))
179
180    document.addEventListener('moved-to-favorites', function(event) {
181      this.removeDone(event.detail.data)
182    }.bind(this))
183  }
184
185  listenCatalog() {
186    document.querySelectorAll('.cart--add-button').forEach(function(elem) {
187      if (!elem.classList.contains('item_in_cart')) {
188        elem.addEventListener('click', this.AddItemHandlerInstance)
189      }
190    }.bind(this))
191    document.addEventListener('addToCart', (e) => {
192      const detailData = e.detail;
193      this.add(detailData.product.id);
194    });
195  }
196  
197  addItemHandler(event) {
198    if (!event.target.classList.contains('item_in_cart')) {
199      this.add(event.target.dataset.id)
200    }
201  }
202  
203  add(product_id) {
204    if (!this.wait) {
205      this.request(product_id, 'add')
206    }
207  }
208  
209  remove(product_id) {
210    if (!this.wait) {
211      this.request(product_id, 'remove')
212    }
213  }
214
215  update(product_id, value) {
216    if (!this.wait && value >= this.min && value <= this.max) {
217      this.request(product_id, 'update', value)
218    }
219  }
220
221  request(product_id, action, count=1) {
222    this.wait = true
223    const formData  = new FormData()
224    formData.append('id', product_id)
225    formData.append('action', action)
226    formData.append('count', count)
227    fetch(get_url_with_locale(this.url, this.language), {
228        method: 'POST',
229        headers: {
230          'X-CSRFToken': getCookie('csrftoken')
231        },
232        body: formData
233      })
234      .then(checkStatus)
235      .then(parseJSON)
236      .then(function(data) {
237        this.wait = false
238        return data
239      }.bind(this))
240      .then(this[action+'Done'].bind(this))
241      .catch(function() {
242        this.wait = false
243        new PopUp().showErrorPopUp(gettext('An error occurred, try again later'))
244      }.bind(this))
245  }
246  
247  addDone(data) {
248    if (data.need_check_age) {
249      if (data.need_age_request) {
250        new PopUp().showAgeRequest({
251          id: data.product.id,
252          name: data.product.name,
253          image: data.product.image,
254        });
255      } else {
256        new PopUp().showAgeError({
257          id: data.product.id,
258          name: data.product.name,
259          image: data.product.image,          
260        });
261      }
262    }
263    if (data.blocked) {
264      new PopUp().showBlockedCartPopUp(data)
265    }
266    if (data.success) {
267      document.dispatchEvent(new CustomEvent('cart_updated', {detail: {data: data}}))
268      this.cart_items.add(data.product.id)
269      this.render()
270      new PopUp().showAddToCartPopUp(data)
271    }
272    if (data.error) {
273      new PopUp().showErrorPopUp(data.error)
274    }
275  }
276  
277  removeDone(data) {
278    if (data.success && data.products.length == 0) {
279      document.dispatchEvent(new CustomEvent('cart_updated', {detail: {data: data}}))
280      location.reload()
281      return
282    }
283    else if (data.success) {
284      document.dispatchEvent(new CustomEvent('cart_updated', {detail: {data: data}}))
285      this.updatePaymentMethods(data)
286      this.updateCartItemsInfo(data.products.length)
287      this.updateAmountInfo(data)
288      let cartItemNode = document.querySelector('.cart-item[data-id="' + data.id + '"]')
289      if (cartItemNode) {
290        cartItemNode.innerHTML = ''
291        cartItemNode.remove()
292      }
293    }
294
295    if (data.error) {
296      new PopUp().showErrorPopUp(data.error)
297    } else {
298      this.updateDone(data)
299    }
300  }
301  
302  updateDone(data) {
303    if (data.success && data.products.length == 0) {
304      document.dispatchEvent(new CustomEvent('cart_updated', {detail: {data: data}}))
305      location.reload()
306    }
307    else if (data.success) {
308      document.dispatchEvent(new CustomEvent('cart_updated', {detail: {data: data}}))
309      this.updateAmountInfo(data)
310      this.updatePaymentMethods(data)
311      data.products.forEach(function(product) {
312        const element = document.querySelector('.cart-item[data-id="' + product.id + '"]')
313        element.querySelector('.cart-item--price').innerHTML = (product.full_total == product.total ? '' : ('<span>' + product.full_total.toString() + '</span>')) + data.currency_symbol + product.total.toString()
314        const errorsContainer = element.querySelector('.cart-item--errors')
315        if (errorsContainer) {
316          errorsContainer.innerHTML = ''
317          errorsContainer.remove()
318        }
319        if (product.errors) {
320          element.querySelector('.cart-item--data').insertAdjacentHTML('beforeend', '<div class="cart-item--errors"><div class="form-error">' + product.errors.join('<br>') + '</div></div>')
321        }
322      })
323    }
324
325    this.updateCartCouponErrors(data.coupon_errors)
326
327    if (data.error) {
328      new PopUp().showErrorPopUp(data.error)
329    }    
330  }
331
332  updateCartCouponErrors(errors) {
333    const form = document.querySelector('#cart-coupon-form')
334    if (form) {
335      const errorCouponContainers = form.querySelectorAll('.form-error')
336      errorCouponContainers.forEach(function (elem) {
337        elem.innerHTML = ''
338        elem.remove()
339      })
340      if (errors) {
341        errors.forEach(function (error) {
342          form.insertAdjacentHTML('beforeend', '<div class="form-error">' + error + '</div>')
343        })
344      }
345    }
346  }
347
348  updateCartItemsInfo(counter) {
349    const elem = document.querySelector('#cart_link')
350
351    if (elem) {
352      if (counter === null) {
353        counter = elem.dataset.count
354      }
355
356      elem.style.setProperty('--cart-count', counter > 0 ? "'" + counter + "'" : "none")
357    }
358  }
359
360  updatePaymentMethods(data) {
361    const elements = document.querySelectorAll('.cart-payment-item[data-is_account="true"]')
362    elements.forEach(function(elem) {
363      if (data.account_payment_allowed === true) {
364        elem.style.display = ''
365      } else if (data.account_payment_allowed === false) {
366        elem.style.display = 'none'
367      }
368    })
369  }
370  
371  updateAmountInfo(data) {
372    const elemAmount = document.querySelector('.cart-total-value span')
373    if (elemAmount !== null) {
374      elemAmount.innerHTML = data.currency_symbol + data.amount.toString()
375    }
376    const elemDiscount = document.querySelector('.cart-total-description #discount_total')
377    if (elemDiscount !== null) {
378      elemDiscount.innerHTML = data.currency_symbol + data.discount.toString()
379    }
380    const elemTax = document.querySelector('.cart-tax-value')
381    if (elemTax !== null) {
382      let htmlTax = ''
383      if (data.taxes) {
384        for (let index = 0; index < data.taxes.length; index++) {
385          htmlTax += data.taxes[index].type + ': ' + data.currency_symbol + data.taxes[index].tax.toString() + '<br>'
386        }
387      }
388      elemTax.innerHTML = htmlTax
389    }
390    const elemCouponNotification = document.querySelector('.cart-total-description #no_coupon_applied')
391    if (elemCouponNotification) {
392      elemCouponNotification.innerHTML = data.coupon === null ? '(' + elemCouponNotification.dataset.text + ')' : ''
393    }
394  }
395}
396
397class Favorite {
398  constructor(is_auth, language, is_favorit_page) {
399    this.url = '/api/favorite/'
400    this.is_auth = is_auth
401    this.language = language
402    this.is_favorit_page = false
403
404    this.handlerInstance = this.handler.bind(this)
405
406    this.listen()
407
408    return this
409  }
410
411  setIsFavoritPage() {
412    this.is_favorit_page = true
413  }
414
415  listen() {
416    this.listenCatalog()
417
418    document.addEventListener('catalog-refresh', function() {
419      this.listenCatalog()
420    }.bind(this))
421  }
422
423  listenCatalog() {
424    document.querySelectorAll('.favorite--button').forEach(function(elem) {
425      elem.addEventListener('click', this.handlerInstance)
426    }.bind(this))
427  }
428
429  handler(event) {
430    event.preventDefault()
431    if (!this.is_auth) {
432      new PopUp().showAddFavoriteAuth()
433    }
434    else if (event.target.classList.contains('is_active')) {
435      this.request(event.target.dataset.id, 'delete')
436    }
437    else if (event.target.dataset.action) {
438      this.request(event.target.dataset.id, event.target.dataset.action)
439    }
440    else {
441      this.request(event.target.dataset.id, 'add')
442    }
443  }
444
445  request(product_id, action) {
446    const formData  = new FormData()
447    formData.append('id', product_id)
448    formData.append('action', action)
449    fetch(get_url_with_locale(this.url, this.language), {
450        method: 'POST',
451        headers: {
452          'X-CSRFToken': getCookie('csrftoken')
453        },
454        body: formData
455      })
456      .then(checkStatus)
457      .then(parseJSON)
458      .then(function(data) {
459        if (data.hasOwnProperty('success') && data.success) {
460          const element = document.querySelector('.favorite--button[data-id="' + product_id + '"]')
461          if (action == 'move-to-favorites') {
462            let elem = document.querySelector('.cart-item[data-id="' + data.id + '"]')
463            let item = {
464              item_id: elem.dataset.sku,
465              item_name: elem.dataset.name,
466              brand: elem.dataset.vendor_name,
467              item_list_id: 'cart',
468              currency: elem.dataset.currency,
469              price: elem.dataset.price,
470              quantity: elem.dataset.quantity
471            }
472            gtag("event", "add_to_wishlist", {
473              send_to: 'G-4WET38ZXRM',
474              item_list_id: 'cart',
475              items: [item]
476            })
477            document.dispatchEvent(new CustomEvent('moved-to-favorites', {detail: {data: data}}))
478          }
479          else if (action == 'add') {
480            element.classList.add('is_animation')
481            sleep(500).then(() => {
482              element.classList.add('is_active')
483              element.classList.remove('is_animation')
484            })
485            try {
486              let catalog = element.closest('.catalog-list')
487              let elem = element.closest('.product--item')
488              let item = {
489                item_id: elem.dataset.sku,
490                item_name: elem.dataset.name,
491                brand: elem.dataset.vendor_name,
492                item_list_id: catalog.dataset.ga_slug,
493                currency: elem.dataset.currency,
494                price: elem.dataset.price,
495                quantity: 1
496              }
497              gtag("event", "add_to_wishlist", {
498                send_to: 'G-4WET38ZXRM',
499                item_list_id: catalog.dataset.ga_slug,
500                items: [item]
501              })
502            } catch {}
503          }
504          else if (action == 'delete') {
505            element.classList.add('is_animation')
506            sleep(500).then(() => {
507              element.classList.remove('is_active')
508              element.classList.remove('is_animation')
509            })
510            if (this.is_favorit_page) {
511              const parentElement = element.parentNode.parentNode.parentNode
512              parentElement.innerHTML = ''
513              parentElement.remove()
514
515              if (document.querySelectorAll('.catalog-item-container') === null || document.querySelectorAll('.catalog-item-container').length == 0) {
516                location.reload()
517              }
518            }
519          }
520        }
521        else if (data.hasOwnProperty('blocked') && data.blocked) {
522          new PopUp().showBlockedSystemAlert(data.popup);
523        }
524        else {
525          throw new Error('')
526        }
527      }.bind(this))
528      .catch(function(e) {
529        new PopUp().showErrorPopUp(gettext('An error occurred, try again later'))
530      }.bind(this))
531  }
532}
533
534class PopUp {
535  constructor() {
536    this.id = 'popup'
537    this.btnCloseClass = 'popup-close' 
538    this.containerClass = ''
539    this.headline = ''
540    this.bodyHtml = ''
541    this.footerHtml = ''
542    this.showPopupCloseButton = true
543    this.runCloseEvent = true
544    this.additionalPopupClass = ''
545    this.isModal = false
546    this.afterClose = function() {}
547    this.removeNode()
548    return this
549  } 
550
551  setAdditionalPopupClass(value) {
552    this.additionalPopupClass = value
553    return this
554  }
555
556  setClass(containerClass) {
557    this.containerClass = containerClass
558    return this
559  }
560  
561  setHeadline(headline) {
562    this.headline = headline
563    return this
564  }
565  
566  setBodyHtml(bodyHtml) {
567    this.bodyHtml = bodyHtml
568    return this
569  }
570  
571  setFooterHtml(footerHtml) {
572    this.footerHtml = footerHtml
573    return this
574  }  
575  
576  show(id=null, attachment=false) {
577    id = id === null ? this.id : id
578    let popupHtml = ''
579    popupHtml += '<div id="' + id + '" class="popup ' + this.additionalPopupClass + '">'
580    popupHtml += '<div class="popup-main ' + this.containerClass + '" id="' + (attachment ? 'top-pop-up-attachment' : 'top-p
580op-up') + '">'
581    if (this.headline != '') {
582      popupHtml += '<div class="popup--header"><h4>' + this.headline + '</h4></div>'
583    }
584    if (this.bodyHtml != '') {
585      popupHtml += '<div class="popup--body">' + this.bodyHtml + '</div>'
586    }
587    if (this.footerHtml != '') {
588      popupHtml += '<div class="popup--footer">' + this.footerHtml + '</div>'
589    }
590    if (this.showPopupCloseButton) {
591      popupHtml += '<div class="popup--close-button popup-close"></div>'
592    }
593    popupHtml += '</div>'
594    popupHtml += '</div>'
595    if (attachment) {
596      document.querySelector('#' + this.id).insertAdjacentHTML('afterend', popupHtml)
597    }
598    else {
599      document.querySelector('main.wrapper').insertAdjacentHTML('beforebegin', popupHtml)
600    }
601    document.querySelector('body').style.overflow = 'hidden'
602    if (this.runCloseEvent) {
603      setTimeout(this.closeEvent.bind(this), 500)
604    }
605  }
606  
607  removeNode(id=null) {
608    id = id === null ? this.id : id
609    let node = document.getElementById(id)
610    if (node && node.parentNode) {
611      node.parentNode.removeChild(node)
612    }
613    document.querySelector('body').style.overflow = 'auto'
614  }
615  
616  close(id=null) {
617    id = id === null ? this.id : id
618    this.removeNode(id)
619    if (this.afterClose) {
620      this.afterClose()
621      this.afterClose = function() {}
622    }
623  }
624  
625  closeEvent() {
626    let element = document.querySelector('#' + this.id + ' > .popup-main')
627    
628    let isVisible = function(elem) {
629      return !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length )
630    }
631    
632    let removeNode = function() {
633      removeClickListener()
634      this.close()
635    }.bind(this)
636    
637    let outsideClickListener = function(event) {
638      if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
639        element.style.display = 'none'
640        removeNode()
641      }
642    }
643    
644    let removeClickListener = function() {
645      document.removeEventListener('click', outsideClickListener)
646    }
647    
648    if (!this.isModal) {
649        document.addEventListener('click', outsideClickListener)
650    }
651
652    this.closeButtonHandler()
653  }
654
655  closeButtonHandler(id=null) {
656    id = id === null ? this.id : id
657
658    let closeButtonElements = document.querySelectorAll('#' + id + ' .' + this.btnCloseClass)
659
660    let removeNode = function() {
661      removeClickListener()
662      this.close(id)
663    }.bind(this)
664
665    let removeClickListener = function() {
666      if (closeButtonElements !== null) {
667        for (let index = 0; index < closeButtonElements.length; index++) {
668          closeButtonElements[index].removeEventListener('click', removeNode)
669        }
670      }
671    }
672
673    if (closeButtonElements !== null) {
674      for (let index = 0; index < closeButtonElements.length; index++) {
675        closeButtonElements[index].addEventListener('click', removeNode)
676      }
677    }
678  }
679
680  showErrorPopUp(headline, body=null, footer=null) {
681    let popup = this.setClass('popup-container-error').setHeadline(headline)
682    if (body !== null) {
683      popup = popup.setBodyHtml(body)
684    }
685    if (footer !== null) {
686      popup = popup.setFooterHtml(footer)
687    }
688    popup.show()
689  }
690  
691  showAddToCartPopUp(data) {
692    let bodyHtml = ''
693    let footerHtml = ''
694
695    bodyHtml += '<div class="popup-container-cart--product-image"><img src="' + data.product.image + '" alt="' + data.product.name + '"></div>'
696    bodyHtml += '<div class="popup-container-cart--product-name">' + data.product.name + '</div>'
697    bodyHtml += '<div class="popup-container-cart--product-price">'
698    if (data.product.base_price > data.product.price) {
699      bodyHtml += '<div class="popup-container-cart--product-price-full">' + data.product.currency_symbol + data.product.base_price + '</div>'
700    }
701    bodyHtml += '<div class="popup-container-cart--product-price-current">' + data.product.currency_symbol + data.product.price + '</div>'
702    bodyHtml += '</div>'
703
704    footerHtml += '<div class="popup-container-cart--footer-title"><font class="color-dodger-blue">' + data.count + '</font> ' + ngettext('product', 'products', data.count) + gettext(' in cart') + '</div>'
705    footerHtml += '<div class="popup-container-cart--footer-checkout"><a href="/cart/" class="button">' + gettext('View cart') + '</a></div>'
706    footerHtml += '<div class="popup-container-cart--footer-close"><button type="button" class="button button-clear popup-close">' + gettext('Continue shopping') + '</button></div>'
707
708    this.setClass('popup-container-cart').setHeadline(gettext('Product added to cart')).setBodyHtml(bodyHtml).setFooterHtml(footerHtml).show()
709  }
710
711  showBlockedCartPopUp(data) {
712    let bodyHtml = '';
713    let footerHtml = '';
714
715    bodyHtml += '<div class="popup-container-cart--product-name">';
716    bodyHtml += 'To comply with UK law, you must be 18 or older to view or purchase products with adult content. To verify your age, please contact our support team.';
717    bodyHtml += '<br><i>This is in accordance with the UK Online Safety Act 2023.</i>';
718    bodyHtml += '</div>';
719
720    footerHtml += '<div class="popup-container-cart--footer-checkout"><a href="/contact/" class="button">' + gettext('Contact Support') + '</a></div>'
721
722    this.setClass('popup-container-cart').setHeadline(gettext('Age Verification Required')).setBodyHtml(bodyHtml).setFooterHtml(footerHtml).show()    
723  }
724
725  showAddFavoriteAuth() {
726    let headline = ''
727    let bodyHtml = ''
728    let footerHtml = ''
729
730    headline += gettext("Heya!")
731    bodyHtml += gettext("Just click to add a game to favo... wait a second, you are not logged in! You'll need to register or log in to do that!")
732
733    footerHtml += '<div class="popup-container--footer-action"><a href="/login?returnurl=' + window.location.pathname.toString() + '" class="button">' + gettext('Sign In') + '</a></div>'
734    footerHtml += '<div class="popup-container--footer-close m-r-1"><button type="button" class="button button-outline popup-close">' + gettext('Cancel') + '</button></div>'
735
736    this.setClass('popup-container').setHeadline(headline).setBodyHtml(bodyHtml).setFooterHtml(footerHtml).show()
737  }
738
739  showSystemAlert(message, signin_required) {
740    let headline = ''
741    let bodyHtml = ''
742    let footerHtml = ''
743
744    headline += gettext("Heya!")
745    bodyHtml += message
746
747    if (signin_required) {
748      footerHtml += '<div class="popup-container--footer-action"><a href="/login?returnurl=' + window.location.pathname.toString() + '" class="button">' + gettext('Sign In') + '</a></div>'
749      footerHtml += '<div class="popup-container--footer-close m-r-1"><button type="button" class="button button-outline popup-close">' + gettext('Cancel') + '</button></div>'
750    }
751    this.setClass('popup-container').setHeadline(headline).setBodyHtml(bodyHtml).setFooterHtml(footerHtml).show()
752  }
753
754  showBlockedSystemAlert(data) {
755    let headline = '';
756    let bodyHtml = '';
757    let footerHtml = '';
758
759    headline += data.title;
760    bodyHtml += data.content;
761
762    footerHtml += '<div class="popup-container--footer-action"><a href="' + data.btn.link + '" class="button">' + data.btn.text + '</a></div>';
763    footerHtml += '<div class="popup-container--footer-close m-r-1"><button type="button" class="button button-outline popup-close">' + gettext('Cancel') + '</button></div>';
764
765    this.setClass('popup-container').setHeadline(headline).setBodyHtml(bodyHtml).setFooterHtml(footerHtml).show()
766  }  
767
768  showAvatars(data) {
769    let bodyHtml = '';
770    let footerHtml = '';
771
772    bodyHtml += '<div class="popup-container-avatars">';
773    data.forEach((item) => {
774      if (item.is_active) {
775        bodyHtml += '<div class="popup-container-avatar is_active" data-hash="' + item.hash + '" data-src="' + item.url + '" data-action="change-avatar">';
776      } else {
777        bodyHtml += '<div class="popup-container-avatar" data-hash="' + item.hash + '" data-src="' + item.url + '" data-action="change-avatar">';
778      }
779      bodyHtml += '<img src="' + item.url + '" alt="">';
780      bodyHtml += '</div>';
781    });
782    bodyHtml += '</div>';
783
784    this.setClass('popup-container-cart').setHeadline(gettext('Choose profile picture that you like')).setBodyHtml(bodyHtml).setFooterHtml(footerHtml).show();
785  }
786
787  showScreenshot(screenshotUrl) {
788    let bodyHtml = '';
789
790    bodyHtml += '<div class="popup-screenshot-container with-icon icon-waiting">';
791    bodyHtml += '<img src="' + screenshotUrl + '" alt="">';
792    bodyHtml += '</div>';
793
794    this.setBodyHtml(bodyHtml).show();
795  }
796
797  showGDPR(message) {
798    const headline = ''
799    const bodyHtml = message
800    let footerHtml = ''
801    footerHtml += '<div class="popup-container--footer-action"><button id="accept_gdpr_button" type="button" class="button">' + gettext('Accept') + '</a></div>'
802    this.showPopupCloseButton = false
803    this.runCloseEvent = false
804    this.setAdditionalPopupClass('popup-gdpr').setClass('popup-container popup-container-gdpr').setHeadline(headline).setBodyHtml(bodyHtml).setFooterHtml(footerHtml).show()
805  }
806
807  showGDPRPrivacyPolicy(content) {
808    const id = 'popup_gdpr_privacy_policy'
809    const headline = gettext("Privacy policy")
810    const bodyHtml = content
811    let footerHtml = ''
812    footerHtml += '<div class="popup-container--footer-close"><button type="button" class="button popup-close">' + gettext('Close') + '</button></div>'
813    footerHtml += '<div class="popup-container--footer-action m-r-1"><a href="#top-pop-up-attachment" aria-label="' + gettext('Go back') + '" class="button button-outline">
813' + gettext('Go back') + '</a></div>'
814
815    this.showPopupCloseButton = true
816    this.setClass('popup-container popup-gdpr-privacy-policy').setHeadline(headline).setBodyHtml(bodyHtml).setFooterHtml(footerHtml).show(id, true)
817
818    this.closeButtonHandler(id)
819  }
820
821  showRedirect(name, content, redirect_link) {
822    const id = 'popup_redirect'
823    const headline = name;
824    const bodyHtml = content;
825    let footerHtml = ''
826    footerHtml += '<div class="popup-container--footer-action m-r-1"><a href="' + redirect_link + '" class="button">Ir a GamersGate.es</a></div>'    
827    footerHtml += '<div class="popup-container--footer-close"><button type="button" class="button button-outline popup-close" id="refuse_redirect_popup_btn">Seguir en .com/Stay on .com</button></div>'
828
829    this.showPopupCloseButton = true
830    this.setClass('popup-container popup-redirect').setHeadline(headline).setBodyHtml(bodyHtml).setFooterHtml(footerHtml).show()
831
832    this.closeButtonHandler(id)
833  }
834
835  showAgeRequest(data) {
836    const id = 'popup_age_request';
837    const body = document.querySelector('#age_request_popup_body');
838    body.querySelector('#check-age-container--name').innerHTML = data.name;
839    body.querySelector('#check-age-container--image').src = data.image;
840    body.querySelector('.check-age-form').dataset.productId = data.id;
841    this.showPopupCloseButton = true;
842    this.setClass('popup-container popup_age_request').setBodyHtml(body.innerHTML).show();
843
844    this.closeButtonHandler(id);
845    new Select('.popup_age_request .select');
846    ageFormHandler(document.querySelector('.popup_age_request .check-age-form'));
847  }
848
849  showAgeError(data) {
850    const id = 'popup_age_error';
851    const body = document.querySelector('#age_error_popup_body');
852    body.querySelector('#check-age-container--name').innerHTML = data.name;
853    body.querySelector('#check-age-container--image').src = data.image;
854    const form = body.querySelector('.check-age-form');
855    form.dataset.productId = data.id;
856    form.dataset.productName = data.name;
857    form.dataset.productImage = data.image;
858    this.showPopupCloseButton = true;
859    this.setClass('popup-container popup_age_error').setBodyHtml(body.innerHTML).show();
860
861    this.closeButtonHandler(id);
862    ageFormHandler(document.querySelector('.popup_age_error .check-age-form'));
863  }  
864}
865
866class SearchForm {
867  constructor(selector, language) {
868    this.form = document.querySelector(selector)
869    this.language = language
870    this.url = '/api/search/'
871    this.wait = false
872    if (this.form) {
873      this.listen()
874    }
875    return this
876  }
877  
878  listen() {
879    this.form.querySelector('input[type="search"]').addEventListener('input', this.query.bind(this))
880    this.form.querySelector('input[type="button"]').addEventListener('click', function(event) {
881      this.form.submit()
882    }.bind(this))
883  }
884
885  listenItem() {
886    this.form.parentNode.querySelectorAll('.navigation-search--data--item').forEach(function(elem) {
887      elem.addEventListener('click', function() {
888        document.dispatchEvent(new CustomEvent('select_dynamic_search_item', {detail: {data: this.dataset}}))
889        top.location.href = this.dataset.href
890      })
891    })
892  }
893  
894  query(event) {
895    const value = event.target.value
896    if (value.length > 0) {
897      this.wait = true
898      this.remove()
899      fetch(encodeURI(get_url_with_locale(this.url, this.language) + value + '/'), {
900          method: 'GET',
901          headers: {
902            'X-CSRFToken': getCookie('csrftoken')
903          }
904        })
905        .then(checkStatus)
906        .then(parseJSON)
907        .then(function(data) {
908          this.wait = false
909          return data
910        }.bind(this))
911        .then(this.show.bind(this))
912        .catch(function() {
913          this.wait = false
914        }.bind(this))
915    }
916    
917    if (value.length == 0) {
918      this.wait = true
919      this.remove()
920    }
921  }
922  
923  show(data) {
924    if (data.success) {
925      let html = ''
926      html += '<div class="navigation-search--data">'
927      if (data.genres) {
928        html += '<div class="navigation-search--data--description">'
929        html += '<p>' + gettext('Search by game genres') + '</p>'
930        html += '<div class="navigation-search--data--genres">'
931        for (let index = 0; index < data.genres.length; index++) {
932          html += '<div class="navigation-search--data--genre"><a href="' + data.genres[index].link + '">' + data.genres[index].name + '</a></div>'
933        }
934        html += '</div>'
935        html += '</div>'
936      }
937      html += '<ul class="navigation-search--data--list">'
938      for (let index = 0; index < data.products.length; index++) {
939        html += '<li class="navigation-search--data--item" data-href="' + data.products[index].link + '" data-name="' + data.products[index].name + '" data-sku="' + data.products[index].sku + '" data-vendor_name="' + data.products[index].vendor_name + '" data-price="' + data.products[index].raw_price + '" data-currency="' + data.products[index].currency_code + '">'
940        html += '<div class="navigation-search--data--item--image">'
941        html += '<a href="' + data.products[index].link + '"><img src="' + data.products[index].image + '" alt="' + data.products[index].name + '"></a>'
942        if (data.products[index].labels) {
943          html += '<ul class="product--labels">'
944          for (let key in data.products[index].labels) {
945            html += '<li class="navigation-search--data--item--label product--label-' + key + '"><span>' + data.products[index].labels[key] + '</span></li>'
946          }
947          html += '</ul>'
948        }
949        html += '</div>'
950        html += '<div class="navigation-search--data--item--description">'
951        html += '<div class="navigation-search--data--item--title"><a href="' + data.products[index].link + '">' + data.products[index].name + '</a></div>'
952        if ('discount' in data.products[index].labels) {
953          html += '<div class="navigation-search--data--item--full-price">' + data.products[index].full_price + '</div>'
954        }
955        html += '<div class="navigation-search--data--item--price">' + data.products[index].price + '</div>'
956        html += '</div>'
957        html += '</li>'
958      }
959      html += '</ul>'  
960      html += '</div>'
961      
962      this.remove()
963      this.form.parentNode.insertAdjacentHTML('beforeend', html)
964      this.listenItem()
965      document.dispatchEvent(new CustomEvent('dynamic_search_shown'))
966      setTimeout(this.closeEvent.bind(this), 500)
967    }
968  }
969  
970  remove() {
971    const node = this.form.parentNode.querySelector('.navigation-search--data')
972    if (node && node.parentNode) {
973      node.parentNode.removeChild(node)
974    }
975    this.wait = false
976  }
977  
978  closeEvent() {
979    const element = this.form.parentNode
980    
981    const isVisible = function(elem) {
982      return !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length )
983    }
984    
985    const removeNode = function() {
986      removeClickListener()
987      this.remove()
988    }.bind(this)
989    
990    const outsideClickListener = function(event) {
991      if (!element.contains(event.target) && isVisible(element)) {
992        removeNode()
993      }
994    }
995    
996    const removeClickListener = function() {
997      document.removeEventListener('click', outsideClickListener)
998    }
999    
1000    document.addEventListener('click', outsideClickListener)
1001  }  
1002}
1003
1004class Slider {
1005  constructor(selector) {
1006    this.speed = 5000;
1007    this.time = null;
1008    this.slider = document.querySelector(selector);
1009    this.bannerCount = 0;
1010    if (this.slider) {
1011      this.init()
1012      this.listen()
1013      this.listenRotate()
1014      this.listenSwipe()
1015    }
1016  }
1017
1018  init() {   
1019    this.banners = this.slider.querySelectorAll('.slider-item');
1020    this.build();
1021    if (this.bannerCount > 0) {
1022      this.current = parseInt(this.slider.querySelector('.slider-item.is_active').dataset.banner_id);
1023
1024      const firstItem = this.slider.querySelectorAll('.slider-item').item(0);
1025      const firstItemImage = firstItem.querySelector('img');
1026
1027      firstItemImage.decode().then(() => {
1028        this.slider.querySelectorAll('.slider-item img').forEach((imgEl) => {
1029          imgEl.removeAttribute('loading');
1030        });
1031        this.run();
1032      });
1033    }
1034  }
1035  
1036  listen() {
1037    this.slider.querySelectorAll('.slider-dot').forEach(function(elem, index) {
1038      elem.addEventListener('click', function(event) {this.next(event.target.dataset.banner_id)}.bind(this))
1039    }.bind(this))
1040    this.slider.querySelector('.slider-next-button').addEventListener('click', function(event) {this.next()}.bind(this))
1041    this.slider.querySelector('.slider-prev-button').addEventListener('click', function(event) {this.prev()}.bind(this))
1042  }
1043
1044  listenSwipe() {
1045    this.slider.addEventListener('swiped', function(event) {
1046      if (event.detail.dir == 'left') {
1047        this.next()
1048      }
1049      else if (event.detail.dir == 'right') {
1050        this.prev()
1051      }
1052    }.bind(this))
1053  }
1054
1055  listenRotate() {
1056    document.addEventListener('rotate', function(event) {
1057      this.bannerCount = 0
1058      this.time = null
1059      this.removeNode(this.slider.querySelector('.slider-prev-button'))
1060      this.removeNode(this.slider.querySelector('.slider-next-button'))
1061      this.removeNode(this.slider.querySelector('.slider-dots'))
1062      this.init()
1063      this.listen()
1064    }.bind(this))
1065  }
1066  
1067  build() {
1068    let dots = []
1069    this.banners.forEach(function(elem) {
1070        delete elem.dataset.banner_id
1071        elem.classList.remove('is_active')
1072        const style = getComputedStyle(elem)
1073        if (style.getPropertyValue('--view').trim() === "true") {
1074          elem.dataset.banner_id = this.bannerCount
1075          dots.push('<div class="slider-dot' + (this.bannerCount == 0 ? ' is_active' : '') + '" data-banner_id="' + this.bannerCount + '"></div>')
1076          this.bannerCount = this.bannerCount + 1
1077        }
1078    }.bind(this))
1079    if (this.bannerCount > 0) {
1080      this.slider.insertAdjacentHTML('beforeend', '<div class="slider-dots">' + dots.join('') + '</div>')
1081      this.slider.querySelector('[data-banner_id="0"]').classList.add('is_active')
1082      this.background()
1083      this.slider.insertAdjacentHTML('beforeend', '<div class="slider-prev-button"></div>')
1084      this.slider.insertAdjacentHTML('beforeend', '<div class="slider-next-button"></div>')
1085    }
1086    this.current = 0
1087  }
1088
1089  run() {
1090    this.time = setTimeout(this.next.bind(this), this.speed)
1091    this.next(0)
1092  }
1093  
1094  prev() {
1095    const index = this.current == 0 ? this.bannerCount - 1 : this.current - 1
1096    this.next(index)
1097  }
1098  
1099  next(index=null) {
1100    clearTimeout(this.time)
1101    this.slider.querySelectorAll('[data-banner_id="' + this.current + '"]').forEach(function(elem, index) {
1102      elem.classList.remove("is_active")
1103    })
1104    this.current = (this.current + 1 >= this.bannerCount) ? 0 : this.current + 1
1105    if (index !== undefined && index !== null) {
1106      this.current = parseInt(index)
1107    }
1108    this.slider.querySelectorAll('[data-banner_id="' + this.current + '"]').forEach(function(elem, index) {
1109      elem.classList.add("is_active")
1110    })
1111    this.background()
1112
1113    let active_frame = this.slider.querySelector('.slider-item[data-banner_id="' + this.current + '"]')
1114    document.dispatchEvent(new CustomEvent('slider_frame_shown', {detail: active_frame.dataset}))
1115
1116    this.time = setTimeout(this.next.bind(this), this.speed)
1117  }
1118
1119  background() {
1120    const dataset = this.slider.querySelector('.slider-item.is_active').dataset
1121    this.slider.parentNode.style.background = 'linear-gradient(to right, ' + dataset.bg_left + ' 50%, ' + dataset.bg_right + ' 50%)'
1122  }
1123
1124  removeNode(node) {
1125    if (node && node.parentNode) {
1126      node.parentNode.removeChild(node)
1127    }
1128  }
1129}
1130
1131class ProductSlider {
1132  constructor(selector) {
1133    this.screen = null
1134    this.screenData = {}
1135    this.screenListenerHandler = (event) => {
1136      if (this.screenData.type === 'youtube') {
1137        const url = 'https://www.youtube-nocookie.com/embed/' + this.screenData.youtubeId + '?enablejsapi=1&amp;version=3&amp;playerapiid=ytplayer-' + this.screenData.youtubeId + '&amp;autoplay=1&amp;showinfo=0&amp;rel=0&amp;loop=1&amp;';
1138        let markup = '';
1139        markup += '<iframe id="ytplayer-' + this.screenData.youtubeId + '" type="text/html" referrerpolicy="str
1139ict-origin-when-cross-origin" width="' + this.screen.clientWidth + '" height="' + Math.floor(0.565 * this.screen.clientWidth) + '" data-cookieconsent="preferences" src="' + url + '" frameborder="0" allowfullscreen="" allow="autoplay"></iframe>';
1140        this.screen.innerHTML = markup;
1141      }
1142      if (this.screenData.type === 'image') {
1143        event.preventDefault();
1144        new PopUp().showScreenshot(this.screen.querySelector('img').dataset.original);
1145      }    
1146    };
1147    this.choices = document.querySelector('.product-slider-choices')
1148    if (this.choices) {
1149      this.previewContainer = this.choices.querySelector('.preview-container')
1150      this.choicesList = this.previewContainer.querySelector('ul')
1151      this.choicesFirstItem = this.choicesList.querySelectorAll('li')[0]
1152      this.choicesListWidth = this.choicesList.clientWidth/10
1153      this.choicesListFullWidth = this.choicesList.scrollWidth/10
1154      this.sliderItemWidth = 0
1155      this.sliderItemCount = 0
1156      this.offsetMax = 0
1157      this.current = 0
1158
1159      this.build()
1160      this.listen()
1161    }
1162  }
1163
1164  build() {
1165    this.choices.insertAdjacentHTML('beforebegin', '<div class="product-slider-screen"></div>')
1166    this.screen = document.querySelector('.product-slider-screen')
1167    this.sliderItemCount = this.choicesList.querySelectorAll('li').length
1168    this.sliderItemWidth = this.choicesFirstItem.offsetWidth/10 + 2
1169    for (let i = 0; i < this.sliderItemCount; i++) {
1170      const offset = -1*this.sliderItemWidth*i + 1
1171      if (this.choicesListFullWidth + offset + 1 <= this.choicesListWidth) {
1172        break
1173      }
1174      this.offsetMax = offset
1175    }
1176    this.choicesList.style.width = this.choicesListFullWidth + 'rem'
1177    if (this.choicesListWidth < this.choicesListFullWidth) {
1178      this.choices.insertAdjacentHTML('afterbegin', '<button class="prev" id="product-slider-prev-btn" aria-label="Previous"></button>')
1179      this.choices.insertAdjacentHTML('beforeend', '<button class="next" id="product-slider-next-btn" aria-label="Next"></button>')
1180    }
1181  }
1182
1183  listen() {
1184    this.choicesList.querySelectorAll('li').forEach(function(elem, index) {
1185      if (index === 0) {
1186        this.handler(elem)
1187      }
1188      elem.dataset.id = index
1189      elem.addEventListener('click', function(event) {
1190        this.current = parseInt(event.target.dataset.id)
1191        this.handler(event.target)
1192      }.bind(this))
1193    }.bind(this))
1194
1195    if (this.choicesListWidth < this.choicesListFullWidth) {
1196      this.choices.querySelector('.next').addEventListener('click', function(event) {
1197        this.current = this.current + 1
1198        if (this.current > this.sliderItemCount - 1) {
1199          this.current = 0
1200        }
1201        this.handler(this.choicesList.querySelector('li[data-id="' + this.current + '"]'), true)
1202      }.bind(this))
1203
1204      this.choices.querySelector('.prev').addEventListener('click', function(event) {
1205        this.current = this.current - 1
1206        if (this.current < 0) {
1207          this.current = this.sliderItemCount - 1
1208        }
1209        this.handler(this.choicesList.querySelector('li[data-id="' + this.current + '"]'), true)
1210      }.bind(this))
1211    }
1212  }
1213
1214  handler(elem, to_move=false) {
1215    const type = elem.dataset.type
1216    const src = elem.dataset.src
1217    const original = elem.dataset.original
1218    const youtubeId = elem.dataset.youtubeId
1219
1220    if (this.screen.offsetHeight > 0) {
1221      this.screen.style.height = this.screen.offsetHeight + 'px'
1222    }
1223    this.screen.innerHTML = ''
1224
1225    if (to_move) {
1226      const offset = -1*this.sliderItemWidth*this.current + 1
1227      //this.choicesFirstItem.style.cssText = 'margin-left:' + (offset < this.offsetMax ? this.offsetMax : offset) + 'rem !important'
1228      this.choicesList.style.transform = 'translateX(' + (offset < this.offsetMax ? this.offsetMax : offset) + 'rem)'
1229    }
1230
1231    this.choicesList.querySelectorAll('li').forEach(function(e) {
1232      e.classList.remove('is_active')
1233    })
1234    elem.classList.add('is_active')
1235
1236    this.screen.removeEventListener('click', this.screenListenerHandler);
1237
1238    switch(type) {
1239      case 'youtube':
1240        this.screenData = {
1241          'type': type,
1242          'src': src,
1243          'youtubeId': youtubeId
1244        }
1245        this.handlerYoutube()
1246        break
1247      case 'image':
1248        this.screenData = {
1249          'type': type,
1250          'src': src,
1251          'original': original
1252        }        
1253        this.handlerImage()
1254        break
1255      default:
1256        break
1257    }
1258  }
1259
1260  handlerYoutube() {
1261    this.screen.style.position = 'relative'
1262    let html = ''
1263    html += '<img src="' + this.screenData.src + '" alt="Click to view video">'
1264    html += '<div class="youtube-play-button-for-product-slider"><svg width="72" height="84" viewBox="0 0 72 84" fill="none" xmlns="http://www.w3.org/2000/svg" id="youtube_play_button_for_product_slider">'
1265    html += '<path d="M70.5 39.4019C72.5 40.5566 72.5 43.4434 70.5 44.5981L4.50001 82.7032C2.50001 83.8579 1.13066e-05 82.4145 1.14076e-05 80.1051L1.47388e-05 3.89488C1.48398e-05 1.58548 2.50001 0.142104 4.50001 1.2968L70.5 39.4019Z" fill="white"/>'
1266    html += '</svg></div>'
1267    this.screen.innerHTML = html
1268    this.screen.style.height = Math.floor(this.screen.clientWidth * 0.565) + 'px'
1269    this.screen.addEventListener('click', this.screenListenerHandler)
1270  }
1271
1272  handlerImage() {
1273    this.screen.innerHTML = '<img src="' + this.screenData.src + '" data-original="' + this.screenData.original + '" alt="Click to view larger image">'
1274    this.screen.querySelector('img').style.height = Math.floor(this.screen.clientWidth * 0.565) + 'px'
1275    this.screen.querySelector('img').style.objectFit = 'contain'
1276    this.screen.addEventListener('click', this.screenListenerHandler)
1277  }
1278}
1279
1280class BaseForm {
1281  constructor() {
1282    this.form_errors = []
1283    this.form = null
1284    this.formErrorElem = null
1285  }
1286
1287  reset() {
1288    this.form_errors = []
1289    if (this.formErrorElem !== undefined && this.formErrorElem !== null) {
1290      this.formErrorElem.innerHTML = ''
1291      this.formErrorElem.remove()
1292    }
1293    this.formErrorElem = null
1294  }
1295
1296  error(elem, text) {
1297    let i=1;
1298    elem.parentNode.classList.remove('error')
1299    if (text != '') {
1300      elem.parentNode.classList.add('error')
1301    }
1302    while (elem) {
1303      elem = elem.nextSibling
1304      if (elem && elem.classList && elem.classList.contains('field-error')) {
1305        elem.innerHTML = text
1306        elem.style.display = text == '' ? 'none' : 'block'
1307      }
1308      i++
1309      if (i>7) {
1310        break
1311      }
1312    }
1313  }
1314
1315  form_error(elem, text) {
1316    const error_key = typeof elem === 'string' ? elem : elem.id.toString()
1317    this.form_errors[error_key] = text
1318    if (text == '') {
1319      delete this.form_errors[error_key]
1320    }
1321
1322    if (Object.keys(this.form_errors).length > 0) {
1323      if (this.formErrorElem == null) {
1324        const nodes = this.form.querySelectorAll('.form-group:last-child')
1325        nodes[nodes.length- 1].insertAdjacentHTML('beforebegin', '<div class="form-error"></div>')
1326        this.formErrorElem = this.form.querySelector('.form-error')
1327      }
1328      this.formErrorElem.innerHTML = Object.keys(this.form_errors).map(function(x){return this.form_errors[x];}.bind(this)).join('<br>')
1329    }
1330    else {
1331      this.reset()
1332    }
1333  }
1334}
1335
1336
1337class ProductRecommendedGames {
1338  constructor(product_id, language) {
1339    this.product_id = product_id;
1340    this.language = language;
1341    this.url = '/api/recommended_games/';
1342    this.get();
1343  }
1344
1345  get() {
1346    const params = new URLSearchParams();
1347    params.append('product_id', this.product_id);
1348    fetch(get_url_with_locale(this.url, this.language)  + '?' + params.toString(), {
1349        method: 'GET',
1350        headers: {
1351          'X-CSRFToken': getCookie('csrftoken'),
1352          'Accept': 'application/json',
1353          'Content-Type': 'application/json;charset=UTF-8',
1354          'X-Requested-With': 'XMLHttpRequest'
1355        },
1356        async: true
1357      })
1358      .then(checkStatus)
1359      .then(parseJSON)
1360      .then(function(data) {
1361        if (data.hasOwnProperty('success') && data.hasOwnProperty('markup')) {
1362          const containerElement = document.getElementById('recommended-games-container');
1363          containerElement.innerHTML = data.markup;
1364          new Tab(containerElement);
1365        }
1366      }.bind(this))
1367      .catch(function(message) {})
1368  }
1369}
1370
1371
1372class SteamDeckDescriptions {
1373  constructor(product_id, language) {
1374    this.product_id = product_id
1375    this.language = language
1376    this.url = '/api/steam_deck_descriptions/'
1377    this.listen()
1378  }
1379
1380  listen() {
1381    const btnEl = document.getElementById('steam-deck-popup')
1382    if (btnEl) {
1383      btnEl.addEventListener('click', (event) => {
1384        event.preventDefault()
1385        this.get_descriptions()
1386      })
1387    }
1388  }
1389
1390  get_descriptions() {
1391    const params = (new URL(window.location.toString().split(/[?#]/)[0])).searchParams
1392    params.append('product_id', this.product_id)
1393    fetch(get_url_with_locale(this.url, this.language)  + '?' + params.toString(), {
1394        method: 'GET',
1395        headers: {
1396          'X-CSRFToken': getCookie('csrftoken'),
1397          'Accept': 'application/json',
1398          'Content-Type': 'application/json;charset=UTF-8',
1399          'X-Requested-With': 'XMLHttpRequest'
1400        },
1401        async: false
1402      })
1403      .then(checkStatus)
1404      .then(parseJSON)
1405      .then(function(data) {
1406        this.render(data)
1407      }.bind(this))
1408      .catch(function(message) {})
1409  }
1410
1411  render(data) {
1412    let body = '<div class="steam-deck-descriptions">'
1413    data.descriptions.forEach(function(i) {
1414      body = body + '<div class="steam-deck-description steam-deck-' + i.type + '">' + i.content + '</div>'
1415    })
1416    body = body + '</div>'
1417    new PopUp().showErrorPopUp(data.title, body)
1418  }
1419}
1420
1421class ProductComments extends BaseForm {
1422  constructor(product_id, is_auth, language, nickname, is_disallow, product_url) {
1423    super()
1424    this.product_id = product_id
1425    this.is_auth = is_auth
1426    this.language = language
1427    this.nickname = nickname
1428    this.product_url = product_url
1429    this.is_disallow = is_disallow
1430    this.is_empty = true
1431    this.containerElement = document.querySelector('.product-user-comments')
1432    this.listSectionElement = null
1433    this.url = '/api/comments/'
1434    this.page = null
1435    this.get_comments()
1436  }
1437
1438  get_comments() {
1439    if (!this.containerElement) return;
1440    const params = (new URL(window.location.toString().split(/[?#]/)[0])).searchParams
1441    params.append('product_id', this.product_id)
1442    if (this.page !== null) {
1443      params.append('page', this.page)
1444    }
1445    fetch(get_url_with_locale(this.url, this.language)  + '?' + params.toString(), {
1446        method: 'GET',
1447        headers: {
1448          'X-CSRFToken': getCookie('csrftoken'),
1449          'Accept': 'application/json',
1450          'Content-Type': 'application/json;charset=UTF-8',
1451          'X-Requested-With': 'XMLHttpRequest'
1452        },
1453        async: false
1454      })
1455      .then(checkStatus)
1456      .then(parseJSON)
1457      .then(function(data) {
1458        this.renderListSection(data)
1459        this.renderEmptySection()
1460        this.renderToolsSection(data.next_page)
1461        this.renderAddNewCommentButton()
1462      }.bind(this))
1463      .catch(function(message) {})
1464  }
1465
1466  renderListSection(data) {
1467    if (this.is_empty && data.count > 0) {
1468      this.initListSection()
1469    }
1470    data.tickets.forEach(function(ticket, index) {
1471      this.listSectionElement.insertAdjacentHTML('beforeend', this.getCommentsBranchHtml(ticket.messages))
1472    }.bind(this))
1473    this.containerElement.querySelectorAll('.comment--reply-button').forEach(function(elem) {
1474      this.listenReplyCommentButton(elem.dataset.id)
1475    }.bind(this))
1476  }
1477
1478  initListSection() {
1479    this.is_empty = false
1480    this.containerElement.insertAdjacentHTML('beforeend', '<div class="comment-list"></div>')
1481    this.listSectionElement = this.containerElement.querySelector('.comment-list')
1482  }
1483
1484  getCommentsBranchHtml(messages, get_only_body=false) {
1485    let html = ''
1486    if (messages.length > 0) {
1487      const message = messages.shift()
1488      const id = message.id
1489
1490      if (!get_only_body) {
1491        html += '<div class="comment" data-id="' + message.id + '">'
1492      }
1493
1494      html += this.getCommentHtml(message, !this.is_disallow)
1495
1496      if (messages.length > 0) {
1497        html += '<ul class="comment--child-comments">'
1498        messages.forEach(function(message, index) {
1499          html += this.getCommentHtml(message, false, true)
1500        }.bind(this))
1501        html += '</ul>'
1502      }
1503      if (!get_only_body) {
1504        html += '</div>'
1505      }
1506    }
1507    return html
1508  }
1509
1510  getCommentHtml(message, show_reply, first_comment=false) {
1511    let html = ''
1512
1513    if (first_comment) {
1514      html += '<div class="comment">'
1515    }
1516    html += '  <div class="comment--header">'
1517    html += '    <div class="comment--avatar"><img src="' + message.user_src + '" loading="auto" alt=""></div>'
1518    html += '    <div class="comment--title">'
1519    html += '      <div class="comment--username">' + (message.is_admin ? gettext('Reply from GamersGate') : message.user_name) + '</div>'
1520    html += '      <div class="comment-created">' + timestamp_to_datetime_format(message.created) +'</div>'
1521    html += '      <div class="comment--content">' + message.content.replace(/\n\r/g, "<br/>").replace(/\n/g, "<br/>") + '</div>'
1522    if (message.is_admin && message.image_url) {
1523      html += '    <div class="comment--image"><a href="' + message.image_url + '" target="_blank"><img src="' + message.image_url + '" loading="auto" alt=""></a></div>'
1524    }
1525    if (show_reply) {
1526      html += '      <a class="comment--reply-button" href="#" data-id="' + message.id + '">' + gettext('Reply') + '</a>'
1527    }
1528    html += '    </div>'
1529    html += '  </div>'
1530    if (first_comment) {
1531      html += '</div>'
1532    }
1533    return html
1534  }
1535
1536  renderEmptySection() {
1537    if (this.containerElement.querySelector('.comment--empty')) {
1538      this.containerElement.querySelector('.comment--empty').innerHTML = ''
1539      this.containerElement.querySelector('.comment--empty').remove()
1540    }
1541
1542    let html = ''
1543    if (this.is_empty && this.is_auth) {
1544      html += '<div class="comment--empty"><p>' + gettext('No user comments yet') + '</p></div>'
1545    }
1546    if (!this.is_auth) {
1547      html += '<div class="comment--empty">'
1548        html += '<p>'
1549        if (this.is_empty) {
1550            html += gettext('Nothing has been said about this yet. You will literally be the first person to say something about it.')
1551            html += '</p><p>'
1552            html += gettext('Just {0} or {1} and make history!').format('<a href="' + get_url_with_locale('/login', this.language) + '?returnurl=' + this.product_url + '">' + gettext('Sign In') + '</a>', '<a href="' + get_url_with_locale('/register', this.language) + '">' + gettext('Create account') + '</a>')
1553        } else {
1554            html += gettext('Please {0} or {1} to comment').format('<a href="' + get_url_with_locale('/login', this.language) + '?returnurl=' + this.product_url + '">' + gettext('Sign In') + '</a>', '<a href="' + get_url_with_locale('/register', this.language) + '">' + gettext('Create account') + '</a>')
1555        }
1556        html += '</p>'
1557      html += '</div>'
1558    }
1559    this.containerElement.querySelector('h2').insertAdjacentHTML('afterend', html)
1560  }
1561
1562  renderToolsSection(page) {
1563    if (this.containerElement.querySelector('.comment--tools')) {
1564      this.containerElement.querySelector('.comment--tools').innerHTML = ''
1565      this.containerElement.querySelector('.comment--tools').remove()
1566    }
1567    if (page !== null) {
1568      this.page = page
1569      this.containerElement.insertAdjacentHTML('beforeend', '<div class="comment--tools"><button class="comment--show-more-button">' + gettext('Show more') + '</button></div>')
1570      this.listenShowMoreButton()
1571    }
1572  }
1573
1574  renderAddNewCommentButton() {
1575    if (!this.is_disallow && this.is_auth && !this.containerElement.querySelector('.comment--add-button')) {
1576      this.containerElement.querySelector('h2').insertAdjacentHTML('beforeend', '<a href="#" class="comment--add-button">' + gettext('+ New сomment') + '</a>')
1577      this.listenAddNewCommentButton()
1578    }
1579  }
1580
1581  listenReplyCommentButton(id) {
1582    this.containerElement.querySelector('.comment--reply-button[data-id="' + id + '"]').addEventListener('click', this.replyCommentButtonHandler.bind(this))
1583  }
1584
1585  listenAddNewCommentButton() {
1586    this.containerElement.querySelector('.comment--add-button').addEventListener('click', this.addNewCommentButtonHandler.bind(this))
1587  }
1588
1589  listenPostCommentForm() {
1590    this.containerElement.querySelector('input[type="button"]').addEventListener('click', this.postCommentFormHandler.bind(this))
1591  }
1592
1593  listenRemoveCommentForm() {
1594    this.containerElement.querySelector('input[type="reset"]').addEventListener('click', this.removeCommentFormHandler.bind(this))
1595  }
1596
1597  listenShowMoreButton() {
1598    this.containerElement.querySelector('.comment--show-more-button').addEventListener('click', this.get_comments.bind(this))
1599  }
1600
1601  addNewCommentButtonHandler(event) {
1602    event.preventDefault()
1603    event.target.style.display = 'none'
1604
1605    this.removeForms()
1606    this.showAllReplyCommentButton()
1607    this.removeEmptySection()
1608    this.containerElement.querySelector('h2').insertAdjacentHTML('afterend', this.getCommentFormHtml())
1609
1610    this.listenPostCommentForm()
1611    this.listenRemoveCommentForm()
1612  }
1613
1614  replyCommentButtonHandler(event) {
1615    event.preventDefault()
1616
1617    const id = event.target.dataset.id
1618
1619    this.removeForms()
1620    this.showAllReplyCommentButton()
1621    this.containerElement.querySelector('.comment--add-button').style.display = 'inline-block'
1622    event.target.style.display = 'none'
1623    event.target.insertAdjacentHTML('afterend', this.getCommentFormHtml(id))
1624
1625    this.listenPostCommentForm()
1626    this.listenRemoveCommentForm()
1627  }
1628
1629  validUsername() {
1630    const element = this.form.querySelector('input[name="nickname"]')
1631    if (element) {
1632      const is_valid = validatorEmpty(element.value.trim()) && element.value.trim().length <= 30
1633      this.form_error(element, is_valid ? '' : gettext('Username must not be empty or longer than 30 characters'))
1634      return is_valid
1635    }
1636    return true
1637  }
1638
1639  validContent() {
1640    const element = this.form.querySelector('[name="content"]')
1641    const is_valid = validatorEmpty(element.value.trim()) && element.value.trim().length <= 3000
1642    this.form_error(element, is_valid ? '' : gettext('Your comment should not be empty or longer than 3000 symbols'))
1643    return is_valid
1644  }
1645
1646  validCharUsername() {
1647    const element = this.form.querySelector('input[name="nickname"]')
1648    if (element) {
1649      const is_valid = element.value.trim().match(/^[\p{L}\p{N}_]+$/u)
1650      this.form_error(element, is_valid ? '' : gettext('Username should contain only letters, numbers and underscore (the "_" symbol)'))
1651      return is_valid
1652    }
1653    return true
1654  }
1655
1656  postCommentFormHandler(event) {
1657    event.preventDefault()
1658
1659    const formElement = event.target.closest('form')
1660    const formData  = new FormData(formElement)
1661    formData.append('product_id', this.product_id)
1662
1663    this.form = formElement
1664    this.reset()
1665
1666    if (this.validUsername() && this.validCharUsername() && this.validContent()) {
1667      fetch(get_url_with_locale(this.url, this.language), {
1668          method: 'POST',
1669          headers: {
1670            'X-CSRFToken': getCookie('csrftoken')
1671          },
1672          body: formData
1673        })
1674        .then(checkStatus)
1675        .then(parseJSON)
1676        .then(function(data) {
1677          if (data.success) {
1678            if (formData.has('comment_id')) {
1679              this.listSectionElement.querySelector('.comment[data-id="' + formData.get('comment_id') + '"]').innerHTML = ''
1680              this.listSectionElement.querySelector('.comment[data-id="' + formData.get('comment_id') + '"]').insertAdjacentHTML('beforeend', this.getCommentsBranchHtml(data.messages, true))
1681              this.listenReplyCommentButton(formData.get('comment_id'))
1682            }
1683            else {
1684              if (this.is_empty) {
1685                this.initListSection()
1686              }
1687              this.listSectionElement.insertAdjacentHTML('afterbegin', this.getCommentsBranchHtml(data.messages))
1688              this.listenReplyCommentButton(data.id)
1689            }
1690            this.removeCommentFormHandler(event)
1691          }
1692          if (data.errors) {
1693            const e = Object.keys(data.errors).map(function(x){
1694              return data.errors[x].join('<br>')
1695            }).join('<br>')
1696            this.form_error('comment_error', e)
1697          }
1698        }.bind(this))
1699        .catch(function(error) {
1700          if (error.response.status == 403) {
1701            const title = gettext("Heya!")
1702            const description = gettext('Please {0} or {1} to comment').format('<a href="' + get_url_with_locale('/login', this.language) + '?returnurl=' + this.product_url + '">' + gettext('Sign In') + '</a>', '<a href="' + get_url_with_locale('/register', this.language) + '">' + gettext('Create account') + '</a>')
1703            new PopUp().showErrorPopUp(title, '<p>' + description + '</p>')
1704            return
1705          }
1706          new PopUp().showErrorPopUp(gettext('An error occurred, try again later'))
1707        }.bind(this))
1708    }
1709  }
1710
1711  removeCommentFormHandler(event) {
1712    const formElement = event.target.parentNode.parentNode
1713    const comment_id = formElement.dataset.id
1714
1715    formElement.innerHTML = ''
1716    formElement.parentNode.remove()
1717
1718    this.containerElement.querySelector('.comment--add-button').style.display = 'inline-block'
1719
1720    if (comment_id > 0) {
1721      this.containerElement.querySelector('.comment--reply-button[data-id="' + comment_id + '"]').style.display = 'inline-block'
1722    } else {
1723      this.renderEmptySection()
1724    }
1725  }
1726
1727  getCommentFormHtml(comment_id=null) {
1728    let html = ''
1729    html += '<div class="comment--form' + (comment_id === null ? '' : ' comment--reply-form') + '">'
1730    html += '  <form method="POST" data-id="' + (comment_id === null ? null : comment_id) + '">'
1731    if (comment_id !== null) {
1732      html += '    <input type="hidden" name="comment_id" value="' + comment_id + '">'
1733    }
1734    if (this.nickname === null || this.nickname.toString().length === 0 || this.nickname.includes('customer')) {
1735      html += '    <div class="form-group">'
1736      html += '      <input type="text" name="nickname" placeholder="' + gettext('Type your name') + ' ..." value="' + (this.nickname.includes('customer') ? this.nickname:'') + '" autocomplete="off">'
1737      html += '    </div>'
1738    }
1739    else if (comment_id == null) {
1740      html += '    <p>' + gettext('You will be commenting as') + ' ' + this.nickname + '</p>'
1741    }
1742    if (comment_id === null) {
1743      html += '    <div class="form-group form-group--input-with-button">'
1744      html += '      <textarea rows="6" name="content" placeholder="' + gettext('Type your comment') + ' ..." autocomplete="off"></textarea>'
1745      html += '    </div>'
1746      html += '    <div class="form-group form-group--input-with-button form-group--with-reply-button">'
1747      html += '      <input type="button" value="' + gettext('POST') + '">'
1748      html += '      <input type="reset" class="button-clear" value="' + gettext('HIDE THIS') + '">'
1749      html += '    </div>'
1750    } else {
1751      html += '    <div class="form-group form-group--input-with-button">'
1752      html += '      <textarea rows="6" name="content" placeholder="' + gettext('Type your comment') + ' ..." autocomplete="off"></textarea>'
1753      html += '      <div class="form-group form-group--input-with-button form-group--with-reply-button">'
1754      html += '        <input type="button" value="' + gettext('POST') + '">'
1755      html += '        <input type="reset" class="button-clear" value="' + gettext('HIDE THIS') + '">'
1756      html += '      </div>'
1757      html += '    </div>'
1758      html += '    <div class="form-group"></div>'
1759    }
1760    html += '  </form>'
1761    html += '</div>'
1762
1763    return html
1764  }
1765
1766  removeForms() {
1767    this.containerElement.querySelectorAll('.comment--form').forEach(function(elem) {
1768      elem.innerHTML = ''
1769      elem.remove()
1770    })
1771  }
1772
1773  removeEmptySection() {
1774    if (this.containerElement.querySelector('.comment--empty')) {
1775      this.containerElement.querySelector('.comment--empty').remove()
1776    }
1777  }
1778
1779  showAllReplyCommentButton() {
1780    this.containerElement.querySelectorAll('.comment--reply-button').forEach(function(elem) {
1781      elem.style.display = 'inline-block'
1782    })
1783  }
1784}
1785
1786class AuthorizationForm extends BaseForm {
1787  constructor(selector, show_email, show_password, show_recaptcha) {
1788    super()
1789    this.form = document.querySelector(selector)
1790    this.form_errors = []
1791    this.auto_valid = false
1792    if (this.form !== null) {
1793      this.formErrorElem = this.form.querySelector('.form-error')
1794    }
1795  }
1796  
1797  validEmail() {
1798    this.emailElem.value = this.emailElem.value.trim()
1799    const is_valid = validatorEmail(this.emailElem.value)
1800    this.error(this.emailElem, is_valid ? '' : gettext('Incorrect email'))
1801    return is_valid
1802  }
1803  
1804  validEmailRepeat() {
1805    const is_valid = validatorRepeat(this.emailElem.value, this.emailRepeatElem.value)
1806    this.error(this.emailRepeatElem, is_valid ? '' : gettext('No match'))
1807    return is_valid
1808  }
1809  
1810  noCopyEmailRepeat() {
1811      this.error(this.emailRepeatElem, gettext('No copy&paste'))
1812  }
1813
1814  validPassword() {
1815    const is_valid = validatorEmpty(this.passwordElem.value)
1816    this.error(this.passwordElem, is_valid ? '' : gettext('Incorrect password'))
1817    return is_valid
1818  }
1819
1820  validCaptcha() {
1821    this.form_error(this.recaptchaElem, this.recaptchaError ? gettext('Incorrect CAPTCHA') : '')
1822    return !this.recaptchaError
1823  }
1824
1825  verifyRecaptchaCallback(response) {
1826    this.recaptchaError = (response === undefined || response === null || response === "") ? true : false
1827    this.validCaptcha()
1828  }
1829}
1830
1831class RemindForm extends AuthorizationForm {
1832  constructor(selector, show_recaptcha) {
1833    super(selector)
1834    this.show_recaptcha = show_recaptcha
1835    if (this.form !== null) {
1836      this.emailElem = this.form.querySelector('#id_email')
1837      if (this.show_recaptcha) {
1838        this.recaptchaError = true
1839        this.recaptchaElem = this.form.querySelector('#id_recaptcha')
1840      }
1841      this.agreeElem = this.form.querySelector('#id_agree')
1842      this.listen()
1843    }
1844  }
1845
1846  listen() {
1847    this.form.querySelector('[type="submit"][name="remind"]').addEventListener('click', function(event) {
1848      event.preventDefault()
1849      if (this.valid()) {
1850        this.form.submit()
1851      }
1852    }.bind(this))
1853
1854    this.emailElem.addEventListener('input', function(event) {
1855      if (this.auto_valid) {
1856        this.validEmail()
1857      }
1858    }.bind(this))
1859
1860    this.agreeElem.addEventListener('change', function(event) {
1861      this.validAgree()
1862    }.bind(this))    
1863  }
1864
1865  valid() {
1866    const validItems = []
1867    validItems.push(this.validEmail())
1868    validItems.push(this.validAgree())
1869    if (this.show_recaptcha) {
1870      validItems.push(this.validCaptcha())
1871    }
1872    this.auto_valid = true
1873    return validItems.reduce(function(res, item) {
1874      return res && item
1875    })
1876  }
1877
1878  validAgree() {
1879    var is_valid = true
1880    this.form_error(this.agreeElem.parentNode, '')
1881    if (!this.agreeElem.checked) {
1882      this.form_error(this.agreeElem.parentNode, gettext('Please agree to the Privacy Policy'))
1883      is_valid = false
1884    }
1885    return is_valid
1886  }  
1887}
1888
1889class LoginForm extends AuthorizationForm {
1890  constructor(selector, show_email, show_password, show_recaptcha) {
1891    super(selector)
1892    this.show_email = show_email
1893    this.show_password = show_password
1894    this.show_recaptcha = show_recaptcha
1895    if (this.form !== null) {
1896      if (this.show_email) {
1897        this.emailElem = this.form.querySelector('#id_email')
1898      }
1899      if (this.show_password) {
1900        this.passwordElem = this.form.querySelector('#id_password')
1901      }
1902      if (this.show_recaptcha) {
1903        this.recaptchaError = true
1904        this.recaptchaElem = this.form.querySelector('#id_recaptcha')
1905      }
1906      this.listen()
1907    }
1908  }
1909
1910  listen() {
1911    this.form.querySelector('[type="submit"][name="login"]').addEventListener('click', function(event) {
1912      event.preventDefault()
1913      if (this.valid()) {
1914        this.form.submit()
1915      }
1916    }.bind(this))
1917
1918    if (this.show_email) {
1919      this.emailElem.addEventListener('input', function(event) {
1920        if (this.auto_valid) {
1921          this.validEmail()
1922        }
1923      }.bind(this))
1924    }
1925    if (this.show_repeat) {
1926      this.passwordElem.addEventListener('input', function(event) {
1927        if (this.auto_valid) {
1928          this.validPassword()
1929        }
1930      }.bind(this))
1931    }
1932  }
1933
1934  valid() {
1935    var validItems = []
1936    if (this.show_email) {
1937      validItems.push(this.validEmail())
1938    }
1939    if (this.show_password) {
1940      validItems.push(this.validPassword())
1941    }
1942    if (this.show_recaptcha) {
1943      validItems.push(this.validCaptcha())
1944    }
1945    this.auto_valid = true
1946    return validItems.reduce(function(res, item) {
1947      return res && item
1948    })
1949  }
1950}
1951
1952class RegistrationForm extends AuthorizationForm {
1953  constructor(selector, show_recaptcha) {
1954    super(selector)
1955    this.show_recaptcha = show_recaptcha
1956    if (this.form !== null) {
1957      this.emailElem = this.form.querySelector('#id_email')
1958      this.emailRepeatElem = this.form.querySelector('#id_email_repeat')
1959      if (this.show_recaptcha) {
1960        this.recaptchaError = true
1961        this.recaptchaElem = this.form.querySelector('#id_recaptcha')
1962      }
1963      this.agreeElem = this.form.querySelector('#id_agree')
1964      this.listen()
1965    }
1966  }
1967
1968  listen() {
1969    this.form.querySelector('[type="submit"][name="register"]').addEventListener('click', function(event) {
1970      event.preventDefault()
1971      if (this.valid()) {
1972        this.form.submit()
1973      }
1974    }.bind(this))
1975
1976    this.emailElem.addEventListener('input', function(event) {
1977      if (this.auto_valid) {
1978        this.validEmail()
1979        this.noCopyEmailRepeat()
1980      }
1981    }.bind(this))
1982
1983    this.emailRepeatElem.addEventListener('input', function(event) {
1984      this.validEmailRepeat()
1985    }.bind(this))
1986
1987    this.emailRepeatElem.addEventListener('paste', function(event) {
1988      event.preventDefault()
1989      this.noCopyEmailRepeat()
1990      if (this.auto_valid) {
1991        this.validEmail()
1992      }
1993    }.bind(this))
1994
1995    this.agreeElem.addEventListener('change', function(event) {
1996      this.validAgree()
1997    }.bind(this))
1998  }
1999
2000  valid() {
2001    var validItems = []
2002    validItems.push(this.validEmail())
2003    validItems.push(this.validEmailRepeat())
2004    validItems.push(this.validAgree())
2005    if (this.show_recaptcha) {
2006      validItems.push(this.validCaptcha())
2007    }
2008    this.auto_valid = true
2009    return validItems.reduce(function(res, item) {
2010      return res && item
2011    })
2012  }
2013
2014  validAgree() {
2015    var is_valid = true
2016    this.form_error(this.agreeElem.parentNode, '')
2017    if (!this.agreeElem.checked) {
2018      this.form_error(this.agreeElem.parentNode, gettext('Please agree to the terms and conditions'))
2019      is_valid = false
2020    }
2021    return is_valid
2022  }
2023}
2024
2025class Paginator {
2026  constructor(pageNumber, numberOfPages) {
2027    this.init(pageNumber, numberOfPages)
2028    this.calc()
2029  }
2030
2031  init(pageNumber, numberOfPages) {
2032    this.pageNumber = parseInt(pageNumber)
2033    this.pageNumber = !this.pageNumber ? 1 : this.pageNumber
2034    this.numberOfPages = parseInt(numberOfPages)
2035    this.frameLong = 10
2036    this.framePages = []
2037    this.prevLink
2038    this.prevForwardLink
2039    this.nextLink
2040    this.nextForwardLink
2041    this.showFirst = false
2042    this.showFirstDelimiter = false
2043    this.showLast = false
2044    this.showLastDelimiter = false
2045  }
2046
2047  calc() {
2048    if (this.pageNumber !== null && this.numberOfPages !== null && this.numberOfPages !== 1) {
2049      const frameLong = this.frameLong - 1
2050
2051      this.prevLink = this.pageNumber == 1 ? null : this.pageNumber - 1
2052      this.nextLink = this.numberOfPages == this.pageNumber ? null :  this.pageNumber + 1
2053      this.prevForwardLink = this.pageNumber <= this.frameLong ? null : this.pageNumber - this.frameLong
2054      this.nextForwardLink = this.pageNumber >= this.numberOfPages - this.frameLong ? null : this.pageNumber + this.frameLong
2055
2056      let frameStart = this.pageNumber - parseInt(frameLong / 2)
2057
2058      if (frameStart < 1) {
2059          frameStart = 1
2060      }
2061
2062      if (this.numberOfPages - frameStart < frameLong) {
2063          frameStart = this.numberOfPages - frameLong
2064      }
2065
2066      const frameEnd = frameStart + frameLong
2067
2068      let i = 0
2069      while (i < this.numberOfPages) {
2070          i++
2071          if (i < frameStart) {
2072              continue
2073          }
2074          if (i > frameEnd) {
2075              break
2076          }
2077          this.framePages.push(i)
2078      }
2079
2080      if (this.pageNumber > frameLong/2 + 1) {
2081          this.showFirst = true
2082      }
2083      if (this.pageNumber > frameLong/2 + 2) {
2084          this.showFirstDelimiter = true
2085      }
2086      if (this.pageNumber < this.numberOfPages - frameLong/2 - 1) {
2087          this.showLast = true
2088      }
2089      if (this.pageNumber < this.numberOfPages - frameLong/2 - 2) {
2090          this.showLastDelimiter = true
2091      }
2092    }
2093  }
2094}
2095
2096class CatalogFilter {
2097  constructor() {
2098    this.filters = document.querySelector('.sidebar-catalog-filters')
2099    if (this.filters !== null) {
2100      this.listen()
2101    }
2102  }
2103
2104  listen() {
2105    this.filters.querySelectorAll('.sidebar-catalog-filter .show-all-items').forEach(function(elem) {
2106      elem.addEventListener('click', function(event) {
2107        event.preventDefault()
2108
2109        const newTitle = this.dataset.title
2110        this.dataset.title = this.innerHTML
2111        this.innerHTML = newTitle
2112
2113        const listElement = this.previousElementSibling
2114        listElement.classList && listElement.classList.contains('show_first_any_items') ? listElement.classList.remove('show_first_any_items') : listElement.classList.add('show_first_any_items')
2115      })
2116    })
2117
2118    this.filters.querySelectorAll('.sidebar-catalog-filter.sidebar-action-toggle > span').forEach(function(elem) {
2119      elem.addEventListener('click', function(event) {
2120        event.preventDefault()
2121        const filterElement = this.parentNode
2122        filterElement.classList && filterElement.classList.contains('is_show') ? filterElement.classList.remove('is_show') : filterElement.classList.add('is_show')
2123      })
2124    })
2125
2126    this.filters.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(function(elem) {
2127      elem.addEventListener('change', function(event) {
2128        this.filters.querySelector('.sidebar-catalog-tools').classList.add('is_visible', 'has_delimiter')
2129        this.handler()
2130      }.bind(this))
2131    }.bind(this))
2132
2133    this.filters.querySelectorAll('button[type="submit"]').forEach(function(elem) {
2134      elem.addEventListener('click', function(event) {
2135        event.preventDefault()
2136        this.filters.querySelector('.sidebar-catalog-tools').classList.add('is_visible', 'has_delimiter')
2137        this.handler()
2138      }.bind(this))
2139    }.bind(this))
2140
2141    this.filters.querySelectorAll('[type="number"].only-integer').forEach(function(elem){
2142      const max = parseInt(elem.dataset.max)
2143      const min = parseInt(elem.dataset.min)
2144
2145      function changeInput(e) {
2146        const value = parseInt(this.value)
2147        elem.value = value > max ? max : (value < min ? min : value)
2148      }
2149
2150      function keyInput(e) {
2151        const charCode = (e.which) ? e.which : e.keyCode;
2152        if ((charCode > 47 && charCode < 58) || (charCode >
2152 95 && charCode < 106) || charCode == 8) {
2153          return true
2154        }
2155        e.preventDefault()
2156        return false
2157      }
2158
2159      elem.onfocus = keyInput
2160      elem.onkeydown = keyInput
2161      elem.onchange = changeInput
2162      elem.onpaste = changeInput
2163
2164      elem.oninput = function(e) {
2165        return elem.validity.valid || (elem.value = this.previousValue)
2166      }
2167    })
2168
2169    this.filters.querySelector('#catalog-filter-reset').addEventListener('click', function(event) {
2170      event.preventDefault()
2171      this.filters.querySelectorAll('input').forEach(function(elem) {
2172        const field_type = elem.type.toLowerCase()
2173        switch (field_type) {
2174          case "email":
2175          case "number":
2176          case "text":
2177          case "password":
2178              elem.value = null
2179              break
2180          case "radio":
2181          case "checkbox":
2182              if (elem.checked) {
2183                  elem.checked = false
2184              }
2185              break
2186          default:
2187              break
2188        }
2189      })
2190      this.filters.querySelector('.sidebar-catalog-tools').classList.remove('is_visible', 'has_delimiter')
2191      this.filters.querySelectorAll('.sidebar-catalog-filter.sidebar-action-toggle.is_show').forEach(function(elem) {
2192        elem.classList.remove('is_show')
2193      })
2194      this.handler()
2195    }.bind(this))
2196
2197    document.addEventListener('show_mobile_filter', function(event) {
2198      this.filters.querySelectorAll('.sidebar-catalog-filter.is_show').forEach(function(elem) {
2199        elem.classList.remove('is_show')
2200      })
2201      this.filters.classList.add('show_mobile')
2202      document.querySelector('body').style.overflow = 'hidden'
2203      if (!this.filters.querySelector('#catalog-filter-hide')) {
2204        const elem = this.filters.querySelector('.sidebar-catalog-tools')
2205        elem.classList.add('is_visible', 'has_delimiter')
2206        elem.insertAdjacentHTML('beforeend', '<button class="button button-outline" id="catalog-filter-hide">' + gettext('Hide filters') + '</button>')
2207        this.filters.querySelector('#catalog-filter-hide').addEventListener('click', function(event) {
2208          event.preventDefault()
2209          this.hideMobile()
2210        }.bind(this))
2211      }
2212    }.bind(this))
2213  }
2214
2215  handler() {
2216    document.dispatchEvent(new CustomEvent('catalog', {detail: {}}))
2217    this.hideMobile()
2218  }
2219
2220  hideMobile() {
2221    if ([...this.filters.classList].includes('show_mobile')) {
2222      document.querySelector('body').style.overflow = 'initial'
2223      this.filters.classList.remove('show_mobile')
2224    }
2225  }
2226}
2227
2228class CatalogTools {
2229  constructor() {
2230    this.tools = document.querySelector('.catalog-tools')
2231    if (this.tools) {
2232      this.top = this.tools.getBoundingClientRect().top
2233      this.ticking = false
2234      this.init()
2235      this.listen()
2236    }
2237  }
2238
2239  init() {
2240    if (localStorage.getItem("gg_catalog_view_type")) {
2241      const view = localStorage.getItem("gg_catalog_view_type")
2242      this.tools.querySelectorAll('.catalog-view-type-choice > span').forEach(function(element) {
2243        element.classList.remove('is_active')
2244        if (element.dataset.view == view) {
2245          element.classList.add('is_active')
2246        }
2247      })
2248      document.dispatchEvent(new CustomEvent('catalog-view', {detail: {'view': view}}))
2249    }
2250  }
2251
2252  show() {
2253    this.tools.style.display = 'flex'
2254    return this
2255  }
2256
2257  hide() {
2258    this.tools.style.display = 'none'
2259    return this
2260  }
2261
2262  listen() {
2263    this.tools.querySelectorAll('.catalog-view-type-choice > span').forEach(function(elem) {
2264      elem.addEventListener('click', function(event) {
2265        event.preventDefault()
2266        this.tools.querySelectorAll('.catalog-view-type-choice > span').forEach(function(element) {
2267          element.classList.remove('is_active')
2268        })
2269        event.target.classList.add('is_active')
2270        const view = event.target.dataset.view
2271        localStorage.setItem("gg_catalog_view_type", view)
2272        document.dispatchEvent(new CustomEvent('catalog-view', {detail: {'view': view}}))
2273      }.bind(this))
2274    }.bind(this))
2275
2276    this.tools.querySelectorAll('.catalog-products-per-page .select a').forEach(function(elem) {
2277      elem.addEventListener('click', function(event) {
2278        event.preventDefault()
2279        this.handlerProductsPerPageChoice(event.target.dataset.value)
2280      }.bind(this))
2281    }.bind(this))
2282
2283    this.tools.querySelectorAll('.catalog-sort .select a').forEach(function(elem) {
2284      elem.addEventListener('click', function(event) {
2285        event.preventDefault()
2286        this.handlerSortChoice(event.target.dataset.value)
2287      }.bind(this))
2288    }.bind(this))
2289
2290    document.addEventListener('swiped', function(event) {
2291      if (event.detail.dir == 'up' || event.detail.dir == 'down') {
2292        this.animationTools()
2293      }
2294    }.bind(this))
2295
2296    document.addEventListener('scroll', function(event) {
2297      if (!this.ticking) {
2298        window.requestAnimationFrame(function() {
2299          this.animationTools()
2300        }.bind(this))
2301        this.ticking = true
2302      }
2303    }.bind(this))
2304
2305    if (this.tools.querySelector('#catalog-filter-button')) {
2306      this.tools.querySelector('#catalog-filter-button').addEventListener('click', function(event) {
2307        document.dispatchEvent(new CustomEvent('show_mobile_filter'))
2308      })
2309    }
2310  }
2311
2312  animationTools() {
2313    const toolsTop = this.tools.getBoundingClientRect().top
2314    const bodyTop = document.querySelector('body').getBoundingClientRect().top
2315    const is_fixed = [...this.tools.classList].includes('is_fixed')
2316    if (toolsTop && toolsTop <= 20 && !is_fixed) {
2317      this.tools.classList.add('is_fixed')
2318    }
2319    else if (Math.abs(bodyTop) <= this.top && is_fixed) {
2320      this.tools.classList.remove('is_fixed')
2321    }
2322    this.ticking = false
2323  }
2324
2325  handlerProductsPerPageChoice(value) {
2326    document.dispatchEvent(new CustomEvent('catalog', {detail: {'per_page': value}}))
2327  }
2328
2329  handlerSortChoice(value) {
2330    document.dispatchEvent(new CustomEvent('catalog', {detail: {'sort': value}}))
2331  }
2332}
2333
2334class CatalogPaginator extends Paginator {
2335  constructor(pageNumber=null, numberOfPages=null) {
2336    super(pageNumber, numberOfPages)
2337    this.container = document.querySelector('.catalog-paginator')
2338  }
2339
2340  clear() {
2341    this.container.innerHTML = ''
2342    return this
2343  }
2344
2345  refresh(pageNumber, numberOfPages) {
2346    this.init(pageNumber, numberOfPages)
2347    this.calc()
2348    this.render()
2349    this.listen()
2350    return this
2351  }
2352
2353  render() {
2354    if (this.pageNumber !== null && this.numberOfPages !== null && this.numberOfPages !== 1) {
2355      this.container.insertAdjacentHTML('beforeend', this.template())
2356    }
2357    return this
2358  }
2359
2360  listen() {
2361    this.container.querySelectorAll('a').forEach(function(elem) {
2362      elem.addEventListener('click', function(event) {
2363        event.preventDefault()
2364        this.handler(event.target.dataset.page)
2365      }.bind(this))
2366    }.bind(this))
2367  }
2368
2369  handler(page) {
2370    document.dispatchEvent(new CustomEvent('catalog', {detail: {'page': page}}))
2371    window.scrollTo({top: 0, behavior: 'smooth'})
2372  }
2373
2374  getUrl(searchParams) {
2375    const loc = document.location
2376    const loc_search = searchParams.toString();
2377    return loc.protocol + '//' + loc.hostname + loc.pathname + (loc_search ? '?' + loc_search : '')
2378  }
2379
2380  template() {
2381    const params = (new URL(document.location)).searchParams
2382    const originalParams = params
2383    params.delete('page')
2384    let html = ''
2385    if (this.framePages) {
2386      html += '<div class="paginator"><ul>'
2387
2388      if (this.showFirst) {
2389        html += '<li><a href="' + this.getUrl(params) + '" data-page="1">1</a></li>'
2390      }
2391
2392      if (this.showFirstDelimiter) {
2393        html += '<li class="empty"></li>'
2394      }
2395
2396      this.framePages.forEach(function(i) {
2397        if (i === this.pageNumber) {
2398          html += '<li><span class="is_active">' + i + '</span></li>'
2399        } else {
2400          params.delete('page')
2401          if (i !== 1) {
2402            params.append('page', i.toString())
2403          }
2404          html += '<li><a href="' + this.getUrl(params) + '" data-page="' + i + '">' + i + '</a></li>'
2405        }
2406      }.bind(this))
2407
2408      if (this.showLastDelimiter) {
2409        html += '<li class="empty"></li>'
2410      }
2411
2412      if (this.showLast) {
2413        params.delete('page')
2414        params.append('page', this.numberOfPages.toString())
2415        html += '<li><a href="' + this.getUrl(params) + '" data-page="' + this.numberOfPages + '">' + this.numberOfPages + '</a></li>'
2416      }
2417
2418      html += '</ul></div>'
2419    }
2420    return html
2421  }
2422
2423  notVisible() {
2424    return getComputedStyle(this.container).display == 'none'
2425  }
2426}
2427
2428class Catalog {
2429  constructor(language) {
2430    this.language = language
2431    this.catalog = document.querySelector('.catalog-list')
2432    this.catalogs = document.querySelectorAll('.catalog-list')
2433    this.url = null
2434    this.currentUrl = null
2435    this.show_all = true
2436    this.need_change_browser_url = true
2437    this.timestamp = 0
2438    this.filterInstance
2439    this.paginatorInstance
2440    this.toolsInstance
2441    this.buildFilter()
2442    this.buildPaginator()
2443    this.buildTools()
2444    this.listen()
2445    this.blockRequest = false
2446    this.useRefreshSpinner = true
2447    this.useWaitSpinner = false
2448    this.changeContent = true
2449    this.endCatalog = false
2450    
2451    return this
2452  }
2453
2454  listen() {
2455    document.addEventListener('catalog', function(e) {
2456      this.request(e.detail)
2457    }.bind(this))
2458
2459    document.addEventListener('catalog-view', function(e) {
2460      this.catalog.dataset.view = e.detail.view
2461
2462      this.catalog.querySelectorAll('.catalog-item--image > img').forEach(function(elem) {
2463        elem.src = e.detail.view == 'grid' ? elem.src.replace("w=139", "w=310") : elem.src.replace("w=310", "w=139")
2464      })
2465    }.bind(this))
2466
2467    this.catalog.addEventListener('swiped', function(event) {
2468      if (this.paginatorInstance.notVisible() && !this.endCatalog && !this.blockRequest && event.detail.dir == 'up') {
2469        const top = this.catalog.getBoundingClientRect().top
2470        const height = this.catalog.getBoundingClientRect().height
2471        const next = this.paginatorInstance.pageNumber + 1
2472        const delta = 1 - 0.5/this.paginatorInstance.pageNumber
2473        if (next <= this.paginatorInstance.numberOfPages && height > 0 && Math.abs(top/height) > delta) {
2474          this.useRefreshSpinner = false
2475          this.useWaitSpinner = true
2476          this.changeContent = false
2477          this.need_change_browser_url = false
2478          this.request({'page': next})
2479        }
2480      }
2481    }.bind(this))
2482
2483    window.addEventListener('popstate', function(event) {
2484      location.reload()
2485    })
2486  }
2487
2488  buildFilter() {
2489    this.filterInstance = new CatalogFilter()
2490    return this
2491  }
2492
2493  buildPaginator() {
2494    this.paginatorInstance = new CatalogPaginator()
2495    this.paginatorInstance.listen()
2496    return this
2497  }
2498
2499  buildTools() {
2500    this.toolsInstance = new CatalogTools()
2501    return this
2502  }
2503
2504  clear() {
2505    if (this.changeContent) {
2506      this.catalog.innerHTML = ''
2507      this.catalog.setAttribute('data-ga_slug', '')
2508    }
2509    return this
2510  }
2511
2512  setSlug(data) {
2513    if(data.catalog.length && data.ga_slug) {
2514      this.catalog.setAttribute('data-ga_slug', data.ga_slug)
2515    }
2516    return this
2517  }
2518
2519  fill(data, message) {
2520    for (let index = 0; index < data.length; index++) {
2521      const item = this.fillItem(data[index])
2522      this.catalog.insertAdjacentHTML('beforeend', item)
2523    }
2524    this.catalog.classList.remove('catalog-empty-list')
2525    if (data.length == 0) {
2526      this.endCatalog = true
2527      if (this.changeContent) {
2528        const item = this.fillEmpty(message)
2529        this.catalog.insertAdjacentHTML('beforeend', item)
2530        this.catalog.classList.add('catalog-empty-list')
2531      }
2532    }
2533
2534    this.changeContent = true
2535    this.need_change_browser_url = true
2536
2537    return this
2538  }
2539
2540  fillItem(data) {
2541    let html = ''
2542    html += '  <div class="column catalog-item product--item" data-id="' + data.id + '" data-name="' + data.name + '" data-sku="' + data.sku + '" data-vendor_name="' + data.vendor_name + '" data-price="' + data.raw_price + '" data-currency="' + data.currency_code + '">'
2543    html += '    <div class="catalog-item-container">'
2544    html += '      <div class="catalog-item--image' + (data.is_blured_cover ? ' blured':'') + '" data-required-age="' + data.required_age +'">'
2545    html += '        <a href="' + data.link + '" title="' + data.name + '"><img src="' + data.image + '" alt="' + data.name + '" loading="auto"></a>'
2546    html += '      </div>'
2547    html += '      <div class="catalog-item--description">'
2548    html += '        <div class="catalog-item--title"><a href="' + data.link + '" title="' + data.name + '">' + data.name + '</a></div>'
2549
2550    if (this.show_all && data.labels && 'discount' in data.labels) {
2551      html += '        <div class="catalog-item--product-label product--label-discount for-list">' + data.labels['discount'] + '</div>'
2552    }
2553
2554    if (this.show_all) {
2555      html += '        <div class="catalog-item--price">'
2556      html += '          <span>' + data.price + '</span>'
2557      if (data.discount_percent > 0) {
2558        html += '            <div class="catalog-item--full-price">' + data.baseprice + '</div>'
2559      }
2560      html += '        </div>'
2561    }
2562
2563    html += '      </div>'
2564
2565    if (this.show_all) {
2566      html += '      <div class="catalog-item--button">'
2567      html += '        <a class="catalog-item--button-product-link" href="' + data.link + '" title="' + data.name + '"></a>'
2568      html += '        <button id="favorite-button-' + data.id + '" aria-label="Add to favorites" data-id="' + data.id + '" type="button" class="catalog-item--button-add-favorite favorite--button' + (data.is_favorite ? ' is_active':'') + '"></button>'
2569      if (data.is_available && data.st === 2) {
2570        html += '<button data-id="' + data.id + '" type="button" class="catalog-item--button-add-cart cart--add-button">' + gettext('Add to сart') + '</button>'
2571      }
2572      else if (data.st === 3) {
2573        html += '<button data-id="' + data.id + '" type="button" class="catalog-item--button-add-cart cart--add-button">' + gettext('Coming soon') + '</button>'
2574      }
2575      else {
2576        html += '<button data-id="' + data.id + '" type="button" class="catalog-item--button-restock-alert not-hover">' + gettext('Out of stock') + '</button>'
2577      }
2578      html += '      </div>'
2579      html += '      <ul class="catalog-item--product-labels-v2 product--labels">'
2580      Object.keys(data.labels).forEach(function(key) {
2581        html += '        <li class="catalog-item--product-label-v2 product--label-' + key + '"><span>' + data.labels[key] + '</span></li>'
2582      })
2583      html += '      </ul>'
2584    }
2585    html += '    </div>'
2586    html += '  </div>'
2587
2588    return html
2589  }
2590
2591  fillEmpty(message) {
2592    return '<div class="column">' + message + '</div>'
2593  }
2594
2595  refreshPaginator(pageNumber, numberOfPages) {
2596    this.paginatorInstance.clear().refresh(pageNumber, numberOfPages)
2597    return this
2598  }
2599
2600  showTools() {
2601    this.toolsInstance.show()
2602    return this
2603  }
2604
2605  hideTools() {
2606    this.toolsInstance.hide()
2607    return this
2608  }
2609
2610  showRefreshSpinner() {
2611    if (this.useRefreshSpinner) {
2612      this.catalog.insertAdjacentHTML('afterbegin', '<div class="refresh"></div>')
2613    }
2614    if (this.useWaitSpinner) {
2615      this.catalog.insertAdjacentHTML('beforeend', '<div class="wait with-icon icon-waiting"></div>')
2616    }
2617  }
2618
2619  hideRefreshSpinner() {
2620    const refreshSpinnerElement = this.catalog.querySelector('.refresh')
2621    const waitSpinnerElement = this.catalog.querySelector('.wait')
2622    if (this.useRefreshSpinner && refreshSpinnerElement) {
2623      refreshSpinnerElement.remove()
2624    }
2625    if (this.useWaitSpinner && waitSpinnerElement) {
2626      waitSpinnerElement.remove()
2627    }
2628  }
2629
2630  setUrl(url) {
2631    this.url = url
2632    if (this.url.includes('my_games') || this.url.includes('my_discount_codes')) {
2633      this.setShowAll(false)
2634    }
2635  }
2636
2637  setShowAll(value) {
2638    this.show_all = value
2639  }
2640
2641  setNeedChangeBrowserUrl(value) {
2642    this.need_change_browser_url = value
2643  }
2644
2645  request(data={}) {
2646    this.blockRequest = true
2647
2648    this.showRefreshSpinner()
2649
2650    const formData = this.filterInstance.filters === null ? new FormData() : new FormData(this.filterInstance.filters.querySelector('form'))
2651    const currentUrlParams = (new URL(this.currentUrl === null ? window.location.toString().split(/[?#]/)[0] : this.currentUrl)).searchParams
2652    const params = (new URL('https://' + window.location.host)).searchParams
2653
2654    let url = this.url
2655    for (const pair of formData.entries()) {
2656      if (pair[0] === 'api') {
2657        this.url = pair[1]
2658      }
2659      else if (pair[1] !== undefined && pair[1] !== null && pair[1] !== '') {
2660        params.append(pair[0], pair[1].toString())
2661      }
2662    }
2663
2664    for (const key in data) {
2665      if (data[key] !== undefined) {
2666        params.append(key, data[key]);
2667      }
2668    }
2669
2670    if (data.sort === undefined && currentUrlParams.get('sort')) {
2671      params.set('sort', currentUrlParams.get('sort'))
2672    }
2673    if (data.per_page === undefined && currentUrlParams.get('per_page')) {
2674      params.set('per_page', currentUrlParams.get('per_page'))
2675    }
2676
2677    this.timestamp = Date.now()
2678    params.set('timestamp', this.timestamp)
2679
2680    if (this.need_change_browser_url) {
2681      params.set('need_change_browser_url', this.need_change_browser_url)
2682    }
2683
2684    if (params.get('page') && params.get('page') === '1') {
2685      params.delete('page')
2686    }
2687
2688    fetch(encodeURI(this.url + '?' + params.toString()), {
2689        method: 'GET',
2690        headers: {
2691          'X-CSRFToken': getCookie('csrftoken')
2692        }
2693      })
2694      .then(checkStatus)
2695      .then(parseJSON)
2696      .then(function(data) {
2697        if(data.timestamp && parseInt(data.timestamp) >= this.timestamp) {
2698          if (data.page_number <= data.number_of_pages) {
2699            this.clear().setSlug(data).fill(data.catalog, data.empty_text).refreshPaginator(data.page_number, data.number_of_pages)
2700
2701            /*
2702            if(data.catalog.length > 0) {
2703              this.showTools()
2704            } else {
2705              this.hideTools()
2706            }
2707            */
2708
2709            document.dispatchEvent(new CustomEvent('catalog-refresh', {}))
2710
2711            if (this.need_change_browser_url) {
2712              params.delete('timestamp')
2713              params.delete('need_change_browser_url')
2714              this.currentUrl = window.location.protocol + '//' + window.location.hostname + window.location.pathname + (params.toString() ? '?' + params.toString() : '')
2715              window.history.pushState({}, '', this.currentUrl)
2716            }
2717          }
2718          else {
2719            this.endCatalog = true
2720          }
2721        }
2722        this.hideRefreshSpinner()
2723
2724        this.blockRequest = false
2725        this.useRefreshSpinner = true
2726        this.useWaitSpinner = false
2727      }.bind(this))
2728      .catch(function(message) {
2729        this.hideRefreshSpinner()
2730        this.blockRequest = false
2731        this.useRefreshSpinner = true
2732        this.useWaitSpinner = false
2733      }.bind(this))
2734
2735    if (this.changeContent) {
2736      window.scrollTo({top: 0, behavior: 'smooth'})
2737    }
2738  }
2739}
2740
2741class FlatCatalog {
2742  constructor(params) {
2743    this.id = params.id;
2744
2745    this.states = {
2746      'hidden': 'hidden',
2747      'visible': 'visible'
2748    }
2749
2750    this.stateTitles = {
2751      'hidden': gettext("Show more"),
2752      'visible': gettext("Show less")
2753    }
2754
2755    this.catalogElement = null;
2756    this.showCatalogElement = null;
2757    this.run();
2758  }
2759
2760  run() {
2761    this.catalogElement = document.querySelector('#' + this.id);
2762    if (this.catalogElement) {
2763      this.showCatalogElement = this.catalogElement.parentElement.querySelector('[data-type="show-catalog"]');
2764      if (this.showCatalogElement) {
2765        if (this.checkWidthViewportForShowButton()) {
2766          this.listen();
2767        } else {
2768          this.showCatalogElement.remove();
2769        }
2770      }
2771    }
2772  }
2773
2774  listen() {
2775    this.showCatalogElement.addEventListener('click', (event) => {
2776      this.showCatalogHandler(event.target.dataset.state);
2777    });
2778  }
2779
2780  showCatalogHandler(state) {
2781    if (state === this.states.hidden) {
2782      this.showCatalogElement.dataset.state = this.states.visible;
2783      this.showCatalogElement.innerHTML = this.stateTitles.visible;
2784      this.catalogElement.classList.add('is_open');
2785    }
2786    else if (state === this.states.visible) {
2787      this.showCatalogElement.dataset.state = this.states.hidden;
2788      this.showCatalogElement.innerHTML = this.stateTitles.hidden;
2789      this.catalogElement.classList.remove('is_open');
2790      this.catalogElement.parentElement.scrollIntoView({block: "center", behavior: "smooth"});
2791    }
2792    this.showCatalogElement.blur();
2793    this.catalogElement.blur();
2794  }
2795
2796  checkWidthViewportForShowButton() {
2797    const width = window.innerWidth;
2798    const catalogLength = this.catalogElement.querySelectorAll('.catalog-item').length;
2799
2800    return (width >= 1281 && catalogLength > 3) || (width >= 768 && width < 1281 && catalogLength > 2) || (width < 768 && catalogLength > 1);
2801  }
2802}
2803
2804class Select {
2805  constructor(selectSelector=null) {
2806    this.selectSelector = selectSelector ?? '.select';
2807    this.toggleList();
2808    this.listenItem();
2809  }
2810
2811  toggleList() {
2812    document.querySelectorAll(this.selectSelector + ' > span').forEach((elem) => {
2813      elem.addEventListener('click', (event) => {
2814        event.preventDefault()
2815        const selectElement = elem.parentNode
2816        if (selectElement.classList && selectElement.classList.contains('is_active')) {
2817          selectElement.classList.remove('is_active')
2818        } else {
2819          selectElement.classList.add('is_active')
2820          setTimeout(this.closeEvent.bind(null, selectElement), 500)
2821        }
2822      })
2823    })
2824  }
2825
2826  listenItem() {
2827    document.querySelectorAll(this.selectSelector + ' > ul > li > a').forEach((elem) => {
2828      elem.addEventListener('click', (event) => {
2829        event.preventDefault()
2830        const value = elem.dataset.value
2831        const selectElement = elem.parentNode.parentNode.parentNode
2832        const displayElement = selectElement.querySelector('span')
2833        const inputElement = selectElement.querySelector('input[type="hidden"]')
2834        selectElement.querySelectorAll('ul > li').forEach(function(elem) {
2835          const elemValue = elem.querySelector('a').dataset.value
2836          elemValue === value ? elem.classList.add('selected') : elem.classList.remove('selected')
2837        })
2838
2839        if (displayElement && displayElement.innerHTML.length > 0) {
2840          displayElement.innerHTML = elem.innerHTML
2841        }
2842        if (inputElement) {
2843          inputElement.value = value
2844        }
2845        selectElement.classList.remove('is_active')
2846      })
2847    })
2848  }
2849
2850  closeEvent(element) {
2851    const isVisible = function(elem) {
2852      return !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length )
2853    }
2854
2855    const removeNode = function() {
2856      removeClickListener()
2857      element.classList.remove('is_active')
2858    }
2859
2860    const outsideClickListener = function(event) {
2861      if (!element.contains(event.target) && isVisible(element)) {
2862        removeNode()
2863      }
2864    }
2865
2866    const removeClickListener = function() {
2867      document.removeEventListener('click', outsideClickListener)
2868    }
2869
2870    document.addEventListener('click', outsideClickListener)
2871  }
2872}
2873
2874class AccountSettings extends BaseForm {
2875  constructor(args) {
2876    super()
2877
2878    this.apiUrl = args.apiUrl
2879
2880    this.accountAvatarForm = document.querySelector('.account-avatar-form')
2881    this.accountUsernameForm = document.querySelector('.account-username-form')
2882    this.accountEmailForm = document.querySelector('.account-email-form')
2883    this.accountPasswordForm = document.querySelector('.account-password-form')
2884    this.accountLanguageForm = document.querySelector('.account-language-form')
2885
2886    this.reset()
2887    this.listen()
2888  }
2889
2890  listen() {
2891    this.accountAvatarForm.querySelector('#show-avatars').addEventListener('click', this.showAvatarsHandler.bind(this))
2892    this.accountUsernameForm.querySelector('button[type="submit"]').addEventListener('click', this.usernameChangeHandler.bind(this))
2893    this.accountEmailForm.querySelector('button[type="submit"]').addEventListener('click', this.emailChangeHandler.bind(this))
2894    this.accountPasswordForm.querySelector('button[type="submit"]').addEventListener('click', this.passwordChangeHandler.bind(this))
2895    if (this.accountLanguageForm) {
2896      this.accountLanguageForm.querySelector('button[type="submit"]').addEventListener('click', this.languageChangeHandler.bind(this));
2897    }
2898  }
2899
2900  showAvatarsHandler(event) {
2901    fetch(encodeURI(this.apiUrl), {
2902        method: 'GET',
2903        headers: {
2904          'X-CSRFToken': getCookie('csrftoken')
2905        }
2906      })
2907      .then(checkStatus)
2908      .then(parseJSON)
2909      .then((data) => {
2910        this.showAvatarsPopUp(data);
2911      })
2912      .catch((message) => {});
2913  }
2914
2915  showAvatarsPopUp(data) {
2916    const popup = new PopUp();
2917
2918    popup.showAvatars(data);
2919
2920    document.querySelector('#' + popup.id).querySelectorAll('[data-action="change-avatar"]').forEach((avatar) => {
2921      const hash = avatar.dataset.hash;
2922      const src = avatar.dataset.src;
2923      avatar.addEventListener('click', (event) => {
2924        const formData  = new FormData();
2925        formData.append('action', 'change-avatar');
2926        formData.append('hash', hash);
2927
2928        fetch(this.accountAvatarForm.action, {
2929            method: 'POST',
2930            headers: {
2931              'X-CSRFToken': getCookie('csrftoken')
2932            },
2933            body: formData
2934          })
2935          .then(checkStatus)
2936          .then(parseJSON)
2937          .then((data) => {
2938            if (data.success) {
2939              return data;
2940            }
2941          })
2942          .then((data) => {
2943            document.querySelector('.navigation-link--icon-user').style.cssText = '--avatar-url: url("' + src + '")';
2944            document.querySelectorAll('[data-avatar="true"]').forEach((elem) => {
2945              elem.src = src;
2946            });
2947            popup.close();
2948          })
2949          .catch((message) => {});
2950      });
2951    });
2952  }
2953
2954  usernameChangeHandler(event) {
2955    event.preventDefault()
2956
2957    this.reset()
2958    this.form = this.accountUsernameForm
2959
2960    if (this.validUsername() && this.validCharUsername()) {
2961      this.accountUsernameForm.submit()
2962    }
2963  }
2964
2965  emailChangeHandler(event) {
2966    event.preventDefault()
2967
2968    this.reset()
2969    this.form = this.accountEmailForm
2970
2971    const validItems = []
2972    validItems.push(this.validEmail())
2973    validItems.push(this.validEmailRepeat())
2974
2975    const is_valid = validItems.reduce(function(res, item) {
2976      return res && item
2977    })
2978
2979    if (is_valid) {
2980      this.accountEmailForm.submit()
2981    }
2982  }
2983
2984  passwordChangeHandler(event) {
2985    event.preventDefault()
2986
2987    this.reset()
2988    this.form = this.accountPasswordForm
2989
2990    const validItems = []
2991    validItems.push(this.validPassword())
2992    validItems.push(this.validPasswordRepeat())
2993
2994    const is_valid = validItems.reduce(function(res, item) {
2995      return res && item
2996    })
2997
2998    if (is_valid) {
2999      this.accountPasswordForm.submit()
3000    }
3001  }
3002
3003  languageChangeHandler(event) {
3004    event.preventDefault();
3005    this.reset();
3006    this.form = this.accountLanguageForm;
3007    this.accountLanguageForm.submit();
3008  } 
3009
3010  validUsername() {
3011    const usernameElement = this.accountUsernameForm.querySelector('input[name="username"]')
3012    const is_valid = validatorEmpty(usernameElement.value.trim()) && usernameElement.value.trim().length <= 30
3013    this.form_error(usernameElement, is_valid ? '' : gettext('Username must not be empty or longer than 30 characters'))
3014    return is_valid
3015  }
3016
3017  validCharUsername() {
3018    const usernameElement = this.accountUsernameForm.querySelector('input[name="username"]')
3019    const is_valid = usernameElement.value.trim().match(/^[\p{L}\p{N}_]+$/u)
3020    this.form_error(usernameElement, is_valid ? '' : gettext('Username should contain only letters, numbers and underscore (the "_" symbol)'))
3021    return is_valid
3022  }
3023
3024  validEmail() {
3025    const emailElement = this.accountEmailForm.querySelector('input[name="new_email"]')
3026    const is_valid = validatorEmail(emailElement.value)
3027
3028    this.form_error(emailElement, is_valid ? '' : gettext('Incorrect email'))
3029    return is_valid
3030  }
3031
3032  validEmailRepeat() {
3033    const emailElement = this.accountEmailForm.querySelector('input[name="new_email"]')
3034    const emailRepeatElement = this.accountEmailForm.querySelector('input[name="new_email_repeat"]')
3035    const is_valid = validatorRepeat(emailElement.value, emailRepeatElement.value)
3036
3037    this.form_error(emailRepeatElement, is_valid ? '' : gettext('The emails you\'ve entered do n
3037ot match'))
3038    return is_valid
3039  }
3040
3041  validPassword() {
3042    const passwordElement = this.accountPasswordForm.querySelector('input[name="new_password"]')
3043    const is_valid_length = validatorPasswordLength(passwordElement.value)
3044    const is_valid_char = validatorPasswordChar(passwordElement.value)
3045    let text = is_valid_length ? '' : gettext('Password must contain at least 8 characters')
3046
3047    this.form_error(passwordElement, text)
3048    this.form_error(passwordElement, is_valid_char ? text : text + '<br>' + gettext('Include a-z, A-Z, 0-9 or symbols for a stronger password'))
3049    return is_valid_length && is_valid_char
3050  }
3051
3052  validPasswordRepeat() {
3053    const passwordElement = this.accountPasswordForm.querySelector('input[name="new_password"]')
3054    const passwordRepeatElement = this.accountPasswordForm.querySelector('input[name="new_password_repeat"]')
3055    const is_valid = validatorRepeat(passwordElement.value, passwordRepeatElement.value)
3056
3057    this.form_error(passwordRepeatElement, is_valid ? '' : gettext('The passwords you\'ve entered do not match'))
3058    return is_valid
3059  }
3060}
3061
3062class AccountDeletion extends BaseForm {
3063  constructor() {
3064    super()
3065    this.form = document.querySelector('.account-delete-form')
3066    this.reset()
3067    this.listen()
3068  }
3069
3070  listen() {
3071    this.form.querySelector('button[type="submit"]').addEventListener('click', this.handler.bind(this))
3072  }
3073
3074  handler(event) {
3075    event.preventDefault()
3076
3077    this.reset()
3078
3079    const validItems = []
3080    this.form.querySelectorAll('input[type="text"]').forEach(function(elem) {
3081      validItems.push(this.validTextInput(elem))
3082    }.bind(this))
3083    validItems.push(this.validAgree())
3084
3085    const is_valid = validItems.reduce(function(res, item) {
3086      return res && item
3087    })
3088
3089    if (is_valid) {
3090      this.form.submit()
3091    }
3092  }
3093
3094  validTextInput(element) {
3095    const is_valid = validatorEmpty(element.value)
3096    this.error(element, is_valid ? '': gettext('Required'))
3097    return is_valid
3098  }
3099
3100  validAgree() {
3101    const element = this.form.querySelector('input[name="agree"]')
3102    const is_valid = element.checked
3103    this.form_error(element, is_valid ? '' : gettext('Please agree to the conditions'))
3104    return is_valid
3105  }
3106}
3107
3108class AccountGDPR extends BaseForm {
3109  constructor() {
3110    super()
3111    this.form = document.querySelector('.account-gdpr-form')
3112    this.listen()
3113  }
3114
3115  listen() {
3116    this.form.querySelector('button[type="submit"]').addEventListener('click', this.handler.bind(this))
3117  }
3118
3119  handler(event) {
3120    event.preventDefault()
3121    const element = this.form.querySelector('input[name="password"]')
3122    const is_valid = validatorEmpty(element.value)
3123    this.error(element, is_valid ? '': gettext('Required'))
3124
3125    if (is_valid) {
3126      this.form.submit()
3127    }
3128  }
3129}
3130
3131class Product {
3132  constructor(is_auth) {
3133    this.is_auth = is_auth
3134    this.systemRequirementsContainerElement = document.querySelector('.product-system-requirements')
3135    this.listen()
3136  }
3137
3138  listen() {
3139    const regionsButtonElement = document.querySelector('#regions-button')
3140    if (regionsButtonElement) {
3141      regionsButtonElement.addEventListener('click', this.regionsHandler)
3142    }
3143    const systemAlertForm = document.querySelector('#system-alert-form');
3144    if (systemAlertForm) {
3145      const systemAlertCheckBox = systemAlertForm.querySelector('#id_subscribe');
3146      if (systemAlertCheckBox) {
3147        systemAlertCheckBox.addEventListener('change', (event) => {
3148          this.systemAlertHandler(new FormData(systemAlertForm))
3149        });
3150      }
3151    }
3152    if (this.systemRequirementsContainerElement != null) {
3153      this.systemRequirementsContainerElement.querySelectorAll('.product-system-requirements--item').forEach(function(elem) {
3154        elem.addEventListener('click', function(event) {
3155          const id = event.target.dataset.id
3156          this.systemRequirementsContainerElement.querySelectorAll('.product-system-requirements--item').forEach(function(elem) {
3157            elem.classList.remove('is_active')
3158          })
3159          this.systemRequirementsContainerElement.querySelectorAll('.product-system-requirements--content').forEach(function(elem) {
3160            elem.classList.remove('is_active')
3161          })
3162          event.target.classList.add('is_active')
3163          this.systemRequirementsContainerElement.querySelector('.product-system-requirements--content[data-target="' + id + '"]').classList.add('is_active')
3164        }.bind(this))
3165      }.bind(this))
3166    }
3167    const product_section = document.querySelector('.page.catalog.product')
3168    if (product_section) {
3169      document.dispatchEvent(new CustomEvent('product_shown', {detail: {data: product_section.dataset}}))
3170    }
3171  }
3172
3173  regionsHandler(event) {
3174    event.preventDefault()
3175    const newTitle = event.target.dataset.title
3176    event.target.dataset.title = event.target.innerHTML
3177    event.target.innerHTML = newTitle
3178    const descriptionElement = document.querySelector('#regions-description')
3179    if (event.target.classList.contains('is_active')) {
3180      event.target.classList.remove('is_active')
3181      descriptionElement.style.display = 'none'
3182    } else {
3183      event.target.classList.add('is_active')
3184      descriptionElement.style.display = 'block'
3185    }
3186  }
3187
3188  systemAlertHandler(data) {
3189    fetch(window.location.pathname.toString(), {
3190        method: 'POST',
3191        headers: {
3192          'X-CSRFToken': getCookie('csrftoken')
3193        },
3194        body: data
3195      })
3196      .then(checkStatus)
3197      .then(parseJSON)
3198      .then((data) => {
3199        if (data.hasOwnProperty('success') && data.success && data.message) {
3200          new PopUp().showSystemAlert(data.message, data.signin_required)
3201        }
3202        else if (data.hasOwnProperty('blocked') && data.blocked) {
3203          new PopUp().showBlockedSystemAlert(data.popup)
3204        }
3205      })
3206      .catch((error) => {
3207        new PopUp().showErrorPopUp(gettext('An error occurred, try again later'))
3208      })
3209  }
3210}
3211
3212class CouponForm {
3213  constructor() {
3214    this.form = document.querySelector('#cart-coupon-form')
3215    this.listen()
3216  }
3217
3218  listen() {
3219    const el = this.form ? this.form.querySelector('input[type="button"]'):null
3220    if (el) {
3221      el.addEventListener('click', this.handler.bind(this))
3222    }
3223  }
3224
3225  handler(event) {
3226    event.preventDefault()
3227    const action = event.target.dataset.action
3228    const inputElem = this.form.querySelector('input[type="text"]')
3229    if (inputElem.value.trim() !== '') {
3230      if (action === 'cancel') {
3231        inputElem.value = ' '
3232      }
3233      this.form.submit()
3234    }
3235  }
3236}
3237
3238class CheckoutForm extends BaseForm {
3239  constructor(is_auth, proceed, apiUrl) {
3240    super()
3241    this.form = document.querySelector('#cart-checkout-form')
3242    this.is_auth = is_auth
3243    this.apiUrl = apiUrl
3244    this.proceed = proceed
3245    if (this.form !== null) {
3246      this.paymentMethodListElem = document.querySelector('.cart-payment-list')
3247      this.paymentMethodElem = this.form.querySelector('#id_payment')
3248      this.phoneElems = this.form.querySelectorAll('#id_phone_container input');
3249      this.agreeElem = this.form.querySelector('#id_agree');
3250      this.withdrawalRightAgreeElem = this.form.querySelector('#id_withdrawal_right_agree');
3251      if (!this.is_auth) {
3252        this.recaptchaError = true
3253        this.emailElem = this.form.querySelector('#id_email')
3254        this.emailRepeatElem = this.form.querySelector('#id_email_repeat')
3255        this.recaptchaElem = this.form.querySelector('#id_recaptcha')
3256        this.changeEmailLinkElem = this.form.querySelector('#change_email_link')
3257      }
3258      this.render()
3259      this.listen()
3260    }
3261  }
3262
3263  render() {
3264    const BreakException = {}
3265    if (this.paymentMethodListElem.querySelectorAll('.cart-payment-item.is_active').length == 0) {
3266      try {
3267        this.paymentMethodListElem.querySelectorAll('.cart-payment-item').forEach(function(elem) {
3268          this.paymentMethodElem.value = elem.dataset.value
3269          elem.classList.add('is_active');
3270          this.setActivePaymentMethod();
3271          throw BreakException
3272        }.bind(this))
3273      } catch (e) {
3274        if (e !== BreakException) throw e
3275      }
3276    }
3277  }
3278
3279  listen() {
3280    this.paymentMethodListElem.querySelectorAll('.cart-payment-item').forEach(function(elem) {
3281      elem.addEventListener('click', function(event) {
3282        this.paymentMethodElem.value = event.target.dataset.value
3283        this.paymentMethodListElem.querySelectorAll('.cart-payment-item.is_active').forEach(function(elem) {
3284          elem.classList.remove('is_active')
3285        })
3286        event.target.classList.add('is_active')
3287        this.form_error(this.paymentMethodElem, '')
3288        this.setActivePaymentMethod();
3289      }.bind(this))
3290    }.bind(this))
3291
3292    this.form.querySelector('[type="submit"]').addEventListener('click', function(event) {
3293      event.preventDefault()
3294      if (this.valid()) {
3295        try {
3296          this.form.insertAdjacentHTML('beforeend', '<input type="hidden" name="ga_client_id" value="' + window.gClientId + '">')
3297        } catch {}
3298        this.form.submit()
3299      }
3300    }.bind(this))
3301
3302    if (!this.is_auth) {
3303      if (this.emailElem !== null) {
3304        this.emailElem.addEventListener('input', function(event) {
3305          this.validEmail()
3306        }.bind(this))
3307      }
3308
3309      if (this.emailRepeatElem !== null) {
3310        this.emailRepeatElem.addEventListener('input', function(event) {
3311          this.validEmailRepeat()
3312        }.bind(this))
3313      }
3314
3315      if (this.emailRepeatElem !== null) {
3316        this.emailRepeatElem.addEventListener('paste', function(event) {
3317          event.preventDefault()
3318          this.noCopyEmailRepeat()
3319        }.bind(this))
3320      }
3321
3322      if (this.agreeElem !== null) {
3323        this.agreeElem.addEventListener('change', function(event) {
3324          this.validAgree()
3325        }.bind(this))
3326      }
3327
3328      if (this.withdrawalRightAgreeElem !== null) {
3329        this.withdrawalRightAgreeElem.addEventListener('change', function(event) {
3330          this.validWithdrawalRightAgree()
3331        }.bind(this))
3332      }
3333
3334      if (this.changeEmailLinkElem !== null) {
3335        this.changeEmailLinkElem.addEventListener('click', function(event) {
3336          event.preventDefault()
3337          this.form.querySelector('[name="action"]').value = 'change_email'
3338          this.form.submit()
3339        }.bind(this))
3340      }
3341    }
3342  }
3343
3344  setActivePaymentMethod() {
3345    const formData = new FormData()
3346    formData.append('payment_method', this.paymentMethodElem.value)
3347    formData.append('action', 'SetPaymentMethod')
3348    fetch(encodeURI(this.apiUrl), {
3349      method: 'POST',
3350      headers: {
3351        'X-CSRFToken': getCookie('csrftoken')
3352      },
3353      body: formData
3354    })
3355    .then(checkStatus)
3356    .then(parseJSON)
3357    .then((data) => {
3358      if (
3359        data.hasOwnProperty('id') && data.hasOwnProperty('is_phone_req') && data.hasOwnProperty('success') && 
3360        data.success && data.id.toString() === this.paymentMethodElem.value
3361      ) {
3362        const phoneFieldLabel = document.querySelector('#id_phone_label');
3363        const phoneFieldContainer = document.querySelector('#id_phone_container');
3364        if (phoneFieldLabel && phoneFieldContainer) {
3365          if (data.is_phone_req) {
3366            phoneFieldLabel.classList.remove('hidden');
3367            phoneFieldContainer.classList.remove('hidden');
3368            phoneFieldContainer.querySelectorAll('input').forEach((element) => {
3369              element.setAttribute("required", "");
3370            });
3371          } else if (!phoneFieldLabel.classList.contains('hidden')) {
3372            phoneFieldLabel.classList.add('hidden');
3373            phoneFieldContainer.classList.add('hidden');
3374            phoneFieldContainer.querySelectorAll('input').forEach((element, index) => {
3375              element.removeAttribute("required");
3376              if (index === 1) {
3377                element.value = '';
3378              }
3379            });            
3380          }
3381        }
3382      }
3383    });
3384  }
3385
3386  valid() {
3387    if (this.paymentMethodListElem.querySelector('.cart-payment-item.is_active') === null) {
3388      this.form_error(this.paymentMethodElem, gettext('Unfortunately there are no payment options available in your location at the moment. Please, check back later. We apologize for the inconvenience.'))
3389      return false
3390    }
3391
3392    let validation_list = [this.validPaymentMethod()]
3393    if (this.agreeElem !== null) {
3394      validation_list.push(this.validAgree())
3395    }
3396    if (this.withdrawalRightAgreeElem !== null) {
3397      validation_list.push(this.validWithdrawalRightAgree())
3398    }
3399    if (this.phoneElems && this.phoneElems.length) {
3400      validation_list.push(this.validPhone());
3401    }
3402    if (!this.is_auth) {
3403      validation_list = validation_list.concat([this.validPaymentMethod()])
3404      if (this.proceed) {
3405        validation_list = validation_list.concat([this.validEmail(), this.validEmailRepeat(), this.validCaptcha()])
3406      }
3407    }
3408    return validation_list.reduce(function(res, item) {
3409      return res && item
3410    })
3411  }
3412
3413  validPaymentMethod() {
3414    const is_valid = this.paymentMethodElem.value.trim().length != ''
3415    this.form_error(this.paymentMethodElem, is_valid ? '' : gettext('Choose a payment method'))
3416    return is_valid
3417  }
3418
3419  validPhone() {
3420    let is_valid = true;
3421    this.phoneElems.forEach((element) => {is_valid = is_valid && element.validity.valid});
3422    this.form_error(this.phoneElems[0], is_valid ? '' : gettext('Type your valid phone number'))
3423    return is_valid
3424  }
3425
3426  validEmail() {
3427    const is_valid = validatorEmail(this.emailElem.value)
3428    this.form_error(this.emailElem, is_valid ? '' : gettext('Type your email'))
3429    return is_valid
3430  }
3431
3432  validEmailRepeat() {
3433    const is_valid = validatorRepeat(this.emailElem.value, this.emailRepeatElem.value)
3434    this.form_error(this.emailRepeatElem, is_valid ? '' : gettext('The emails you\'ve entered should match'))
3435    return is_valid
3436  }
3437
3438  noCopyEmailRepeat() {
3439      this.form_error(this.emailRepeatElem, gettext('The confirmation email must be entered manually'))
3440  }
3441
3442  validCaptcha() {
3443    this.form_error(this.recaptchaElem, this.recaptchaError ? gettext('Incorrect CAPTCHA') : '')
3444    return !this.recaptchaError
3445  }
3446
3447  verifyRecaptchaCallback(response) {
3448    this.recaptchaError = (response === undefined || response === null || response === "") ? true : false
3449    this.validCaptcha()
3450  }
3451
3452  validAgree() {
3453    this.form_error(this.agreeElem.parentNode, this.agreeElem.checked ? '' : gettext('Please agree to the terms and conditions.'))
3454    return this.agreeElem.checked
3455  }
3456
3457  validWithdrawalRightAgree() {
3458    this.form_error(this.withdrawalRightAgreeElem.parentNode, this.withdrawalRightAgreeElem.checked ? '' : gettext('Please confirm the digital content withdrawal consent before proceeding to checkout.'))
3459    return this.withdrawalRightAgreeElem.checked
3460  }
3461}
3462
3463class Order {
3464  constructor(code, apiUrl) {
3465    this.code = code
3466    this.delay = 20
3467    this.timer = null
3468    this.apiUrl = apiUrl
3469    this.completed_order_viewed_block = false
3470
3471    if (this.code !== null) {
3472      this.runTimer()
3473    }
3474  }
3475
3476  runTimer() {
3477    this.timer = setTimeout(this.timerHandler.bind(this), this.delay*1000)
3478  }
3479
3480  timerHandler() {
3481    fetch(encodeURI(this.apiUrl + '?code=' + this.code), {
3482        method: 'GET',
3483        headers: {
3484          'X-CSRFToken': getCookie('csrftoken')
3485        }
3486      })
3487      .then(checkStatus)
3488      .then(parseJSON)
3489      .then(function(data) {
3490        if ('code' in data && data.code != this.code.toString()) {
3491          location.reload()
3492        }
3493        this.resetTimer()
3494      }.bind(this))
3495      .catch(function() {
3496        this.resetTimer()
3497      }.bind(this))
3498  }
3499
3500  completed_order_conversion() {
3501    if (!this.completed_order_viewed_block) {
3502      const formData = new FormData()
3503      formData.append('completed_order_viewed', 'yes')
3504      this.completed_order_viewed_block = true
3505      fetch(encodeURI(this.apiUrl), {
3506          method: 'POST',
3507          headers: {
3508            'X-CSRFToken': getCookie('csrftoken')
3509          },
3510          body: formData
3511        })
3512        .then(checkStatus)
3513        .catch(function() {})
3514    }
3515  }
3516
3517  resetTimer() {
3518    clearTimeout(this.timer)
3519    this.runTimer()
3520  }
3521}
3522
3523class Support {
3524  constructor(language) {
3525    this.language = language
3526    this.searchElement = document.querySelector('.support--search')
3527    this.searchUrl = '/api/support/search/'
3528    this.searchTimestamp = 0
3529    this.searchData = null
3530    this.listen()
3531    return this
3532  }
3533
3534  listen() {
3535    if (this.searchElement) {
3536      this.searchElement.querySelector('input[type="search"]').addEventListener('input', this.searchHandler.bind(this))
3537      this.searchElement.querySelector('input[type="search"]').addEventListener("keypress", function(event) {
3538        if (event.keyIdentifier=='U+000A' || event.keyIdentifier=='Enter' || event.keyCode==13) {
3539          this.showFullSearchResult(event)
3540        }
3541      }.bind(this))
3542      this.searchElement.querySelector('input[type="button"]').addEventListener('click', this.showFullSearchResult.bind(this))
3543    }
3544  }
3545
3546  listenShowFullSearchResultButton() {
3547    this.searchElement.querySelector('.support--search-show-all-button').addEventListener('click', this.showFullSearchResult.bind(this))
3548  }
3549
3550  listenLink() {
3551    document.querySelectorAll('.support--search-item').forEach(function(elem) {
3552      elem.addEventListener('click', function() {
3553        top.location.href = this.dataset.href
3554      })
3555    })
3556  }
3557
3558  searchHandler(event) {
3559    const value = event.target.value
3560    if (value.length >= 3) {
3561      this.remove()
3562      this.searchData = null
3563      this.searchTimestamp = Date.now()
3564      fetch(encodeURI(get_url_with_locale(this.searchUrl, this.language) + value + '?timestamp=' + this.searchTimestamp), {
3565          method: 'GET',
3566          headers: {
3567            'X-CSRFToken': getCookie('csrftoken')
3568          }
3569        })
3570        .then(checkStatus)
3571        .then(parseJSON)
3572        .then(function(data) {
3573          if(data.timestamp && parseInt(data.timestamp) < this.searchTimestamp) {
3574            throw 'expired data'
3575          }
3576          this.searchData = data
3577          return data
3578        }.bind(this))
3579        .then(this.previewSearchResult.bind(this))
3580        .catch(function(e) {
3581          this.searchData = null
3582        }.bind(this))
3583    }
3584
3585    if (value.length == 0) {
3586      this.remove()
3587      this.searchData = null
3588    }
3589  }
3590
3591  previewSearchResult(data) {
3592    if (data.success) {
3593      this.searchData = data
3594
3595      let html = ''
3596
3597      html += '<div class="support--search-result">'
3598      html += '  <div class="support--search-header">'
3599      html += '    <div>' + gettext('Search results') + '</div>'
3600      html += '    <div><span class="support--search-total">
3600' + data.total + '</span>' + ngettext('result', 'results', data.total) + '</div>'
3601      html += '  </div>'
3602      if (data.total > 0) {
3603        html += '  <div class="support--search-body">'
3604        for (let index = 0; index < data.items.length && index < 5; index++) {
3605          html += '    <div class="support--search-item"><a href="' + data.items[index].link + '">' + data.items[index].title + '</a></div>'
3606        }
3607        html += '  </div>'
3608        html += '  <div class="support--search-footer">'
3609        html += '    <a href="#" class="support--search-show-all-button">' + gettext('Show all results') + '</a>'
3610        html += '  </div>'
3611      }
3612      html += '</div>'
3613
3614      this.remove()
3615      this.searchElement.insertAdjacentHTML('beforeend', html)
3616      if (data.total > 0) {
3617        this.listenShowFullSearchResultButton()
3618      }
3619      setTimeout(this.closeEventForSearch.bind(this), 500)
3620    }
3621  }
3622
3623  showFullSearchResult(event) {
3624    event.preventDefault()
3625    this.remove()
3626    if (this.searchData !== null) {
3627      const data = this.searchData
3628      let html = ''
3629
3630      html += '<div class="support--search-result full">'
3631      html += '  <div class="support--search-header">'
3632      html += '    <div>' + gettext('Search results for') + ' "' + escapeHtml(data.query) + '"</div>'
3633      html += '    <div><span class="support--search-total">
3633' + data.total + '</span>' + ngettext('result', 'results', data.total) + '</div>'
3634      html += '  </div>'
3635      if (data.total > 0) {
3636        html += '  <div class="support--search-body">'
3637        for (let index = 0; index < data.items.length; index++) {
3638          html += '    <div data-href="' + data.items[index].link + '" class="support--search-item">'
3639          html += '      <a>' + data.items[index].title + '</a>'
3640          html += '      <div class="support--search-item--description">' + data.items[index].desc + '</div>'
3641          html += '    </div>'
3642        }
3643        html += '  </div>'
3644      }
3645      html += '</div>'
3646
3647      document.querySelector('.support--blocks').insertAdjacentHTML('afterend', html)
3648      document.querySelector('.support--blocks').style.display = 'none'
3649
3650      this.listenLink()
3651    }
3652  }
3653
3654  remove() {
3655    this.removeSearchResult()
3656    this.removeSearchFullResult()
3657  }
3658
3659  removeSearchResult() {
3660    const node = this.searchElement.querySelector('.support--search-result')
3661    if (node && node.parentNode) {
3662      node.parentNode.removeChild(node)
3663    }
3664  }
3665
3666  removeSearchFullResult() {
3667    const node = document.querySelector('.support--search-result.full')
3668    if (node && node.parentNode) {
3669      node.parentNode.removeChild(node)
3670      document.querySelector('.support--blocks').style.display = 'flex'
3671    }
3672  }
3673
3674  closeEventForSearch() {
3675    const element = this.searchElement
3676
3677    const isVisible = function(elem) {
3678      return !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length )
3679    }
3680
3681    const removeNode = function() {
3682      removeClickListener()
3683      this.removeSearchResult()
3684    }.bind(this)
3685
3686    const outsideClickListener = function(event) {
3687      if (!element.contains(event.target) && isVisible(element)) {
3688        removeNode()
3689      }
3690    }
3691
3692    const removeClickListener = function() {
3693      document.removeEventListener('click', outsideClickListener)
3694    }
3695
3696    document.addEventListener('click', outsideClickListener)
3697  }
3698}
3699
3700class SubscribeForm extends BaseForm {
3701  constructor(language) {
3702    super()
3703    this.language = language
3704    this.form = document.querySelector('.subscribe-form')
3705    if (this.form) {
3706      this.url = '/api/subscription/subscribe/'
3707      this.defaultErrorView = this.form.dataset && this.form.dataset.default_error
3708      this.defaultSuccessView = this.form.dataset && this.form.dataset.default_success
3709      this.searchTimestamp = 0
3710      this.listen()
3711    }
3712  }
3713
3714  listen() {
3715    if (this.form) {
3716      this.form.querySelector('input[name="email"]').addEventListener("input", function(event) {
3717        this.error(event.target, '')
3718      }.bind(this))
3719      this.form.querySelector('input[name="email"]').addEventListener("keypress", function(event) {
3720        if (event.keyIdentifier=='U+000A' || event.keyIdentifier=='Enter' || event.keyCode==13) {
3721          this.handler(event)
3722        }
3723      }.bind(this))
3724      this.form.querySelector('.subscribe-button').addEventListener('click', this.handler.bind(this))
3725    }
3726  }
3727
3728  handler(event) {
3729    event.preventDefault()
3730    this.removeSubscribeMessage()
3731    this.reset()
3732
3733    const error_messages = []
3734
3735    const element = this.form.querySelector('input[name="email"]')
3736    const is_valid_email = validatorEmail(element.value) && validatorEmpty(element.value)
3737    if (this.defaultErrorView) {
3738      this.error(element, is_valid_email ? '' : gettext('Incorrect email'))
3739    }
3740    else if (!is_valid_email) {
3741      error_messages.push(gettext('Incorrect email'))
3742    }
3743
3744    const elementAgree = this.form.querySelector('input[type="checkbox"]')
3745    const is_valid_agree = elementAgree.checked
3746    if (this.defaultErrorView) {
3747      this.form_error(elementAgree, is_valid_agree ? '' : gettext('Please agree to the privacy policy'))
3748    }
3749    else if (!is_valid_agree) {
3750      error_messages.push(gettext('Please agree to the privacy policy'))
3751    }
3752
3753    if (!this.defaultErrorView) {
3754      this.error('general', error_messages.join('<br>'))
3755    }
3756
3757    if (is_valid_email && is_valid_agree) {
3758      this.searchTimestamp = Date.now()
3759      fetch(encodeURI(get_url_with_locale(this.url, this.language) + '?email=' + element.value + '&agree=1timestamp=' + this.searchTimestamp), {
3760          method: 'GET',
3761          headers: {
3762            'X-CSRFToken': getCookie('csrftoken')
3763          }
3764        })
3765        .then(checkStatus)
3766        .then(parseJSON)
3767        .then(function(data) {
3768          if(data.timestamp && parseInt(data.timestamp) < this.searchTimestamp) {
3769            throw 'expired data'
3770          }
3771          let html = ''
3772          if (data.success) {
3773            if (this.defaultSuccessView) {
3774              html = '<div class="form-success">' + data.message + '</div>'
3775            }
3776            else {
3777              html = '<p class="subscribe-message m-b-1 color-spring-green">' + data.message + '</p>'
3778            }
3779          }
3780          else {
3781            if (this.defaultErrorView) {
3782              this.form_error('general', data.message)
3783            }
3784            else {
3785              html = '<p class="subscribe-message m-b-1 color-roman">' + data.message + '</p>'
3786            }
3787          }
3788          element.value = ''
3789          this.form.insertAdjacentHTML('afterbegin', html)
3790          setTimeout(this.removeSubscribeMessage.bind(this), 8000)
3791        }.bind(this))
3792        .catch(function(e) { })
3793    }
3794  }
3795
3796  error(element, message) {
3797    if (this.defaultErrorView) {
3798      super.error(element, message)
3799    }
3800    else if (message == '') {
3801      this.removeSubscribeMessage()
3802    }
3803    else {
3804      this.form.insertAdjacentHTML('afterbegin', '<p class="subscribe-message m-b-1 color-roman">' + message + '</p>')
3805    }
3806  }
3807
3808  removeSubscribeMessage() {
3809    const nodes = this.form.querySelectorAll('.subscribe-message')
3810    nodes.forEach(function(node) {
3811      if (node && node.parentNode) {
3812        node.parentNode.removeChild(node)
3813      }
3814    })
3815  }
3816}
3817
3818class ContactForm extends BaseForm {
3819  constructor(show_recaptcha) {
3820    super()
3821    this.show_recaptcha = show_recaptcha
3822    this.form = document.querySelector('.contact-form')
3823    if (this.show_recaptcha) {
3824      this.recaptchaError = true
3825    }
3826    this.listen()
3827  }
3828
3829  listen() {
3830    this.form.querySelector('button[type="submit"]').addEventListener('click', this.handler.bind(this))
3831    this.form.querySelector('input[name="agree"]').addEventListener('change', function(event) {
3832      this.validAgree()
3833    }.bind(this))
3834    this.form.querySelector('input[name="email"]').addEventListener('input', function(event) {
3835      this.validEmail()
3836    }.bind(this))
3837    this.form.querySelector('textarea[name="message"]').addEventListener('input', function(event) {
3838      this.validMessage()
3839    }.bind(this))
3840    this.form.querySelector('input[name="attach"]').addEventListener('change', function(event) {
3841      this.validAttach()
3842    }.bind(this))
3843  }
3844
3845  handler(event) {
3846    event.preventDefault()
3847
3848    this.reset()
3849
3850    const validItems = []
3851    if (this.show_recaptcha) {
3852      validItems.push(this.validCaptcha())
3853    }
3854    validItems.push(this.validEmail())
3855    validItems.push(this.validAgree())
3856    validItems.push(this.validMessage())
3857    validItems.push(this.validAttach())
3858
3859    const is_valid = validItems.reduce(function(res, item) {
3860      return res && item
3861    })
3862
3863    if (is_valid) {
3864      this.form.submit()
3865    }
3866  }
3867
3868  validEmail() {
3869    const emailElement = this.form.querySelector('input[name="email"]')
3870    let is_valid = true
3871    if (this.show_recaptcha || emailElement.value) {
3872      is_valid = validatorEmail(emailElement.value)
3873    }
3874    this.form_error(emailElement, is_valid ? '' : gettext('Incorrect email'))
3875    return is_valid
3876  }
3877
3878  validAgree() {
3879    const element = this.form.querySelector('input[name="agree"]')
3880    const is_valid = element.checked
3881    this.form_error(element, is_valid ? '' : gettext('Please agree to the conditions'))
3882    return is_valid
3883  }
3884
3885  validCaptcha() {
3886    const recaptchaElement = this.form.querySelector('#id_recaptcha')
3887    this.form_error(recaptchaElement, this.recaptchaError ? gettext('Incorrect CAPTCHA') : '')
3888    return !this.recaptchaError
3889  }
3890
3891  verifyRecaptchaCallback(response) {
3892    this.recaptchaError = (response === undefined || response === null || response === "") ? true : false
3893    this.validCaptcha()
3894  }
3895
3896  validMessage() {
3897    const messageElement = this.form.querySelector('textarea[name="message"]')
3898    const is_valid = validatorEmpty(messageElement.value)
3899    this.form_error(messageElement, is_valid ? '': gettext('Your message can not be empty'))
3900    return is_valid
3901  }
3902
3903  validAttach() {
3904    const element = this.form.querySelector('input[name="attach"]');
3905    const previewElement = this.form.querySelector('input[name="attach"] + div > span');
3906    if (!element || (element && element.files.length === 0)) {
3907      previewElement.textContent = gettext('No file chosen')
3908      return true;
3909    }
3910    const is_valid_type = this.validFileType(element.files[0], element.dataset.types.split(','));
3911    const is_valid_size = this.validFileSize(element.files[0], element.dataset.size);
3912    if (!is_valid_type) {
3913      this.form_error(element, gettext('Not a valid file type, images and PDF supported only'));
3914    }
3915    else if (!is_valid_size) {
3916      this.form_error(element, gettext('Please select a file less than 10Mb'));
3917    }
3918    else {
3919      this.form_error(element, '');
3920    }
3921
3922    const check = is_valid_type && is_valid_size
3923
3924    if (check) {
3925      previewElement.textContent = element.files[0].name.toString();
3926    }
3927
3928    return check;
3929  }
3930
3931  validFileType(file, allowedTypes) {
3932    return allowedTypes.includes(file.type);
3933  }
3934
3935  validFileSize(file, maxSize) {
3936    return file.size <= parseInt(maxSize) && file.size > 0;
3937  }
3938}
3939
3940class WithdrawForm extends BaseForm {
3941  constructor() {
3942    super()
3943    this.form = document.querySelector('#partner-withdraw-form')
3944    this.listen()
3945  }
3946
3947  listen() {
3948    this.form.addEventListener('submit', this.handler.bind(this))
3949    this.form.querySelector('input[name="amount"]').addEventListener('beforeinput', (event) => {
3950      const data = event.data;
3951      if(data) {
3952          const isAllow = /^\d*\.?\d*$/.test(data);
3953          if(!isAllow) {
3954              event.preventDefault();
3955          }
3956      }
3957    });
3958    this.form.querySelector('input[name="amount"]').addEventListener('change', function(event) {
3959      this.validAmount()
3960    }.bind(this))
3961    this.form.querySelector('input[name="invoice"]').addEventListener('change', function(event) {
3962      this.validInvoice()
3963    }.bind(this))
3964  }
3965
3966  handler(event) {
3967    event.preventDefault()
3968
3969    this.reset()
3970
3971    const validItems = []
3972    validItems.push(this.validAmount())
3973    validItems.push(this.validInvoice())
3974
3975    const is_valid = validItems.reduce(function(res, item) {
3976      return res && item
3977    })
3978
3979    if (is_valid) {
3980      this.form.submit()
3981    }
3982  }
3983
3984  validAmount() {
3985    const amountInput = this.form.querySelector('input[name="amount"]')
3986    const max = parseFloat(amountInput.dataset.vmax)
3987    const min = parseFloat(amountInput.dataset.vmin)
3988    const amount = parseFloat(amountInput.value)
3989    let is_valid = false
3990    if (!validatorEmpty(amountInput.value) || amount == 0) {
3991      this.form_error(amountInput, is_valid ? '': gettext('Amount is required'))
3992    } else if (amount < min) {
3993      this.form_error(amountInput, is_valid ? '': gettext('Min payout amount') + ' - ' + amountInput.dataset.vmin + ' USD')
3994    } else if (amount > max) {
3995      this.form_error(amountInput, is_valid ? '': gettext('Max available payout amount') + ' - ' + amountInput.dataset.vmax + ' USD')
3996    } else {
3997      is_valid = true
3998    }
3999    return is_valid
4000  }
4001
4002  validInvoice() {
4003    const element = this.form.querySelector('input[name="invoice"]');
4004    if (!element || (element && element.files.length === 0)) {
4005      this.form_error(element, gettext('Please attach your invoice'));
4006      return false;
4007    }
4008    const is_valid_type = this.validFileType(element.files[0], element.dataset.types.split(','));
4009    const is_valid_size = this.validFileSize(element.files[0], element.dataset.size);
4010    if (!is_valid_type) {
4011      this.form_error(element, gettext('Not a valid file type, images and PDF supported only'));
4012    }
4013    else if (!is_valid_size) {
4014      this.form_error(element, gettext('Please select a file less than 10Mb'));
4015    }
4016    else {
4017      this.form_error(element, '');
4018    }
4019    return is_valid_type && is_valid_size;
4020  }
4021
4022  validFileType(file, allowedTypes) {
4023    return allowedTypes.includes(file.type);
4024  }
4025
4026  validFileSize(file, maxSize) {
4027    return file.size <= parseInt(maxSize) && file.size > 0;
4028  }
4029}
4030
4031class Tab {
4032  constructor(tabElement=null) {
4033    this.run(tabElement);
4034  }
4035
4036  run(tabElement) {
4037    const tabHandler = (tabElement) => {
4038      let activeButtonElement = undefined;
4039
4040      tabElement.querySelectorAll('.is_active[data-type="navigation"]').forEach((buttonElement) => {
4041        if (activeButtonElement === undefined) {
4042          activeButtonElement = buttonElement;
4043        }
4044      });
4045
4046      tabElement.querySelectorAll('[data-type="navigation"]').forEach((buttonElement, index) => {
4047        if (activeButtonElement === undefined && index === 0) {
4048          activeButtonElement = buttonElement;
4049        }
4050
4051        const target = buttonElement.dataset.target;
4052
4053        if (activeButtonElement === buttonElement) {
4054          buttonElement.classList.add('is_active');
4055          tabElement.querySelector('#' + target).classList.add('is_active');
4056        } else {
4057          buttonElement.classList.remove('is_active');
4058          tabElement.querySelector('#' + target).classList.remove('is_active');
4059        }
4060
4061        buttonElement.addEventListener('click', (event) => {
4062          tabElement.querySelectorAll('[data-type="pane"]').forEach((el) => el.classList.remove('is_active'));
4063          tabElement.querySelector('#' + target).classList.add('is_active');
4064          tabElement.querySelectorAll('[data-type="navigation"]').forEach((el) => el.classList.remove('is_active'));
4065          buttonElement.classList.add('is_active');
4066        });
4067      });
4068    }
4069
4070    if (tabElement !== null) {
4071      tabHandler(tabElement);
4072    } else {
4073      document.querySelectorAll('[data-tab="true"]').forEach((el) => {
4074        tabHandler(el);
4075      });
4076    }
4077  }
4078}
4079
4080;(() => {
4081  'use strict'
4082
4083  window.ubisoftActivation = {
4084    initiated: false,
4085    lang: window.navigator.language || navigator.userLanguage,
4086    appId: '59adae69-0388-4a38-bd8c-1f5cb3a714ef',
4087    genomeId: '7f6502bf-f172-401c-b565-42679f2168f0',
4088    sdk: null,
4089    url: null,
4090    isFirstTry: true,
4091    callbackUrl: null,
4092    activeForm: null,
4093    productName: null,
4094    popup: null,
4095
4096    init: function() {
4097      if (this.initiated) {
4098        return
4099      }
4100      this.initiated = true
4101      Connect.init({
4102        env: 'PROD',
4103        appId: this.appId,
4104        genomeId: this.genomeId,
4105        lang: this.lang,
4106        nextUrl: '',
4107        thirdPartyCookiesSupport: false
4108      });
4109      Connect.sdk.subscribe(function(sdk) {
4110        window.ubisoftActivation.sdk = sdk;
4111        window.ubisoftActivation.proceed();
4112      });
4113      this.url = 'https://connect.ubisoft.com/login?appId=' + this.appId + '&genomeId=' + this.genomeId + '&lang=' + this.lang + '&nextUrl=' + this.callbackUrl
4114    },
4115
4116    proceed: function() {
4117      if (!this.initiated) {
4118        this.init()
4119      }
4120      this.sdk.getTicket().subscribe(function(response) {
4121        if (response.status !== 'ok' && window.ubisoftActivation.isFirstTry) {
4122          let popup = new PopUp()
4123          popup.isModal = true
4124          popup.setHeadline(gettext('Login to Ubisoft')).setBodyHtml('<iframe width="400" height="655" src="' + window.ubisoftActivation.url + '"></iframe>')
4125          popup.afterClose = function() {
4126            if (!window.ubisoftActivation.isFirstTry) {
4127              window.ubisoftActivation.proceed()
4128            }
4129          }
4130          popup.show()
4131          window.ubisoftActivation.popup = popup
4132          return
4133        }
4134        if (response.status !== 'ok' && !window.ubisoftActivation.isFirstTry) {
4135          window.ubisoftActivation.isFirstTry = true
4136          return
4137        }
4138
4139        var ticketData = response.payload
4140        window.ubisoftActivation.sdk.getProfiles(ticketData.ticket, ticketData.sessionId, ticketData.userId).subscribe(function(response){
4141          var confirm_popup = new PopUp()
4142          confirm_popup.isModal = true
4143          confirm_popup.runCloseEvent = false
4144          confirm_popup.showPopupCloseButton = false
4145          confirm_popup.setClass('popup-container').setHeadline(gettext('Activate in Ubisoft Connect')).setBodyHtml('<p>' + gettext('Are you sure that you want to add game "{0}" to account {1}?').format(window.ubisoftActivation.productName, response.payload.profiles[0].nameOnPlatform) + '</p>').setFooterHtml(
4146            '<div class="popup-container--footer-action"><button type="button" class="button" id="confirm-popup-button-yes">' + gettext('Yes') + '</button></div>' +
4147            '<div class="popup-container--footer-close m-r-1"><button type="button" class="button button-outline" id="confirm-popup-button-no">' + gettext('No') + '</button></div>'
4148          ).show()
4149          document.querySelector('#confirm-popup-button-yes').addEventListener('click', function(event) {
4150            confirm_popup.close()
4151            window.ubisoftActivation.activeForm.querySelector('input[name="user_uid"]').value=ticketData.userId
4152            window.ubisoftActivation.activeForm.querySelector('input[name="ticket"]').value=ticketData.ticket
4153            window.ubisoftActivation.sdk.logout(ticketData.ticket, ticketData.sessionId).subscribe(function(s) {
4154              window.ubisoftActivation.activeForm.submit()
4155              return
4156            })
4157          })
4158
4159          document.querySelector('#confirm-popup-button-no').addEventListener('click', function(event) {
4160            confirm_popup.close()
4161            window.ubisoftActivation.sdk.logout(ticketData.ticket, ticketData.sessionId).subscribe(function(s) {
4162              return
4163            });
4164          })
4165        })
4166      })
4167    }
4168  }
4169})()
4170
4171;(() => {
4172  'use strict'
4173
4174  const throttle = (type, name, obj) => {
4175    obj = obj || window;
4176    let running = false;
4177    let widthScreen = 0;
4178    const func = () => {
4179      const tmpWidthScreen = window.innerWidth;
4180      if (running || widthScreen === tmpWidthScreen) { return; }
4181      running = true;
4182      requestAnimationFrame(function() {
4183        widthScreen = tmpWidthScreen;
4184        document.dispatchEvent(new CustomEvent(
4185            name,
4186            {
4187              bubbles: true,
4188              cancelable: true,
4189              detail: {
4190                orientation: widthScreen > window.innerHeight ? "landscape" : "primary"
4191              }
4192            }
4193        ));
4194        running = false;
4195      });
4196    };
4197    obj.addEventListener(type, func);
4198  };
4199
4200  if (window.DeviceOrientationEvent) {
4201    throttle("resize", "rotate");
4202  }
4203})()
4204
4205;(() => {
4206  'use strict'
4207
4208  document.addEventListener('touchstart', handleTouchStart, false)
4209  document.addEventListener('touchmove', handleTouchMove, false)
4210  document.addEventListener('touchend', handleTouchEnd, false)
4211
4212  let xDown = null
4213  let yDown = null
4214  let xDiff = null
4215  let yDiff = null
4216  let timeDown = null
4217  let startElement = null
4218
4219  function handleTouchEnd(e) {
4220    if (startElement !== e.target) return
4221
4222    const swipeThreshold = 20 // 20px
4223    const swipeTimeout = 400 // 400ms
4224    const timeDiff = Date.now() - timeDown
4225    let eventDir = null
4226    let changedTouches = e.changedTouches || e.touches || []
4227
4228    if (Math.abs(xDiff) > Math.abs(yDiff)) {
4229      if (Math.abs(xDiff) > swipeThreshold && timeDiff < swipeTimeout) {
4230        eventDir = xDiff > 0 ? 'left' : 'right'
4231      }
4232    }
4233    else if (Math.abs(yDiff) > swipeThreshold && timeDiff < swipeTimeout) {
4234      eventDir = yDiff > 0 ? 'up' : 'down'
4235    }
4236
4237    if (eventDir !== null) {
4238      startElement.dispatchEvent(new CustomEvent('swiped', { bubbles: true, cancelable: true, detail: {dir: eventDir} }))
4239    }
4240
4241    xDown = null
4242    yDown = null
4243    timeDown = null
4244  }
4245
4246  function handleTouchStart(e) {
4247    startElement = e.target
4248    timeDown = Date.now()
4249    xDown = e.touches[0].clientX
4250    yDown = e.touches[0].clientY
4251    xDiff = 0
4252    yDiff = 0
4253  }
4254
4255  function handleTouchMove(e) {
4256    if (xDown === null || yDown === null) return
4257    xDiff = xDown - e.touches[0].clientX
4258    yDiff = yDown - e.touches[0].clientY
4259  }
4260})()
4261
4262// menu
4263;(() => {
4264  'use strict'
4265
4266  const $popoverLinks = document.querySelectorAll('[data-popover]')
4267  const $popovers = document.querySelectorAll('.popover')
4268  const $popmenuLink = document.querySelector('[data-popmenu]')
4269  const $popmenuCloseLink = document.querySelector('.popmenu-container-close')
4270  const $popmenuToggleLinks = document.querySelectorAll('[data-popmenu_toggle]')
4271  const $popmenutoggles = document.querySelectorAll('.popmenu-toogle')
4272
4273  for (let index = 0; index < $popoverLinks.length; index++) {
4274    $popoverLinks[index].parentNode.addEventListener('mouseover', openPopover)
4275    $popoverLinks[index].addEventListener('click', function() {event.preventDefault()})
4276    $popoverLinks[index].parentNode.addEventListener('touchstart', openPopover, {passive: false})
4277    $popoverLinks[index].parentNode.addEventListener('mouseout', closePopover)
4278  }
4279  
4280  for (let index = 0; index < $popmenuToggleLinks.length; index++) {
4281    $popmenuToggleLinks[index].addEventListener('touchstart', openPopmenuToggle, {passive: false})
4282    $popmenuToggleLinks[index].addEventListener('click', openPopmenuToggle)
4283  }  
4284
4285  if ($popmenuLink) {
4286    $popmenuLink.parentNode.addEventListener('touchstart', openPopmenu, {passive: false})
4287    $popmenuLink.parentNode.addEventListener('click', openPopmenu)
4288  }
4289
4290  if ($popmenuCloseLink) {
4291    $popmenuCloseLink.addEventListener('click', closePopmenu)
4292    $popmenuCloseLink.addEventListener('touchstart', closePopmenu, {passive: false})
4293  }
4294
4295  function closePopover (event) {
4296    for (let index = 0; index < $popovers.length; index++) {
4297      $popovers[index].classList.remove('popover-open')
4298    }
4299  }
4300  
4301  function closePopmenu (event) {
4302    document.querySelector('.wrapper').style.filter = 'none'
4303    document.querySelector('body').style.overflow = 'auto'
4304    document.querySelector($popmenuLink.getAttribute('href')).classList.remove('popmenu-open')
4305  }
4306  
4307  function closePopmenuToggle (event) {
4308    for (let index = 0; index < $popmenutoggles.length; index++) {
4309      $popmenutoggles[index].classList.remove('popmenu-toogle-open')
4310    }
4311    for (let index = 0; index < $popmenuToggleLinks.length; index++) {
4312      $popmenuToggleLinks[index].classList.remove('is_active')
4313    }    
4314  }
4315
4316  function openPopover (event) {
4317    event.preventDefault()
4318    const elem = this.querySelector('[data-popover]')
4319    if (document.querySelector(elem.getAttribute('href')).classList.contains('popover-open')) {
4320      document.querySelector(elem.getAttribute('href')).classList.remove('popover-open')
4321    } else {
4322      closePopover()
4323      document.querySelector(elem.getAttribute('href')).classList.add('popover-open')
4324    }
4325    event.stopImmediatePropagation()
4326  }
4327  
4328  function openPopmenu (event) {
4329    event.preventDefault()
4330    document.querySelector('.popmenu-container--for-list').style.height = '0'
4331    const elem = this.querySelector('[data-popmenu]')
4332    document.querySelector(elem.getAttribute('href')).classList.add('popmenu-open')
4333    document.querySelector('.wrapper').style.filter = 'blur(5px)'
4334    document.querySelector('body').style.overflow = 'hidden'
4335    setTimeout(function() {
4336      document.querySelector('.popmenu-container--for-list').style.height = '100%'
4337    }, 300)
4338    event.stopImmediatePropagation()
4339  }
4340  
4341  function openPopmenuToggle (event) {
4342    event.preventDefault()
4343    if (document.querySelector(this.getAttribute('href')).classList.contains('popmenu-toogle-open')) {
4344      document.querySelector(this.getAttribute('href')).classList.remove('popmenu-toogle-open')
4345      this.classList.remove('is_active')
4346    } else {
4347      closePopmenuToggle()
4348      document.querySelector(this.getAttribute('href')).classList.add('popmenu-toogle-open')
4349      this.classList.add('is_active')
4350    }
4351    event.stopImmediatePropagation()    
4352  }
4353
4354
4355  const formErrorElements = document.querySelectorAll('.form-error')
4356  if (formErrorElements) {
4357    formErrorElements.forEach(function(elem) {
4358      elem.scrollIntoView()
4359    })
4360  }
4361})()
4362
4363;(() => {
4364  'use strict'
4365  const cartInstance = new Cart(window.cart_items, window.current_language_code)
4366  const searchInstance = new SearchForm('#search-form', window.current_language_code)
4367  const selectInstance = new Select()
4368  const subscribeFormInstance = new SubscribeForm(window.current_language_code)
4369})()
4370
4371const ageFormHandler = (form) => {
4372  const isPopup = form.dataset.popup === '1';
4373  form.addEventListener('submit', (e) => {
4374    e.stopPropagation();
4375    e.preventDefault();
4376    const formData  = new FormData(form);
4377    fetch(form.action, {
4378        method: 'POST',
4379        headers: {
4380          'X-CSRFToken': getCookie('csrftoken')
4381        },
4382        body: formData
4383      })
4384      .then(checkStatus)
4385      .then(parseJSON)
4386      .then((data) => {
4387        if (!isPopup) {
4388          location.reload();
4389          return true;
4390        }
4391
4392        if (data.hasOwnProperty('full_years')) {
4393          document.querySelectorAll('[data-required-age]').forEach((el) => {
4394            const requiredAge = parseInt(el.dataset.requiredAge !== '' ? el.dataset.requiredAge : 0);
4395            if (requiredAge <= parseInt(data.full_years)) {
4396              el.classList.remove('blured');
4397            }
4398          });
4399        }
4400
4401        const productId = form.dataset.productId;
4402
4403        if (productId) {
4404        document.dispatchEvent(new CustomEvent('addToCart', {
4405            detail: {
4406              product: {
4407                id: productId
4408              }
4409            }
4410          }));
4411        }
4412      });
4413  });
4414}
4415
4416document.addEventListener('DOMContentLoaded', function() {
4417  const forms = document.querySelectorAll('.check-age-form');
4418  forms.forEach((form) => {
4419    ageFormHandler(form);
4420  });
4421});

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.