PageSourceSearch

https://sakraforum.se/contact

html sakraforum.se collected 2026-10-08 05:05:18 UTC 140,883 bytes, 3,331 lines download raw bytes

1<!DOCTYPE html>
2<html lang="sv">
3
4<head><meta property="ix:host" content="cdn.quickbutik.com/images">
5  <title>Kontakta oss - Säkra forum</title>
6  <meta charset="utf-8">
7
8  <meta name="author" content="Quickbutik">
9  <meta name="description" content="Kontakta oss - Säkra forum">
10
11  <meta property="og:description" content="Kontakta oss - Säkra forum">
12  <meta property="og:title" content="Kontakta oss - Säkra forum">
13  <meta property="og:url" content="https://sakraforum.se/contact">
14  <meta property="og:site_name" content="Säkra forum">
15  <meta property="og:type" content="website"> 
16  <meta property="og:image" content="">
17  
18  <meta property="twitter:card" content="summary">
19  <meta property="twitter:title" content="Säkra forum">
20  <meta property="twitter:description" content="Kontakta oss - Säkra forum">
21  <meta property="twitter:image" content="">
22
23  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
24
25  <link rel="canonical" href="https://sakraforum.se/contact">
26
27  <!-- Google Fonts -->
28  
29  
30  
31  <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
32  <link rel="preload" href="https://fonts.googleapis.com/css?family=Lato:400,700%7CMerriweather:400,600,700&display=swap" as="style">
33  <link href="https://fonts.googleapis.com/css?family=Lato:400,700%7CMerriweather:400,600,700&display=swap" rel="stylesheet" type="text/css" media="all">
34
35  <!-- Preload images -->
36  <link rel="preload" as="image" href="https://cdn.quickbutik.com/images/100911C/templates/lejon-28/assets/logo.png?s=79548&auto=format&fit=clip&h=160&w=300">
37  
38  <!-- Favicon -->
39  <link rel="icon" type="image/png" href="https://cdn.quickbutik.com/images/100911C/templates/lejon-28/assets/favicon.png?s=79548&auto=format">
40
41  <!-- Components -->
42  
42<script defer src="https://storage.quickbutik.com/templates/lejon/js/settings.js?79548"></script>
42
43  
43<script defer src="https://storage.quickbutik.com/templates/lejon/js/tailwind-styling.js?79548"></script>
43
44
45  
45<script src="https://storage.quickbutik.com/templates/lejon/js/appsOverrides.js?79548"></script>
45
46  
47  <!-- Quickbutik -->
48  
49<script type="text/javascript">var qs_store_url = "https://sakraforum.se";</script>
vendor: 1 bytes, line 49
49
50<script type="text/javascript">var qs_store_apps_data = {"location":"\/contact","products":[],"product_id":null,"category_id":null,"category_name":null,"fetch":"fetch","order":null,"search":null,"recentPurchaseId":null}; var qs_store_apps = [];</script>
vendor: 1 bytes, line 50
50
51<script defer type="text/javascript" src="/assets/qb_essentials_alpine.js?v=20260819a"></script>
51
52
53  <!-- Libraries -->
54  
54<script defer type="text/javascript" src="/assets/imgix.min.js?v=290323"></script>
54
55  
55<script defer src="https://storage.quickbutik.com/templates/lejon/js/swiper-bundle.min.js?79548"></script>
55
56  
56<script defer src="https://storage.quickbutik.com/templates/lejon/js/photoswipe-lightbox.umd.min.js?79548"></script>
56
57  
57<script defer src="https://storage.quickbutik.com/templates/lejon/js/photoswipe.umd.min.js?79548"></script>
57
58
59  <!-- Default parameters for Imgix -->
60  
60<script>document.addEventListener("DOMContentLoaded", () => { imgix.config.defaultParams = { auto: 'format' } })</script>
60
61
62  <!-- Scripts in head -->  
63	  <meta name="google-site-verification" content="8p52JvAfHwT4Le0y1Cb1ZN8LEfTucgPO0uuSICutCBw" />
64
65  <!-- Stylesheet -->
66  <style>[x-cloak]{display: none !important;}img[ix-sizes="auto"]{display:block;width:100%;}img[src=""],img[src="#"],img:not([src]){visibility: hidden;}</style>
67  <link rel="preload" href="https://storage.quickbutik.com/templates/lejon/css/swiper-bundle.min.css?79548" as="style" onload="this.onload=null;this.rel='stylesheet'">
68  <noscript><link rel="stylesheet" href="https://storage.quickbutik.com/templates/lejon/css/swiper-bundle.min.css?79548"></noscript>
69  <link rel="preload" href="https://storage.quickbutik.com/templates/lejon/css/photoswipe.css?79548" as="style" onload="this.onload=null;this.rel='stylesheet'">
70  <noscript><link rel="stylesheet" href="https://storage.quickbutik.com/templates/lejon/css/photoswipe.css?79548"></noscript>
71  <link type="text/css" rel="stylesheet" href="https://storage.quickbutik.com/stores/100911C/templates/lejon-28/css/theme.css?79548">
72<script type="text/javascript" defer>document.addEventListener('DOMContentLoaded', () => {
73    document.body.setAttribute('data-qb-page', 'contact');
74});</script>
74<script type="text/javascript" defer>(function () {
75  const MAX_GET_QUERY_LENGTH = 2000;
76
77  window.apps = window.apps ?? {};
78
79  window.registerApp = function (app) {
80    console.debug('registerApp', app);
81    window.apps[app.identifier] = app;
82  };
83
84  document.addEventListener('DOMContentLoaded', () => {
85    const page = document.body.dataset?.qbPage;
86    const bodyFormData = new FormData();
87
88    const productIds = [
89      ...new Set(
90        [...document.querySelectorAll('[data-qb-selector="product-item"]')]
91          .map((product) => product.dataset.productId?.trim())
92          .filter(Boolean)
93      ),
94    ];
95
96    bodyFormData.set('location', window.location.pathname + window.location.search);
97    bodyFormData.set('products', productIds.join(','));
98    bodyFormData.set(
99      'category_id',
100      document.querySelector('[data-qb-category-id]')?.dataset?.qbCategoryId || ''
101    );
102    bodyFormData.set(
103      'product_id',
104      ['product'].includes(page) && document.querySelector('[data-product-id]')?.dataset?.productId || ''
105    );
106
107    bodyFormData.set('fetch', 'fetch');
108    bodyFormData.set('order', '');
109    bodyFormData.set('search', '');
110    bodyFormData.set('recentPurchaseId', '');
111
112    const visitedProductId = bodyFormData.get('product_id');
113    if (visitedProductId) {
114      const visit = new URLSearchParams({ product_id: visitedProductId });
115      if (!(navigator.sendBeacon && navigator.sendBeacon(qs_store_url + '/apps/visit', visit))) {
116        fetch(qs_store_url + '/apps/visit', { method: 'post', body: visit, keepalive: true }).catch(() => {});
117      }
118    }
119
120    const query = [...bodyFormData.entries()]
121      .map(([key, value]) =>
122        encodeURIComponent(key) + '=' +
123        (key === 'products' ? productIds.map(encodeURIComponent).join(',') : encodeURIComponent(value))
124      )
125      .join('&');
126
127    const postApps = () => {
128      const postData = new FormData();
129      bodyFormData.forEach((value, key) => postData.append(key, value));
130      postData.set('variants', JSON.stringify(typeof qs_options !== 'undefined' ? qs_options : []));
131
132      return fetch(qs_store_url + '/apps/fetch', { method: 'post', body: postData }).then((res) => res.json());
133    };
134
135    const appsResponse =
136      query.length <= MAX_GET_QUERY_LENGTH
137        ? fetch(qs_store_url + '/apps/fetch?' + query, { method: 'get' })
138            .then((res) => {
139              if (!res.ok || !res.headers.get('X-QB-Apps-Fetch')) {
140                throw new Error('GET /apps/fetch not accepted (' + res.status + ')');
141              }
142
143              return res.json();
144            })
145            .catch(postApps)
146        : postApps();
147
148    appsResponse
149      .then((data) => {
150        console.debug('Fetch apps success', data);
151
152        if (typeof Alpine === 'undefined') {
153          document.addEventListener('alpine:init', () => {
154            initApps(data);
155          })
156        } else {
157          initApps(data);
158        }
159      })
160      .catch((error) => {
161        console.debug('Fetch apps error', error);
162      });
163  });
164
165  function initApps(appData) {
166    const page = document.querySelector('[data-qb-page]')?.dataset?.qbPage;
167
168    console.debug('initApps', appData, page, window.apps);
169
170    if (page) {
171      Object.keys(window.apps).forEach((appIdentifier) => {
172        if (window.apps[appIdentifier].pages.includes(page)) {
173          let appOverrides = window?.appsOverrides?.[appIdentifier];
174
175          if (appOverrides) {
176            window.apps[appIdentifier] = merge(
177              window.apps[appIdentifier],
178              appOverrides
179            );
180          }
181
182          window.apps[appIdentifier].onMount({ appData, page });
183        }
184      });
185    }
186  }
187
188  function isObject(obj) {
189    return obj !== null && typeof obj === 'object';
190  }
191
192  function merge(target, source) {
193    if (!isObject(target) || !isObject(source)) {
194      throw new Error('Both target and source must be objects.');
195    }
196
197    for (const key in source) {
198      if (source.hasOwnProperty(key)) {
199        const targetValue = target[key];
200        const sourceValue = source[key];
201
202        if (isObject(sourceValue) && isObject(targetValue)) {
203          target[key] = merge(targetValue, sourceValue);
204        } else {
205          target[key] = sourceValue;
206        }
207      }
208    }
209
210    return target;
211  }
212})();
213</script>
213<script src="/assets/shopassets/cookieconsent/cookieconsent.js?v=230"></script>
213
214<style>
215    :root {
216        --cc-btn-primary-bg: #303E80 !important;
217        --cc-btn-primary-text: #fff !important;
218        --cc-btn-primary-hover-bg: #303E80 !important;
219        --cc-btn-primary-hover-text: #fff !important;
220    }
221</style>
222<style>
223    html.force--consent,
224    html.force--consent body{
225        height: auto!important;
226        width: 100vw!important;
227    }
228</style>
229
230    
230<script type="text/javascript">
231    var qb_cookieconsent = initCookieConsent();
232    var qb_cookieconsent_blocks = [{"title":"Vi anv\u00e4nder cookies","description":"Vi anv\u00e4nder cookies f\u00f6r att anpassa det inneh\u00e5ll som visas f\u00f6r dig och f\u00f6r att du ska f\u00e5 b\u00e4sta t\u00e4nkbara upplevelse n\u00e4r du handlar hos oss."},{"title":"N\u00f6dv\u00e4ndiga cookies *","description":"Dessa cookies \u00e4r n\u00f6dv\u00e4ndiga f\u00f6r att butiken ska fungera korrekt och g\u00e5r inte att st\u00e4nga av utan att beh\u00f6va l\u00e4mna denna webbplats. De anv\u00e4nds f\u00f6r att funktionalitet som t.ex. varukorg, skapa ett konto och annat ska fungera korrekt.","toggle":{"value":"basic","enabled":true,"readonly":true}},{"title":"Statistik och prestanda","description":"Dessa cookies g\u00f6r att vi kan r\u00e4kna bes\u00f6k och trafik och d\u00e4rmed ta reda p\u00e5 saker som vilka sidor som \u00e4r mest popul\u00e4ra och hur bes\u00f6karna r\u00f6r sig p\u00e5 v\u00e5r webbplats. All information som samlas in genom dessa cookies \u00e4r sammanst\u00e4lld, vilket inneb\u00e4r att den \u00e4r anonym. Om du inte till\u00e5ter dessa cookies kan vi inte ge dig en lika skr\u00e4ddarsydd upplevelse.","toggle":{"value":"analytics","enabled":false,"readonly":false},"cookie_table":[{"col1":"_ga","col2":"google.com","col3":"2 years","col4":"Google Analytics"},{"col1":"_gat","col2":"google.com","col3":"1 minute","col4":"Google Analytics"},{"col1":"_gid","col2":"google.com","col3":"1 day","col4":"Google Analytics"}]},{"title":"Marknadsf\u00f6ring","description":"Dessa kakor s\u00e4tts via v\u00e5r webbplats av v\u00e5ra annonspartners f\u00f6r att skapa en profil utifr\u00e5n dina intressen och visa relevanta annonser p\u00e5 andra webbplatser. De kan samla information om ditt surfbeteende och anv\u00e4ndas f\u00f6r att identifiera din webbl\u00e4sare och internetenhet. Vissa annonspartners kan \u00e4ven ta emot hashad kontaktinformation (till exempel e-postadress) f\u00f6r att f\u00f6rb\u00e4ttra annonsm\u00e5lgrupperingen. Om du inte till\u00e5ter dessa kakor kommer du att uppleva mindre riktad annonsering.","toggle":{"value":"marketing","enabled":false,"readonly":false}}];
233</script>
233
234<link rel="stylesheet" href="https://storage.quickbutik.com/stores/100911C/templates/lejon-28/css/tailwind.css?79548" />    <!-- Google Analytics 4 -->
vendor: 68 bytes, lines 234-235
234
235    <script async src="https://www.googletagmanager.com/gtag/js?id=
235G-E5QEP6118N
vendor: 16 bytes, lines 235-236
235"></script>
236    
236<script>
237        window.dataLayer = window.dataLayer || [];
238        function gtag(){dataLayer.push(arguments);}
239
240        gtag('consent', 'default', {
241            'analytics_storage': 'denied',
242            'ad_storage': 'denied',
243            'ad_user_data': 'denied',
244            'ad_personalization': 'denied',
245            'wait_for_update': 500,
246        });
247
248        // Update consent if already granted (returning visitor with cc_cookie)
249        (function(){
250            try {
251                var m = document.cookie.match(/(?:^|;\s*)cc_cookie=([^;]+)/);
252                if (m) {
253                    var c = JSON.parse(decodeURIComponent(m[1]));
254                    if (c && c.level) {
255                        var hasAnalytics = c.level.indexOf('analytics') !== -1;
256                        var hasMarketing = c.level.indexOf('marketing') !== -1;
257                        if (hasAnalytics || hasMarketing) {
258                            gtag('consent', 'update', {
259                                'analytics_storage': hasAnalytics ? 'granted' : 'denied',
260                                'ad_storage': hasMarketing ? 'granted' : 'denied',
261                                'ad_user_data': hasMarketing ? 'granted' : 'denied',
262                                'ad_personalization': hasMarketing ? 'granted' : 'denied',
263                            });
264                        }
265                    }
266                }
267            } catch(e) {}
268        })();
269
270        // Handle consent updates (user clicks Accept)
271        document.addEventListener('QB_CookieConsentAccepted', function(event) {
272            var cookie = qbGetEvent(event).cookie;
273            var getConsentLevel = function(level) { return (cookie && cookie.level && cookie.level.includes(level)) ? 'granted' : 'denied'; };
274
275            gtag('consent', 'update', {
276                'analytics_storage': getConsentLevel('analytics'),
277                'ad_storage': getConsentLevel('marketing'),
278                'ad_user_data': getConsentLevel('marketing'),
279                'ad_personalization': getConsentLevel('marketing'),
280            });
281        });
282
283        gtag('js', new Date());
284        gtag('config', 'G-E5QEP6118N', { 'allow_enhanced_conversions' : true } );
285
286        const gaClientIdPromise = new Promise(resolve => {
287            gtag('get', 'G-E5QEP6118N', 'client_id', resolve);
288        });
289        const gaSessionIdPromise = new Promise(resolve => {
290            gtag('get', 'G-E5QEP6118N', 'session_id', resolve);
291        });
292
293        Promise.all([gaClientIdPromise, gaSessionIdPromise]).then(([gaClientId, gaSessionId]) => {
294            var checkoutButton = document.querySelector("a[href='/cart/index']");
295            if (checkoutButton) {
296                checkoutButton.onclick = function() {
297                    var ccLevel = [];
298                    try {
299                        var m = document.cookie.match(/(?:^|;\s*)cc_cookie=([^;]+)/);
300                        if (m) { ccLevel = JSON.parse(decodeURIComponent(m[1])).level || []; }
301                    } catch(e) {}
302                    var hasAnalytics = ccLevel.indexOf('analytics') !== -1;
303                    var hasMarketing = ccLevel.indexOf('marketing') !== -1;
304                    var query = new URLSearchParams({
305                        ...(hasAnalytics ? { gaClientId, gaSessionId } : {}),
306                        consentCategories: [
307                            ...(hasAnalytics ? ['analytics'] : []),
308                            ...(hasMarketing ? ['marketing'] : []),
309                        ],
310                    });
311                    this.setAttribute("href", "/cart/index?" + query.toString());
312                };
313            }
314        });
315    </script>
315
316    <!-- End Google Analytics 4 --></head>
317
318
319
320
321
322<body class="antialiased bg-body text-body font-body">
323  <header
324    data-qb-area="header" data-qb-element=""
325    id="main-nav"
326    x-data="{navMenuOpen: false}"
327    @keydown.escape="navMenuOpen = false; $store.cart.hideCart(); $store.settings.hideSettings();"
328    class="sticky top-0 z-50 bg-header shadow-default">
329  
330    <section x-data class="w-full" data-qb-element="notifications">
331      <template x-for="notification in $store.notifications.notifications" :key="notification.id">
332        <div x-show="notification.visible" x-on:click="$store.notifications.removeNotification(notification.id)"
333        x-transition:enter='transition ease-out duration-default transform'
334        x-transition:enter-start='opacity-0 -translate-y-4'
335        x-transition:enter-end='opacity-100 translate-y-0'
336        x-transition:leave='transition ease-in duration-default transform'
337        x-transition:leave-start='opacity-100 translate-y-0'
338        x-transition:leave-end='opacity-0 -translate-y-4'
339        >
340          <div class="relative w-full px-4 sm:px-8 py-4 bg-black" :style="{'background-color': notification.backgroundColor || '#000'}">
341            <div class="relative z-10 flex items-center justify-between -m-2">
342              <div class="flex-1 p-2" :style="{'color': notification.textColor || '#fff'}">
343                <template x-if="notification.message">
344                  <h3 class="font-heading font-semibold text-lg" x-html="notification.message"></h3>
345                </template>
346                <template x-if="notification.htmlContent">
347                  <div x-html="notification.htmlContent"></div>
348                </template>
349              </div>
350              <div class="p-2">
351                <button
352                  class="w-8 h-8 flex items-center justify-center rounded-full hover:opacity-75 transition-opacity duration-200 cursor-pointer"
353                  :style="{'color': notification.textColor || '#fff'}"
354                  x-on:click.stop="$store.notifications.removeNotification(notification.id)"
355                  aria-label="Close notification"
356                >
357                  <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
358                    <path d="M12 4L4 12M4 4L12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
359                  </svg>
360                </button>
361              </div>
362            </div>
363          </div>
364        </div>
365      </template>
366    </section>
367  
368    <nav class="text-navbar">
369      <div class="container py-2 sm:py-4 mx-auto">
370        <div class="flex items-center justify-between relative min-h-10 md:h-20">
371  
372          <!-- Logo (left-aligned) -->
373          <div class="flex grow items-center justify-start">
374            <a data-qb-element="use_logo" href="https://sakraforum.se" class="content-center transition-all duration-200">
375              <img src="https://cdn.quickbutik.com/images/100911C/templates/lejon-28/assets/logo.png?s=79548&auto=format&fit=clip&h=160&w=400&dpr=2" alt="Säkra forum" class="transition-all duration-200 h-10 md:h-20 object-contain object-left pr-4 w-full md:max-w-[400px]">
376            </a>
377          </div>
378  
379          <!-- Logo (centered, absolute positioning) -->
380  
381          <!-- Smaller menu -->
382          <div data-qb-element="desktop_nav_large" class="hidden lg:flex grow shrink items-center justify-end">
383            <div class="overflow-y-auto">
384              
385            
386              
387              <ul
388                x-data="{ navbarLevelsDepth: 3 }"
389                class="flex flex-wrap justify-end  ">
390            
391            
392                    <li class="mx-2">
393                      <a href="/" class="flex  justify-center py-2 px-2 transition ease-in-out hover-opacity "
394                        title="Hem">
395                        <span>Hem</span>
396                      </a>
397                    </li>
398            
399            
400            
401                    <li class="mx-2">
402                      <a href="/sidor/oppettider" class="flex  justify-center py-2 px-2 transition ease-in-out hover-opacity "
403                        title="Öppettider">
404                        <span>Öppettider</span>
405                      </a>
406                    </li>
407            
408            
409            
410                    <li class="mx-2">
411                      <a href="/sidor/kopiera-nyckel-online" class="flex  justify-center py-2 px-2 transition ease-in-out hover-opacity "
412                        title="Hur går det till?">
413                        <span>Hur går det till?</span>
414                      </a>
415                    </li>
416            
417            
418            
419            
420                    <li x-data="dropdown" @mouseover="open = navbarLevelsDepth >= 1 && true" @mouseleave="open = false" class="mx-2 group">
421                      <div class="relative flex  justify-center items-center">
422                        <a href="/products" id="menu-item-2"
423                          class="flex justify-center py-2 px-2 transition ease-in-out hover-opacity ">
424                          <span>Webbshop</span>
425                        </a>
426                        <svg x-show="navbarLevelsDepth >= 1"  xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
427                            stroke-width="1.5" stroke="currentColor"
428                            class="w-4 h-4 cursor-pointer transform rotate-90 " :class="{'rotate-0' : open}">
429                            <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"></path>
430                        </svg>
431                      </div>
432            
433                      <!-- First drop down -->
434                      <ul x-cloak
435                        x-show="open"
436                        "
437                        x-transition:enter='transition ease-out transform'
438                        x-transition:enter-start='opacity-0 -translate-y-4'
439                        x-transition:enter-end='opacity-100 translate-y-0'
440                        x-transition:leave='transition ease-in transform'
441                        x-transition:leave-start='opacity-100 translate-y-0'
442                        x-transition:leave-end='opacity-0 -translate-y-4'
443                        class="absolute z-50 w-full max-w-xs px-6 py-2 bg-body text-body shadow-default rounded-md" aria-labelledby="menu-item-2">
444            
445            
446                            <li><a href="/products/nyckeltillbehor" class="block py-3 transition ease-in-out hover-opacity ">Nyckeltillbehör</a></li>
447            
448            
449            
450                            <li><a href="/products/lascylindrar" class="block py-3 transition ease-in-out hover-opacity ">LÃ¥scylindrar</a></li>
451            
452            
453            
454                            <li><a href="/products/lastillbehor" class="block py-3 transition ease-in-out hover-opacity ">Låstillbehör</a></li>
455            
456            
457            
458                            <li><a href="/products/nycklar" class="block py-3 transition ease-in-out hover-opacity ">Nycklar</a></li>
459            
460            
461            
462                            <li><a href="/products/hanglas" class="block py-3 transition ease-in-out hover-opacity ">Hänglås</a></li>
463            
464                      </ul>
465                    </li>
466            
467              
468              </ul>
469            </div>
470          </div>
471  
472          <div class="flex flex-none items-center justify-end">
473            
474            <!-- Search -->
475            <button data-qb-element="show_search" @click="$store.search.showSearch(); $nextTick(() => document.getElementById('searchInput').focus());"
476              aria-label="Sök" class="px-2 hover:text-primary transition duration-200 focus:outline-none">
477              <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 sm:w-7 sm:h-7">
478                <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
479              </svg>
480            </button>
481  
482            <!-- Login -->
483  
484            <!-- Wishlist -->
485  
486            <!-- Settings -->
487            
488            <button @click="$store.settings.showSettings()" aria-label="Inställningar"
489              class="hidden lg:block px-2 hover:text-primary transition duration-200 focus:outline-none"
490              style="display:none">
491              <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 sm:w-7 sm:h-7">
492                <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.
492074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
493                <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
494              </svg>
495            </button>
496  
497            <!-- Cart -->
498            <button @click="$store.cart.toggleCart()" aria-label="Varukorg"
499              class="relative px-2 hover:text-primary transition duration-200 focus:outline-none">
500              <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 sm:w-7 sm:h-7">
501                <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"></path>
502              </svg>
503  
504              <!-- Cart bubble -->
505              <template x-if="$store.cart.totalQuantity > 0">
506                <span x-text="$store.cart.totalQuantity" class="cart-bubble absolute flex items-center justify-center -top-2.5 lg:-top-1.5 -right-0.5 h-6 w-6 border-2 border-header bg-primary text-white font-bold text-xs rounded-full"></span>
507              </template>
508  
509            </button>
510  
511            <!-- Mobile menu button -->
512            <div class="flex justify-items-start -mr-2 lg:hidden">
513              <button @click="navMenuOpen = !navMenuOpen" aria-label="Menu"
514                class="navbar-burger py-3 px-2 sm:px-4 hover-opacity transition duration-200 focus:outline-none">
515                <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 sm:w-7 sm:h-7">
516                  <path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 7C3.22386 7 3 6.77614 3 6.5C3 6.22386 3.22386 6 3.5 6H20.5C20.7761 6 21 6.22386 21 6.5C21 6.77614 20.7761 7 20.5 7H3.5ZM3.5 12C3.22386 12 3 11.7761 3 11.5C3 11.2239 3.22386 11 3.5 11H20.5C20.7761 11 21 11.2239 21 11.5C21 11.7761 20.7761 12 20.5 12H3.5ZM3 16.5C3 16.7761 3.22386 17 3.5 17H20.5C20.7761 17 21 16.7761 21 16.5C21 16.2239 20.7761 16 20.5 16H3.5C3.22386 16 3 16.2239 3 16.5Z" fill="currentColor"></path>
517                </svg>
518              </button>
519            </div>
520  
521          </div>
522  
523        </div>
524      </div>
525    </nav>
526  
527    <!-- Larger menu -->
528  
529    <!-- Menu -->
530    <div
531      x-cloak
532      @keydown.escape="navMenuOpen = false"
533      x-show="navMenuOpen"
534      class="navbar-menu fixed top-0 left-0 bottom-0 w-full sm:max-w-sm z-40">
535      
536      <!-- Overlay -->
537      <div
538        x-show='navMenuOpen'
539        @click='navMenuOpen = false'
540        x-transition:enter='transition ease-out duration-default transform'
541        x-transition:enter-start='opacity-0'
542        x-transition:enter-end='opacity-100'
543        x-transition:leave='transition ease-in duration-default transform'
544        x-transition:leave-start='opacity-100'
545        x-transition:leave-end='opacity-0'
546        class='fixed inset-0 transition-opacity bg-overlay bg-opacity-40'
547        aria-hidden='true'
548      ></div>
549      
550      <!-- Menu -->
551      <nav
552        x-show='navMenuOpen'
553        @keydown.escape="navMenuOpen = false"
554        class="relative flex flex-col h-full bg-body"
555        x-transition:enter='transition ease-out duration-default transform'
556        x-transition:enter-start='opacity-0 -translate-x-10 sm:-translate-x-10'
557        x-transition:enter-end='opacity-100 translate-x-0'
558        x-transition:leave='transition ease-in duration-default transform'
559        x-transition:leave-start='opacity-100 translate-x-0'
560        x-transition:leave-end='opacity-0 -translate-x-10 sm:-translate-x-10'
561      >
562        <div class="flex items-center justify-between mb-12 p-6">
563    
564          <!-- Logo -->
565          <a class="items-center" href="https://sakraforum.se">
566            
567            <img alt="Säkra forum" class="img-fluid img-logo object-contain object-left w-full h-10 pr-8"
568              ix-src="https://cdn.quickbutik.com/images/100911C/templates/lejon-28/assets/logo.png?s=79548&auto=format">
569          </a>
570    
571          <!-- Close button -->
572          <button @click="navMenuOpen = false" class="inline-block navbar-close focus:outline-none" type="button"
573            aria-label="Close">
574            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
575              stroke="currentColor" class="w-6 h-6">
576              <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
577            </svg></button>
578        </div>
579    
580        <!-- Menu -->
581        <div class="px-6 overflow-y-auto">
582          
583            <ul>
584              
585                  <li class="">
586                    <a class="block py-3 font-bold hover:text-gray-900 " href="/"
587                      title="Hem">
588                      <span>Hem</span>
589                    </a>
590                  </li>
591    
592                
593              
594                  <li class="">
595                    <a class="block py-3 font-bold hover:text-gray-900 " href="/sidor/oppettider"
596                      title="Öppettider">
597                      <span>Öppettider</span>
598                    </a>
599                  </li>
600    
601                
602              
603                  <li class="">
604                    <a class="block py-3 font-bold hover:text-gray-900 " href="/sidor/kopiera-nyckel-online"
605                      title="Hur går det till?">
606                      <span>Hur går det till?</span>
607                    </a>
608                  </li>
609    
610                
611              
612    
613                <li x-data="{expanded:false}" class="">
614                  <div class="flex">
615                    <a href="/products"
616                      class="flex flex-1 py-3 font-bold hover:text-gray-900 ">
617                      <span id="navMenuItem2">Webbshop</span>
618                    </a>
619                    <div class="flex h-full flex-col py-3">
620                      <svg @click="expanded = !expanded" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
621                        stroke="currentColor" class="w-6 h-6 cursor-pointer transform duration-default" :class="{'rotate-90' : expanded}">
622                        <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"></path>
623                      </svg>
624                    </div>
625                  </div>
626                  <ul x-show="expanded"
627                    x-transition:enter='transition ease-out duration-default transform'
628                    x-transition:enter-start='opacity-0 -translate-y-4'
629                    x-transition:enter-end='opacity-100 translate-y-0'
630                    x-transition:leave='transition ease-in duration-default transform'
631                    x-transition:leave-start='opacity-100 translate-y-0'
632                    x-transition:leave-end='opacity-0 -translate-y-4'
633                    class="pl-4" aria-labelledby="navMenuItem2">
634    
635    
636                      <li><a href="/products/nyckeltillbehor" class="block py-3 ">Nyckeltillbehör</a></li>
637    
638    
639    
640                      <li><a href="/products/lascylindrar" class="block py-3 ">LÃ¥scylindrar</a></li>
641    
642    
643    
644                      <li><a href="/products/lastillbehor" class="block py-3 ">Låstillbehör</a></li>
645    
646    
647    
648                      <li><a href="/products/nycklar" class="block py-3 ">Nycklar</a></li>
649    
650    
651    
652                      <li><a href="/products/hanglas" class="block py-3 ">Hänglås</a></li>
653    
654    
655                  </ul>
656                </li>
657                
658            </ul>
659        </div>
660    
661        <div class="p-6 mt-auto">
662    
663          <!-- Wishlist -->
664    
665          <!-- Settings -->
666          
667          <span href="" @click="$store.settings.showSettings()" class="block px-6 py-3 text-center button-rounding border border-foreground bg-transparent text-body font-bold cursor-pointer hover-opacity mb-2"
668            style="display:none">
669            Inställningar
670          </span>
671    
672          <!-- Login -->
673    
674        </div>
675      </nav>
676    </div>
677    <!-- Settings modal -->
678    <template x-teleport='body'>
679      <div
680        x-data
681        x-cloak
682        x-show='$store.settings.settingsOpen'
683        @keydown.escape='$store.settings.hideSettings()'
684        class='fixed inset-0 z-50 overflow-y-auto'
685        aria-labelledby='modal-title'
686        role='dialog'
687        aria-modal='true'
688      >
689        <div class='flex items-end justify-center min-h-screen text-center md:items-center sm:block'>
690          
691          <!-- Overlay -->
692          <div
693            @click='$store.settings.hideSettings()'
694            x-show='$store.settings.settingsOpen'
695            x-transition:enter='transition ease-out duration-default transform'
696            x-transition:enter-start='opacity-0'
697            x-transition:enter-end='opacity-100'
698            x-transition:leave='transition ease-in duration-default transform'
699            x-transition:leave-start='opacity-100'
700            x-transition:leave-end='opacity-0'
701            class='fixed inset-0 transition-opacity bg-overlay bg-opacity-40'
702            aria-hidden='true'></div>
703    
704          <!-- Modal -->
705          <div x-show='$store.settings.settingsOpen'
706            x-transition:enter='transition ease-out duration-default transform'
707            x-transition:enter-start='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
708            x-transition:enter-end='opacity-100 translate-y-0 sm:scale-100'
709            x-transition:leave='transition ease-in duration-default transform'
710            x-transition:leave-start='opacity-100 translate-y-0 sm:scale-100'
711            x-transition:leave-end='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
712            class='flex flex-col sm:inline-block w-full max-w-xl fixed top-0 bottom-0 sm:top-auto sm:bottom-auto sm:static sm:my-20 overflow-hidden text-left transition-all transform bg-body rounded-md shadow-xl 2xl:max-w-2xl'>
713    
714            <!-- Header -->  
715            <header class='flex items-center justify-between p-6 mb-2'>
716    
717              <!-- Title -->
718              <span class='text-h2 font-bold font-heading'>Inställningar</span>
719    
720              <!-- Close button -->
721              <button @click='$store.settings.hideSettings()' class='text-gray-600 focus:outline-none hover:text-gray-700'>
722                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
723                  <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
724                </svg>
725              </button>
726            </header>
727    
728            <div class="px-6 pb-6 mb-6 h-full overflow-y-auto">
729    
730              <!-- Languages -->
731    
732              <!-- Tax -->
733    
734              <!-- Currencies-->
735            
736            </div>
737          </div>
738        </div>
739      </div>
740    </template>    <!-- Search modal -->
741    <template x-teleport='body'>
742      <div
743        x-data='appSearchModal'
744        x-cloak
745        x-show='$store.search.searchOpen'
746        @keydown.escape='$store.search.hideSearch()'
747        class='fixed inset-0 z-50 overflow-y-auto'
748        aria-labelledby='modal-title'
749        role='dialog'
750        aria-modal='true'
751      >
752        <div class='flex items-end justify-center min-h-screen px-4 text-center md:items-center sm:block sm:p-0'>
753    
754          <!-- Overlay -->
755          <div
756            @click='$store.search.hideSearch()'
757            x-show='$store.search.searchOpen'
758            x-transition:enter='transition ease-out duration-default transform'
759            x-transition:enter-start='opacity-0'
760            x-transition:enter-end='opacity-100'
761            x-transition:leave='transition ease-in duration-default transform'
762            x-transition:leave-start='opacity-100'
763            x-transition:leave-end='opacity-0'
764            class='fixed inset-0 transition-opacity bg-overlay bg-opacity-40'
765            aria-hidden='true'
766          ></div>
767    
768          <!-- Modal -->
769          <div
770            modal="search"
771            x-show='$store.search.searchOpen'
772            x-transition:enter='transition ease-out duration-default transform'
773            x-transition:enter-start='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
774            x-transition:enter-end='opacity-100 translate-y-0 sm:scale-100'
775            x-transition:leave='transition ease-in duration-default transform'
776            x-transition:leave-start='opacity-100 translate-y-0 sm:scale-100'
777            x-transition:leave-end='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
778            class='flex flex-col h-screen sm:inline-block w-full h-max-screen sm:max-w-xl 2xl:max-w-2xl fixed top-0 bottom-0 sm:top-auto sm:bottom-auto sm:static sm:my-20 overflow-hidden text-left transition-all transform'
779            style='@media (min-width: 576px) { max-height: calc(80vh) }'
780          >
781            <div class="pointer-events-auto relative flex w-full flex-col max-h-full overflow-hidden border-none bg-body shadow-xl">
782    
783              <!-- Header -->
784              <header class='flex flex-col flex-shrink-0 p-6'>
785    
786                <div class="flex items-center justify-between pb-6">
787    
788                  <!-- Title -->
789                  <span class='text-h2 font-bold font-heading'>Sök</span>
790    
791                  <!-- Close button -->
792                  <button
793                    @click='$store.search.hideSearch()'
794                    class='text-body focus:outline-none hover-opacity'>
795                    <svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' class='w-6 h-6'>
796                      <path stroke-linecap='round' stroke-linejoin='round' d='M6 18L18 6M6 6l12 12'></path>
797                    </svg>
798                  </button>
799                </div>
800    
801                <form
802                  action='/shop/search'
803                  method='get' class='flex'>
804                  <div class='flex w-full relative'>
805                    <div class="flex h-full flex-col justify-center absolute top-0 left-0 pl-2">
806                      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 opacity-40">
807                        <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
808                      </svg>
809                    </div>
810                    <input
811                      type='search'
812                      name='s'
813                      x-model='searchTerm'
814                      id='searchInput'
815                      @input.debounce.500ms="search"
816                      placeholder='Sök produkter'
817                      aria-label='Sök'
818                      aria-describedby='button-search-desktop'
819                      class='flex flex-1 py-3 pr-3 pl-14 input-bg input-text input-border border input-rou
819nding focus:outline-none'
820                    >
821                  </div>
822                </form>
823              </header>
824    
825              <!-- No results found -->
826              <div x-show='searchTerm && noResults' class="px-6 mb-8">
827                <p class="mt-8 text-center">
828                  Inga sökresultat hittades
829                </p>
830              </div>
831    
832              <!-- Search results -->
833              <div x-show='!noResults && searchTerm != ""' class="relative overflow-y-auto pb-6">
834                <template x-for='result in results'>
835                  <a :href='result.product.url'>
836                    <div class="flex py-2 px-6 transition duration-default hover-opacity">
837                      <div class="flex flex-0 w-16 h-16 mr-6">
838                        <img :src="result.product.firstimage + '?w=100&h=100'" ix-sizes="auto" :alt="result.product.title" class="w-full object-contain object-center">
839                      </div>
840                      <div class="flex flex-1 flex-col justify-center">
841                        <span x-html='highlightTitle(result.product.title)'></span>
842                      </div>
843                      <div class="flex flex-col justify-center">
844                        <span x-text='result.product.price'></span>
845                      </div>
846                    </div>
847                    <hr class="border-foreground opacity-10">
848                  </a>
849                </template>
850              </div>
851          
852            </div>
853    
854          </div>
855        </div>
856      </div>
857    </template>
858    
859  </header>  
860  <!-- Campaign bar -->
861    <!-- Campaign bar -->
862    <div data-qb-area="campaignbar" data-qb-setting="show_campaignbar" id="campaignbar" class="flex bg-campaignbar group">
863      <div class="container text-center">
864  
865  
866          <div class="flex justify-center items-center">
867            <span class="block py-4 font-bold text-campaignbar">
868            FRI FRAKT TILL DE 1000 FÖRSTA KUNDERNA! ANGE KODEN &quot;FRIFRAKT&quot; I KASSAN
869            </span>
870  
871            <!-- Icon (arrow) -->
872          </div>
873  
874      </div>
875    </div>
876  
877
878
879
880  <!-- Standard layout utan sidomeny -->
881
882    <!-- Breadcrumbs -->
883    <section data-qb-area="general" data-qb-setting="show_breadcrumbs" class="py-4 bg-gray-400 bg-opacity-10 text-sm">
884      <div class="container mx-auto">
885        <ul class="flex flex-wrap">
886        
887          <li class="flex items-center mr-3">
888              
889            
890            <a class="inline-block mr-3 opacity-70 transition duration-default hover:opacity-100" href="/">Hem</a>
891            <span class="inline-block opacity-30">
892              /
893            </span>
894    
895          </li>
896          <li class="flex items-center mr-3">
897              
898            <span class="inline-block opacity-70" href="#">Kontakta oss</span>
899            
900    
901          </li>
902    
903        </ul>
904      </div>
905    </section>
906    <section data-qb-area="contact" x-data class="py-16">
907      <div class="container mx-auto">
908        <div class="w-full mx-auto max-w-3xl">
909    
910          <!-- Title -->
911          <h1 data-qb-setting="contact_title" class="mb-8 font-heading text-center text-h1">Hur kan vi hjälpa dig?</h1>
912          
913          <!-- Text -->
914    
915          <!-- Contact form -->
916    
917            <div data-qb-setting="contact_show_form" class="mx-auto">
918              <form role="form" id="contact-form" method="post">
919                
920                <!-- Form post success -->
921                
922    
923                <!-- Form post failed-->
924                
925            
926                <label for="name" class="block mb-2 font-bold">Namn</label>
927                <input class="block w-full input-bg input-text input-border border input-rounding input-py input-px mb-6 text-lg placeholder-gray-300"
928                  type="text" name="name" id="name" required="required" placeholder="Namn">
929                  
930                <label for="email" class="block font-bold mb-2">E-postadress</label>
931                <input class="block w-full input-bg input-text input-border border input-rounding input-py input-px mb-6 text-lg placeholder-gray-300"
932                  type="email" name="email" id="email" required="required" placeholder="E-postadress">
933                
934                <label for="message" class="block font-bold mb-2">Meddelande</label>
935                <textarea
936                  class="block w-full input-bg input-text input-border border input-rou
936nding input-py input-px mb-6 text-lg placeholder-gray-300 resize-none"
937                  name="message" id="message" required="required" cols="30" rows="10"
938                  placeholder="Meddelande"></textarea>
939    
940                <button type="submit" name="send" value="send" class="block w-full py-3 px-8 button-text-primary font-bold bg-primary button-rounding transition duration-default">Skicka</button>
941              </form>
942            </div>
943    
944          
945        </div>
946      </div>
947    </section>
948    
949    <!-- Contact free text -->
950    
951    <!-- Contact details -->
952    <section data-qb-area="contact" class="py-24">
953      <div class="container mx-auto">
954        <div class="flex flex-wrap justify-center">
955          
956    
957            <!-- Email -->
958            <div data-qb-setting="contact_show_contact_details" class="w-full sm:w-1/2 md:w-1/4 px-4 text-center mb-10 md:mb-0">
959              <h3 class="text-h3 font-heading mb-4">E-postadress</h3>
960              <a href="/cdn-cgi/l/email-protection#81e8efe7eec1f2e0eaf3e0e7eef3f4ecaff2e4" class="text-primary transition duration-default hover:text-body"><span class="__cf_email__" data-cfemail="f891969e97b88b99938a999e978a8d95d68b9d">[email&#160;protected]</span></a>
961            </div>
962    
963            <!-- Phone -->
964            <div data-qb-setting="contact_show_contact_details" class="w-full sm:w-1/2 md:w-1/4 px-4 text-center mb-10 md:mb-0">
965              <h3 class="text-h3 font-heading mb-4">Telefonnummer</h3>
966              <p>
967                  <a href="tel:087185505" class="text-primary transition duration-default hover:text-body">087185505</a>
968              </p>
969            </div>
970          
971    
972          <!-- Address -->
973          <div data-qb-setting="contact_show_address" class="w-full sm:w-1/2 md:w-1/4 px-4 text-center mb-10 md:mb-0">
974            <h3 class="text-h3 font-heading mb-4">Adress</h3>
975            <p>
976              Forumvägen 12<br>
977    
978              13153 Nacka<br>
979              
980              Sverige
981            </p>
982          </div>
983    
984          <!-- Social Media -->
985          <div data-qb-setting="contact_show_socialicons" class="w-full sm:w-1/2 md:w-1/4 px-4 text-center">
986            <h3 class="text-h3 font-heading mb-4">Sociala medier</h3>
987            <div>
988                <div class="flex justify-center flex-wrap mx-auto">
989                  
990                  <a class="inline-block mx-2 mb-4 text-primary hover-opacity" href="https://www.facebook.com/profile.php?id=100069590035185&amp;locale=sv_SE" target="_blank">
991                    <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24">
992                      <path fill="currentColor" d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z">
993                      </path>
994                    </svg>
995                  </a>
996                  <a class="inline-block mx-2 mb-4 text-primary hover-opacity" href="https://instagram.com/sakraforum" target="_blank">
997                    <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24">
998                      <path fill="currentColor"
999                        d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z">
1000                      </path>
1001                    </svg>
1002                  </a>
1003                </div>
1004    
1005            </div>
1006          </div>
1007    
1008        </div>
1009      </div>
1010    </section>
1011    
1012
1013
1014  <section data-qb-area="footer" class="bg-footer text-center lg:text-left">
1015    <div class="container py-8 md:py-12 mx-auto ">
1016      <div class="flex flex-wrap justify-between">
1017  
1018        <!-- Footer menu -->
1019        <div class="w-full sm:w-1/3 mb-10 lg:mb-0">
1020          
1021          <h5 data-qb-setting="footer_menu_heading" class="text-h5 font-bold mb-5 text-footer">
1022            Läs mer
1023          </h5>
1024  
1025          <ul data-qb-setting="use_footer_menu" class="opacity-70">
1026            <li class="mb-2">
1027              <a class="inline-block text-footer hover:opacity-50" href="/sidor/terms-and-conditions">Köpvillkor</a>
1028            </li>
1029            <li class="mb-2">
1030              <a class="inline-block text-footer hover:opacity-50" href="/contact">Kontakt</a>
1031            </li>
1032          </ul>
1033        </div>
1034  
1035          
1036        <!-- Social media link -->
1037        <div class="w-full sm:w-1/3 mb-10 lg:mb-0">
1038          <div data-qb-setting="show_footer_socialicons" class="text-footer mb-3">
1039  
1040            <h5 class="text-h5 font-bold mb-5 text-footer">
1041              Sociala medier
1042            </h5>
1043  
1044            <div class="opacity-70">
1045  
1046              <a class="inline-block mr-4 hover-opacity" aria-label="Facebook" href="https://www.facebook.com/profile.php?id=100069590035185&amp;locale=sv_SE" target="_blank">
1047                <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewB
1047ox="0 0 24 24">
1048                  <path fill="currentColor" d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z"></path>
1049                </svg>
1050              </a>
1051              <a class="inline-block mr-4 hover-opacity" aria-label="Instagram" href="https://instagram.com/sakraforum" target="_blank">
1052                <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24">
1053                  <path fill="currentColor" d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"></path>
1054                </svg>
1055              </a>
1056            </div>
1057  
1058          </div>
1059        </div>
1060  
1061        <!-- Newsletter signup -->
1062  
1063      </div>
1064  
1065      <div>
1066        <!-- Payment methods -->
1067        <div data-qb-setting="use_footer_paymentmethods" class="flex justify-center items-center gap-size flex-wrap mt-10">
1068  
1069  
1070  
1071          <img loading="lazy" class="h-10 p-2 w-auto bg-white/10 rounded-md" src="/assets/shopassets/paylogos/payment-visa-blue.svg" width="70" height="23" alt="Payment with Visa">
1072  
1073          <img loading="lazy" class="h-10 p-2 w-auto bg-white/10 rounded-md" src="/assets/shopassets/paylogos/payment-mastercard.svg" width="37" height="23" alt="Payment with Mastercard">
1074  
1075          <img loading="lazy" class="h-10 p-2 w-auto bg-white/10 rounded-md" src="/assets/shopassets/paylogos/payment-klarna.svg" width="41" height="23" alt="Payment with Klarna">
1076  
1077  
1078  
1079  
1080  
1081          <img loading="lazy" class="h-10 p-2 w-auto bg-white/10 rounded-md" src="/assets/shopassets/paylogos/payment-nets.svg" width="77" height="23" alt="Payment with Nets">
1082  
1083  
1084  
1085  
1086  
1087          <img loading="lazy" class="h-10 p-2 w-auto bg-white/10 rounded-md" src="/assets/shopassets/paylogos/payment-swish.png" width="75" height="23" alt="Payment with Swish">
1088  
1089        </div>
1090      </div>
1091    </div>
1092  
1093    <div>
1094      <hr class="border-foreground opacity-10">
1095      <div class="container mx-auto">
1096        <div class="py-8 text-center">
1097          <p class="text-footer text-sm opacity-50">
1098            <!-- Copyright -->
1099            © 2026 Säkra forum
1100          </p>
1101        </div>
1102      </div>
1103    </div>
1104  </section>
1105  <div x-data x-cloak
1106    x-show="$store.cart.cartOpen"
1107    @keydown.escape="$store.cart.hideCart()"
1108    @pageshow.window="$store.cart.init()"
1109    class="navbar-menu fixed top-0 right-0 bottom-0 w-full sm:max-w-md z-50">
1110  
1111    <!-- Overlay -->
1112    <div
1113      x-show='$store.cart.cartOpen'
1114      @click='$store.cart.hideCart()'
1115      x-transition:enter='transition ease-out duration-default transform'
1116      x-transition:enter-start='opacity-0'
1117      x-transition:enter-end='opacity-100'
1118      x-transition:leave='transition ease-in duration-default transform'
1119      x-transition:leave-start='opacity-100'
1120      x-transition:leave-end='opacity-0'
1121      class='fixed inset-0 transition-opacity bg-overlay bg-opacity-40'
1122      aria-hidden='true'
1123    ></div>
1124    
1125    <!-- Cart -->
1126    <div
1127      x-show='$store.cart.cartOpen'
1128      @keydown.escape="$store.cart.hideCart()"
1129      class="relative flex flex-col h-full bg-body"
1130      x-transition:enter='transition ease-out duration-default transform'
1131      x-transition:enter-start='opacity-0 translate-x-10 sm:translate-x-10'
1132      x-transition:enter-end='opacity-100 translate-x-0'
1133      x-transition:leave='transition ease-in duration-default transform'
1134      x-transition:leave-start='opacity-100 translate-x-0'
1135      x-transition:leave-end='opacity-0 translate-x-10 sm:translate-x-10'
1136    >
1137      <header class="flex items-center justify-between p-6">
1138        
1139        <!-- Title -->
1140        <span class="items-center font-heading text-h3">
1141          Varukorg
1142        </span>
1143  
1144        <div class="flex">
1145  
1146          <!-- Close button -->
1147          <button
1148            @click="$store.cart.hideCart()"
1149            class="flex cart-close cursor-pointer py-2 px-4 -mr-4 focus:outline-none hover-opacity"
1150            type="button"
1151            aria-label="Stäng"
1152          >
1153            <span>Stäng</span>
1154            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 ml-2">
1155              <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
1156            </svg>
1157          </button>
1158  
1159        </div>
1160  
1161      </header>
1162  
1163      <!-- Cart contents -->
1164      <div class="h-full p-6 overflow-y-auto">
1165  
1166        <!-- Cart messages -->
1167        <div class="qs-cart-message"></div>
1168  
1169        <!-- Empty cart snippet-->
1170        <!-- In case of empty cart - display this snippet + remove .d-none-->
1171        <div class="hidden qs-cart-empty flex items-center h-full  opacity-50 text-center">
1172          <div>
1173            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="inline-block w-16 h-16 mb-6">
1174              <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"></path>
1175            </svg>
1176            <p>
1177              Varukorgen är tom
1178            </p>
1179          </div>
1180        </div>
1181  
1182        <!-- Cart items -->
1183        <div class="grid grid-cols-1 divide-y divide-gray-400/20">
1184  
1185          <template
1186            x-for="product in $store.cart.products"
1187            :key="`${product.item.id}-${product.qty}-${product.uid}`"
1188          >
1189            <div class="bg-card shadow-default rounded-md  py-4">
1190              
1191              <div class="flex w-full">
1192  
1193                <!-- Product image -->
1194                <div class="flex shrink-0 justify-center items-start pr-4 w-36">
1195                  <a :href="product.item.url">
1196                    <img loading="lazy" :src="product.item.firstimage + '?w=150&h=150'" class="object-contain object-center w-full h-full rounded-md" :alt="product.item.title">
1197                  </a>
1198                </div>
1199                <div class="flex flex-1 flex-col overflow-hidden">
1200  
1201                  <div class="flex flex-col">
1202  
1203                    <!-- Product title -->
1204                    <a :href="product.item.url">
1205                      <h4 class="font-body font-bold" x-text="product.item.title"></h4>
1206                    </a>
1207                    
1208                    <!-- Product variant -->
1209                    <template x-if="product.variant_name">
1210                      <span x-text="product.variant_name" class="text-sm opacity-50"></span>
1211                    </template>
1212  
1213                    <!-- Product propertiers -->
1214                    <template x-if="product.properties">
1215                      <template x-for="property in Object.values(product.properties)">
1216                        <small class="truncate text-ellipsis" x-text="property"></small>
1217                      </template>
1218                    </template>
1219  
1220                  </div>
1221  
1222                  <!-- Current price -->
1223                  <span class="text-sm">
1224                    <span
1225                      :class="product.item.has_discount ? 'text-discounted-price' : ''"
1226                      x-text="product.item.price"
1227                    ></span>
1228  
1229                    <!-- Original price (only show if has discount) -->
1230                    <template x-if="product.item.has_discount">
1231                      <span
1232                        class="opacity-50 line-through ml-1"
1233                        x-text="product.item.original_price"
1234                      ></span>
1235                    </template>            
1236                  </span>
1237  
1238                  <div class="flex flex-1 grow-1">
1239                    <div class="flex self-end flex-1 justify-between"
1240                      x-data="{
1241                        quantity: product.qty,
1242                      }"
1243                      x-init="$watch('quantity',  Alpine.debounce((value) => {
1244                        if (value && value > 0) {
1245                          $store.cart.update({
1246                            productID: product.item.id, 
1247                            quantity: value
1248                          })
1249                        }
1250                      }, 500))"
1251                    >
1252                      <div class="flex input-bg input-text border input-border input-rou
1252nding overflow-hidden">
1253  
1254                        <!-- Decrease quantity -->
1255                        <button
1256                          type="button"
1257                          class="w-8 font-bold cursor-pointer input-bg input-text focus:outline-none hover:bg-gray-100 py-1 px-2"
1258                          @click="quantity = Math.max(1, quantity - 1)"
1259                        >
1260                          -
1261                        </button>
1262  
1263                        <!-- Quantity input -->
1264                        <input
1265                          required
1266                          min="1"
1267                          step="1"
1268                          type="number"
1269                          class="w-12 qs-cart-qty navbar-cart-product-qty input-bg input-text border-0 input-items text-center focus:outline-none"
1270                          @input="quantity = Math.floor(Number($event.target.value))"
1271                          x-model="quantity"
1272                        >
1273  
1274                        <!-- Increase quantity -->
1275                        <button
1276                          type="button"
1277                          class="w-8 font-bold cursor-pointer input-bg input-text focus:outline-none hover:bg-gray-100 py-1 px-2"
1278                          @click="quantity = Number(quantity) + 1"
1279                        >
1280                          +
1281                        </button>
1282                      </div>
1283  
1284                      <!-- Remove from cart -->
1285                      <button
1286                        type="button"
1287                        class="font-medium text-baseThird qs-cart-delete py-2 px-4 -mr-2 hover:text-red-700"
1288                        data-pid=""
1289                        @click="$store.cart.delete({
1290                          productID: product.item.id,
1291                        })"
1292                      >
1293                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
1294                          <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
1295                        </svg>
1296                      </button>
1297                    </div>
1298                  </div>
1299  
1300                </div>
1301              </div>
1302  
1303            </div>
1304          </template>
1305          
1306        </div>
1307      </div>
1308  
1309      <!-- Footer -->
1310      <footer class="mt-auto p-8 bg-body">
1311  
1312        <!-- Total discount (only show if there's a discount) -->
1313        <div class="flex items-end mb-2" x-show="$store.cart.totalDiscountRaw && $store.cart.totalDiscountRaw > 0">
1314          <div class="flex-1">
1315            <span class="text-sm font-medium text-discounted-price">Rabatt</span>
1316          </div>
1317          <div class="flex-1 text-right">
1318            <span
1319              class="text-sm font-medium text-discounted-price"
1320              x-text="'-' + $store.cart.totalDiscount"
1321            ></span>
1322          </div>
1323        </div>
1324        
1325        <div class="flex items-end mb-4">
1326          <div class="w-8/12">
1327            <span class="text-sm font-bold">Total summa
1328          
1329              <!-- Tax -->
1330  
1331          </div>
1332          <div class="w-6/12 text-right">
1333            
1334            <!-- Cart total amount -->
1335            <span
1336              class="text-lg font-bold text-primary"
1337              x-text="$store.cart.totalAmount"
1338            ></span>
1339          </div>
1340        </div>
1341        
1342        <!-- Buttons -->
1343        <div>
1344          <a href="/cart/index" class="block button-px button-py mb-2 text-center text-white font-bold bg-primary hover:bg-gray-900 button-rounding focus:outline-none">
1345            Till kassan
1346          </a>
1347          <button @click='$store.cart.hideCart()' class="block w-full button-px button-py text-center text-primary hover:text-body button-rounding focus:outline-none">
1348            Fortsätt att handla
1349          </button>
1350        </div>
1351      </footer>
1352  
1353    </div>
1354  </div>
1355  <section 
1356    x-data
1357    x-cloak
1358    x-show="$store.modal.modalVisible"
1359    x-transition.opacity
1360    class="fixed inset-0 py-6 sm:py-24 bg-black bg-opacity-50 z-50 h-full overflow-y-auto">
1361      <div class="container mx-auto relative">
1362      <div x-on:click.outside="$store.modal.closeModal()" :style="{'background-color': $store.modal.backgroundColor || '#fff'}"
1363      class="relative max-w-xl mx-auto text-center py-12 md:py-16 px-6 md:px-14 roun
1363ded-md">
1364        <a class="absolute top-4 right-4 inline-block ml-auto cursor-pointer hover-opacity" x-on:click="$store.modal.closeModal()" href="#" rel="nofollow">
1365          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
1366            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
1367          </svg>
1368        </a>
1369        <div x-html="$store.modal.content"></div>
1370      </div>
1371    </div>
1372  </section>
1373
1374  <svg width="0" height="0" style="display:none">
1375    <symbol xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" id="icon--building">
1376      <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z" />
1377    </symbol>
1378  
1379    <symbol xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" id="icon--card">
1380      <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" />
1381    </symbol> 
1382  
1383    <symbol xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" id="icon--chat">
1384      <path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
1385    </symbol> 
1386  
1387    <symbol xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" id="icon--people">
1388      <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
1389    </symbol> 
1390  
1391    <symbol xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" id="icon--store">
1392      <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 21v-7.5a.75.75 0 01.75-.75h3a.75.75 0 01.75.75V21m-4.5 0H2.36m11.14 0H18m0 0h3.64m-1.39 0V9.349m-16.5 11.65V9.35m0 0a3.001 3.001 0 003.75-.615A2.993 2.993 0 009.75 9.75c.896 0 1.7-.393 2.25-1.016a2.993 2.993 0 002.25 1.016c.896 0 1.7-.393 2.25-1.016a3.001 3.001 0 003.75.614m-16.5 0a3.004 3.004 0 01-.621-4.72L4.318 3.44A1.5 1.5 0 015.378 3h13.243a1.5 1.5 0 011.06.44l1.19 1.189a3 3 0 01-.621 4.72m-13.5 8.65h3.75a.75.75 0 00.75-.75V13.5a.75.75 0 00-.75-.75H6.75a.75.75 0 00-.75.75v3.75c0 .415.336.75.75.75z" />
1393    </symbol> 
1394  
1395    <symbol xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" id="icon--truck">
1396      <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 00-10.026 0 1.106 1.106 0 00-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12" />
1397    </symbol> 
1398  
1399    <symbol xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" id="icon--verified">
1400      <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
1401    </symbol> 
1402  
1403    <symbol xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 61 92" stroke-width="1.5" stroke="currentColor" id="icon--marker">
1404      <path fill-rule="evenodd" d="M33.04 89.474s27.607-35.842 27.607-54.635c0-18.792-13.431-34.027-30-34.027-16.568 0-30 15.235-30 34.027 0 18.793 27.621 54.652 27.621 54.652 1.314 1.764 3.456 1.765 4.771-.017Zm-2.393-43.737c8.285 0 15-6.704 15-14.975 0-8.27-6.715-14.975-15-14.975-8.284 0-15 6.705-15 14.975s6.716 14.975 15 14.975Z"/>
1405    </symbol> 
1406  
1407    <symbol xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 1200 1200" stroke-width="1.5" stroke="currentColor" id="icon--marker-2">
1408      <path d="M617.65 0c233.9 0 412.94 176.3 412.94 410.21v13.316c0 308.25-272.78 691.77-420 776.47h-3.531c-147.22-84.707-437.65-468.23-437.65-776.47v-13.312c0-233.92 196.69-410.22 430.59-410.22h17.648z"/>
1409      <path d="M616.77 28.234c222.21 0 385.58 169.26 385.58 391.47v12.648c0 292.83-252.44 654.71-392.3 739.41h-3.348c-139.85-84.707-409.05-446.59-409.05-739.41v-12.648c0-222.21 180.14-391.47 402.35-391.47h16.773z"/>
1409
1410    </symbol> 
1411  
1412    <symbol xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 -1 11 29" stroke-width="1.5" stroke="currentColor" id="icon--marker-3">
1413      <path d="M6 26V10.975A5.499 5.499 0 0 0 5.5 0 5.5 5.5 0 0 0 5 10.975V26h1zM3 5.5c0 .171.018.338.051.5h-1C2.026 5.835 2 5.671 2 5.5c0-1.759 1.309-3.204 3-3.449v1C3.86 3.283 3 4.292 3 5.5z"/>
1414    </symbol>
1415    
1416    <symbol xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" id="icon--chevron-left">
1417      <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
1418    </symbol>
1419    
1420    <symbol xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" id="icon--chevron-right">
1421      <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
1422    </symbol>
1423  </svg>
vendor: 109 bytes, lines 1423-1424
1423
1424<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script>
1424<script type="text/javascript"> var recaptchaSiteKey="6Lc-2GcUAAAAAMXG8Lz5S_MpVshTUeESW6iJqK0_"; </script>
1424<script type="text/javascript">window.registerApp({
1425  name: 'ProductProperties',
1426  identifier: 'QuickbutikProductProperties',
1427  description: 'Show product properties',
1428  version: '1.1.0',
1429  pages: ['product'],
1430  data: {},
1431  config: {
1432    insertTarget: {
1433      selector: '#product-properties',
1434      position: 'afterbegin', // beforebegin | afterbegin | beforeend | afterend
1435    },
1436    fieldsAttributes: {
1437      fileInputAccept: 'image/*,application/pdf',
1438    },
1439    styling: {
1440      formClasses: 'flex gap-4 flex-col pb-4',
1441      inputContainerClasses: 'flex flex-col gap-2',
1442      labelClasses: 'text-sm font-bold text-gray-700',
1443      labelErrorClasses: 'ml-1 text-red-500',
1444      checkboxInputClasses: 'form-checkbox h-5 w-5 text-gray-600',
1445      shortTextInputClasses:
1446        'block w-full p-3 text-xs text-black placeholder-gray-500 transition-all bg-white border border-gray-200 focus:border-gray-700 focus:outline-none',
1447      longTextInputClasses:
1448        'block w-full p-3 text-xs text-black placeholder-gray-500 transition-all bg-white border border-gray-200 focus:border-gray-700 focus:outline-none',
1449      fileContainerClasses: 'flex w-full input-bg input-border input-px input-py input-rounding w-full',
1450      fileInputClasses: 'input-bg input-border input-rounding input-px input-py',
1451      fileInputButtonClasses: 'shrink-0 cursor-pointer bg-primary text-center button-text-primary font-bold button-py button-px button-rounding mr-4 transition duration-100 hover:opacity-70',
1452      fileInputFilenameClasses: 'truncate button-py',
1453      fileUploadProgressContainerClasses: 'block relative w-full mt-2 rounded',
1454      fileUploadProgressBarBackgroundClasses: 'block absolute w-full h-full bg-black opacity-10 text-white rounded',
1455      fileUploadProgressBarClasses: 'block relative bg-primary py-1 px-4 text-xs font-bold text-white rounded',
1456      addonPriceClasses: 'text-sm text-gray-600 ml-0',
1457    },
1458  },
1459  onMount: function ({ appData, page }) {
1460    console.debug(`${this.identifier}@onMount`)
1461
1462    const productID = document
1463      .querySelector('[data-product-id]')
1464      .getAttribute('data-product-id')
1465
1466    if (productID) {
1467      fetch(`${qs_store_url}/apps/productproperties/view?product_id=${productID}`)
1468        .then((response) => response.json())
1469        .then((data) => {
1470          this.data = data
1471          this.renderProductProperties()
1472        })
1473        .catch((error) => {
1474          console.error('Error:', error);
1475        })
1476    }
1477
1478  },
1479  generageInputId: function (id) {
1480    return `ProductProperties-${id}`
1481  },
1482  generateInputName: function (id) {
1483    return `product_properties_${id}`
1484  },
1485  renderProductProperties: function () {
1486    const productPropertiesContainer = document.querySelector(this.data.identifier)
1487
1488    const insertPosition = {
1489      before: 'beforebegin',
1490      after: 'afterend',
1491      in: 'afterbegin',
1492    }[this.data.apply]
1493
1494    if (productPropertiesContainer && insertPosition && this.data.properties) {
1495      const form = document.createElement('form')
1496      form.classList.add(...this.config.styling.formClasses.split(' '))
1497      form.setAttribute('id', 'ProductPropertiesForm')
1498
1499      this.data.properties.forEach((property) => {
1500        const [key] =
1501          Object.entries(property)
1502            .filter(([key]) =>
1503              ['isCheckbox', 'isFile', 'isLongtext', 'isShorttext', 'isAddonProduct'].includes(
1504                key
1505              )
1506            )
1507            .find(([_, value]) => value) || []
1508
1509        let input = null
1510
1511        switch (key) {
1512          case 'isCheckbox':
1513            input = this.renderCheckbox(property)
1514            break
1515          case 'isFile':
1516            input = this.renderFile(property)
1517            break
1518          case 'isLongtext':
1519            input = this.renderLongtext(property)
1520            break
1521          case 'isShorttext':
1522            input = this.renderShorttext(property)
1523            break
1524          case 'isAddonProduct':
1525            input = this.renderAddonProduct(property)
1526            break
1527          default:
1528            console.log('No valid type found')
1529        }
1530
1531        if (input) {
1532          const inputContainer = document.createElement('div')
1533          inputContainer.classList.add(
1534            ...this.config.styling.inputContainerClasses.split(' ')
1535          )
1536          inputContainer.classList.add('product-property-container') // Add a class for easier selection
1537          form.appendChild(inputContainer)
1538
1539          // Only add label for non-addon products and checkboxes
1540          if (!['isAddonProduct', 'isCheckbox'].includes(key)) {
1541            
1542            // Label
1543            const label = document.createElement('label')
1544            label.classList.add(...this.config.styling.labelClasses.split(' '))
1545
1546            // File element
1547            if (key == 'isFile') {
1548              label.setAttribute('for', 'file-' + property.id)
1549            } else {
1550              label.setAttribute('for', this.generageInputId(property.id))
1551            }
1552            label.innerText = property.title
1553
1554            // Add addon price display next to the property title
1555            if (property.addonPriceRaw && parseFloat(property.addonPriceRaw) > 0) {
1556              const addonPriceSpan = document.createElement('span')
1557              addonPriceSpan.classList.add(...this.config.styling.addonPriceClasses.split(' '))
1558              addonPriceSpan.classList.add('addon-price') // Add a class for easier selection
1559              addonPriceSpan.innerText = ` (+${property.addonPrice})`
1560              addonPriceSpan.setAttribute('data-addon-price', property.addonPriceRaw)
1561              label.appendChild(addonPriceSpan)
1562            }
1563
1564            if (property.required === '1') {
1565              const requiredStar = document.createElement('span')
1566              requiredStar.classList.add(...this.config.styling.labelErrorClasses.split(' '))
1567              requiredStar.innerText = '*'
1568              label.appendChild(requiredStar)
1569            }
1570            inputContainer.appendChild(label)
1571          }
1572
1573          // All elements except files and addon products, which need specific structure and attributes
1574          if (key !== 'isFile' && key !== 'isAddonProduct') {
1575            input.setAttribute('id', this.generageInputId(property.id))
1576            input.setAttribute('name', this.generateInputName(property.id))
1577            input.setAttribute('data-qb-property', property.id)
1578
1579            if (property.required === '1') {
1580              input.setAttribute('required', true)
1581            }
1582          }
1583
1584          inputContainer.appendChild(input)
1585
1586          form.appendChild(inputContainer)
1587        }
1588      })
1589
1590      productPropertiesContainer.insertAdjacentElement(insertPosition, form)
1591
1592      // Only render ReCaptcha if we have a File property
1593      if (this.data.hasFileProperties) {
1594        this.renderReCaptcha()
1595      }
1596    }
1597  },
1598  renderCheckbox: function (property) {
1599    const checkboxWrapper = document.createElement('div')
1600    checkboxWrapper.classList.add('flex', 'items-center')
1601
1602    const checkbox = document.createElement('input')
1603    checkbox.classList.add(
1604      ...this.config.styling.checkboxInputClasses.split(' ')
1605    )
1606    checkbox.setAttribute('type', 'checkbox')
1607    checkbox.setAttribute('name', this.generateInputName(property.id))
1608    checkbox.setAttribute('value', '1')
1609    checkbox.setAttribute('data-qb-property', property.id)
1610
1611    // Label
1612    const label = document.createElement('label')
1613    label.classList.add(...this.config.styling.labelClasses.split(' '))
1614    label.classList.add('ml-4')
1615    label.setAttribute('for', this.generageInputId(property.id))
1616    label.innerText = property.title
1617
1618    if (property.required === '1') {
1619      const requiredStar = document.createElement('span')
1620      requiredStar.classList.add(...this.config.styling.labelErrorClasses.split(' '))
1621      requiredStar.innerText = '*'
1622      label.appendChild(requiredStar)
1623
1624      checkbox.setAttribute('required', true)
1625    }
1626
1627    checkboxWrapper.appendChild(checkbox)
1628    checkboxWrapper.appendChild(label)
1629
1630    return checkboxWrapper
1631  },
1632  renderAddonProduct: function (property) {
1633    const addonContainer = document.createElement('div')
1634    addonContainer.classList.add('addon-product-container', 'flex', 'items-center', 'p-4', 'border', 'rounded-md')
1635
1636    // Checkbox
1637    const checkbox = document.createElement('input')
1638    checkbox.classList.add(
1639      ...this.config.styling.checkboxInputClasses.split(' '),
1640      'mr-4'
1641    )
1642    checkbox.setAttribute('type', 'checkbox')
1643    checkbox.setAttribute('name', this.generateInputName(property.id))
1644    checkbox.setAttribute('value', '1')
1645    checkbox.setAttribute('data-qb-property', property.id)
1646
1647    if (property.required === '1') {
1648      checkbox.setAttribute('required', true)
1649    }
1650
1651    // Add checkbox to the container first
1652    addonContainer.appendChild(checkbox)
1653
1654    // Image
1655    if (property.addonProductImage) {
1656      const imageContainer = document.createElement('div')
1657      imageContainer.classList.add('w-16', 'h-16', 'mr-4')
1658
1659      const image = document.createElement('img')
1660      image.src = property.addonProductImage
1661      image.alt = property.addonProductTitle || property.title
1662      image.classList.add('w-full', 'h-full', 'object-cover', 'rounded')
1663
1664      imageContainer.appendChild(image)
1665      addonContainer.appendChild(imageContainer)
1666    }
1667
1668    // Content (Title, Price, Link)
1669    const contentContainer = document.createElement('div')
1670    contentContainer.classList.add('flex', 'flex-col', 'flex-grow')
1671
1672    const titleElement = document.createElement('span')
1673    titleElement.classList.add('font-bold', 'mb-1')
1674    titleElement.textContent = property.addonProductTitle || property.title
1675
1676    const priceElement = document.createElement('span')
1677    priceElement.classList.add('text-sm', 'text-gray-600')
1678    priceElement.textContent = '+' + (property.addonProductPrice || property.addonPrice)
1679
1680    contentContainer.appendChild(titleElement)
1681    contentContainer.appendChild(priceElement)
1682
1683    if (property.addonProductLink) {
1684      const link = document.createElement('a')
1685      link.href = property.addonProductLink
1686      link.target = '_blank'
1687      link.classList.add('text-blue-600', 'hover:underline', 'text-sm', 'mt-1')
1688      link.textContent = property.addonProductLinkText || 'Read More'
1689      contentContainer.appendChild(link)
1690    }
1691
1692    // Add content container last
1693    addonContainer.appendChild(contentContainer)
1694
1695    return addonContainer
1696  },
1697  renderFile: function (property) {
1698
1699    // Wrapper
1700    const fileContainer = document.createElement('div')
1701    fileContainer.classList.add(...this.config.styling.fileContainerClasses.split(' '))
1702
1703    // Input
1704    const file = document.createElement('input')
1705    file.classList.add(...this.config.styling.fileInputClasses.split(' '))
1706    file.classList.add('qs-cart-product-file')
1707    file.setAttribute('id', 'file-' + property.id)
1708    file.setAttribute('type', 'file')
1709    file.setAttribute('name', 'product_fields_files[' + property.id + ']')
1710    file.setAttribute('accept', this.config.fieldsAttributes.fileInputAccept)
1711    if (property.required === '1') {
1712      file.setAttribute('required', true)
1713      file.setAttribute('data-is-required', true)
1714    }
1715    file.style.display = 'none'
1716
1717    file.setAttribute('data-qb-property', property.id)
1718    fileContainer.append(file)
1719
1720    const button = document.createElement('label')
1721    button.setAttribute('for', 'file-' + property.id)
1722    button.classList.add(...this.config.styling.fileInputButtonClasses.split(' '))
1723    button.innerText = this.data.texts.chooseFile
1724    file.addEventListener('change', function () {
1725      filename.innerText = file.files[0].name
1726    })
1727
1728    fileContainer.append(button)
1729
1730    const filename = document.createElement('span')
1731    filename.innerText = this.data.texts.noFileChosen
1732    filename.classList.add(...this.config.styling.fileInputFilenameClasses.split(' '))
1733    fileContainer.append(filename)
1734
1735    return fileContainer
1736  },
1737  renderLongtext: function (property) {
1738    const longtext = document.createElement('textarea')
1739    longtext.classList.add(
1740      ...this.config.styling.longTextInputClasses.split(' ')
1741    )
1742    longtext.setAttribute('name', property.name)
1743    longtext.setAttribute('maxlength', 250)
1744
1745    return longtext
1746  },
1747  renderShorttext: function (property) {
1748    const shorttext = document.createElement('input')
1749    shorttext.classList.add(
1750      ...this.config.styling.shortTextInputClasses.split(' ')
1751    )
1752    shorttext.setAttribute('type', 'text')
1753    shorttext.setAttribute('name', property.name)
1754    shorttext.setAttribute('maxlength', 250)
1755
1756    return shorttext
1757  },
1758  renderReCaptcha: function () {
1759
1760    /* PRODUCT FIELD FILE UPLOAD START */
1761    if (document.readyState === "complete" || document.readyState === "interactive") {
1762
1763      const propertiesForm = document.querySelector('#ProductPropertiesForm')
1764
1765      if (!propertiesForm) {
1766        console.error('Could not find #ProductPropertiesForm')
1767        return false
1768      }
1769
1770      propertiesForm.dataset.prevent = '1'
1771
1772      var recaptchaDiv = document.createElement('div')
1773      recaptchaDiv.setAttribute('class', 'g-recaptcha')
1774      recaptchaDiv.setAttribute('data-sitekey', recaptchaSiteKey)
1775      recaptchaDiv.setAttribute('data-callback', 'recaptchaSubmit')
1776      recaptchaDiv.setAttribute('data-size', 'invisible')
1777      propertiesForm.prepend(recaptchaDiv)
1778
1779      var url = "https://www.google.com/recaptcha/api.js"
1780      var script = document.createElement('script')
1781      script.src = url
1782      script.type = 'text/javascript'
1783      document.getElementsByTagName('head')[0].appendChild(script)
1784    }
1785  },
1786  uploadFiles: function () {
1787    return new Promise((resolve, reject) => {
1788      const form = document.querySelector('#ProductPropertiesForm')
1789
1790      if (form && form.querySelectorAll('input[type="file"]').length > 0) {
1791        var filesToUpload = new FormData()
1792        Array.from(form.querySelectorAll('input[type="file"]')).forEach(function (tag) {
1793          Array.from(tag.files).forEach(function (file) {
1794            filesToUpload.append(tag.name, file)
1795          })
1796        })
1797
1798        var buy_btn_text = null, wait_btn_text = null
1799        var buybtn_selector = document.querySelector('.qs-cart-submit')
1800
1801        var xhr = new XMLHttpRequest()
1802        xhr.open('POST', '/cart/misc', true)
1803        xhr.setRequestHeader('cache-control', 'no-cache')
1804        xhr.responseType = 'json'
1805
1806        var app = this
1807
1808        xhr.onload = function () {
1809          if (xhr.status >= 200 && xhr.status < 300) {
1810            var data = xhr.response
1811
1812            // Reset button
1813            if (buy_btn_text && buy_btn_text.length !== 0) {
1814              buybtn_selector.value = buy_btn_text
1815              buybtn_selector.innerHTML = buy_btn_text
1816            }
1817            buybtn_selector.removeAttribute('disabled')
1818
1819            // Remove progress bar
1820            if (form.querySelector('.productproperties-file-progress')) {
1821              form.querySelector('.productproperties-file-progress').remove()
1822            }
1823
1824            if (data?.properties) {
1825              if (data.properties.errors) {
1826                alert(data.properties.errors)
1827
1828                return false
1829              } else {
1830                data.properties.forEach(function (property, key) {
1831                  // document.querySelector('.product-fields-file:eq('+property.property_id+')').value = property.value
1832                })
1833
1834                document.querySelector('.qs-cart').dataset.prevent = 0
1835                document.querySelector('.qs-cart').dataset.fileuploadstatus = 1
1836                // document.querySelector('.qs-cart').submit()
1837
1838                resolve(true)
1839              }
1840            } else {
1841
1842              // Handle error
1843              console.error('No properties returned from file upload.')
1844              reject(false)
1845            }
1846          } else {
1847
1848            // Handle error
1849            console.error(xhr.statusText)
1850
1851            reject(false)
1852          }
1853        }
1854
1855        xhr.upload.onloadstart = function (e) {
1856
1857          // Progress bar
1858          if (form.querySelectorAll('.productproperties-file-progress').length == 0) {
1859
1860            // Container
1861            const progressContainer = document.createElement('div')
1862            if (app.config?.styling?.fileUploadProgressContainerClasses) {
1863              progressContainer.classList.add(...app.config.styling.fileUploadProgressContainerClasses.split(' '))
1864            }
1865            progressContainer.classList.add('progress', 'productproperties-file-progress')
1866
1867            // Background
1868            const progressBarBackground = document.createElement('div')
1869            if (app.config?.styling?.fileUploadProgressBarBackgroundClasses) {
1870              progressBarBackground.classList.add(...app.config.styling.fileUploadProgressBarBackgroundClasses.split(' '))
1871            }
1872            progressBarBackground.style.zIndex = 1
1873            progressContainer.appendChild(progressBarBackground)
1874
1875            // Bar
1876            const progressBar = document.createElement('div')
1877            if (app.config?.styling?.fileUploadProgressBarClasses) {
1878              progressBar.classList.add(...app.config.styling.fileUploadProgressBarClasses.split(' '))
1879              progressBar.innerText = "&nbsp;"
1880            }
1881            progressBar.style.zIndex = 2
1882            progressBar.classList.add('progress-bar')
1883            progressContainer.appendChild(progressBar)
1884
1885            form.querySelectorAll('input[type="file"]')[form.querySelectorAll('input[type="file"]').length - 1].parentNode.parentNode.insertAdjacentElement('beforeEnd', progressContainer)
1886          }
1887        }
1888
1889        xhr.upload.onloadend = function (e) { }
1890
1891        xhr.upload.onprogress = function (e) {
1892          var percentComplete = Math.ceil((e.loaded / e.total) * 100)
1893        }
1894
1895        xhr.upload.onprogress = function (evt) {
1896
1897          let progressBar
1898          if (form.querySelectorAll('.productproperties-file-progress').length == 0) {
1899            form.querySelectorAll('input[type="file"]')[0].insertAdjacentHTML('afterend', '<div class="progress productproperties-file-progress ' + app.config.styling.fileUploadProgressContainerClasses + '"><div class="progress-bar ' + app.config.styling.fileUploadProgressBarClasses + '"></div></div>')
1900            progressBar = form.querySelectorAll('.productproperties-file-progress')
1901          }
1902
1903          if (evt.lengthComputable) {
1904            var percentComplete = evt.loaded / evt.total
1905            percentComplete = Math.round(percentComplete * 100)
1906
1907
1908            let progressBarBar = form.querySelectorAll('.productproperties-file-progress')[0].querySelector('.progress-bar')
1909            progressBarBar.style.width = percentComplete + '%'
1910            progressBarBar.innerHTML = percentComplete + '%'
1911          }
1912        }
1913
1914
1915        xhr.onerror = function () {
1916          console.error(xhr.statusText)
1917        }
1918
1919        xhr.send(filesToUpload)
1920
1921        // output = false // set as false when this function is executed as sync.
1922      } else {
1923        return true
1924      }
1925
1926      // console.log('Returns: ' + output)
1927      // return output
1928    })
1929  },
1930
1931})
1932
1933function recaptchaSubmit(token) {
1934  fetch('/apps/productproperties/recaptcha', {
1935    method: 'POST',
1936    body: 'recaptchatoken=' + token,
1937    headers: {
1938      'Content-Type': 'application/x-www-form-urlencoded'
1939    }
1940  })
1941    .then((response) => response.json())
1942    .then((data) => {
1943      if (data.success) {
1944        document.querySelector('#ProductPropertiesForm').dataset.prevent = 0
1945
1946        // Verification successful. Upload the file and add the item to the cart
1947        window.apps.QuickbutikProductProperties.uploadFiles().then(async (uploadSuccess) => {
1948          Alpine.store('cart').addItemToCart(window.bodyFormData)
1949        })
1950
1951        try {
1952          // Reset after submit
1953          grecaptcha.reset()
1954        } catch (e) {
1955          console.log(e)
1956        }
1957
1958        document.querySelector('#ProductPropertiesForm').dataset.prevent = 1
1959
1960        return true
1961      } else {
1962        alert('ReCaptcha error: Please try again.')
1963      }
1964    })
1965}
1966
1967
1968
1969</script>
1969<script type="text/javascript">window.registerApp({
1970  name: "ProductReviews",
1971  identifier: "QuickbutikProductReviews",
1972  description: "Show product reviews",
1973  version: "1.3.0",
1974  pages: ["product", "list", "start"],
1975  data: {},
1976  config: {
1977    containerSelector: "#productReviews",
1978    listItemContainerSelector:
1979      '[data-qb-selector="product-apps-reviews-item-reviews"]',
1980    themeBasedReviews: true,
1981    appContainerSelector: '[data-qb-selector="product-apps-reviews"]',
1982    appContainerFormSelector: '[data-qb-selector="product-apps-reviews-form"]',
1983    appContainerStarsSelector:
1984      '[data-qb-selector="product-apps-reviews-total"]',
1985    appContainerButtonSelector:
1986      '[data-qb-selector="product-apps-reviews-write-review"]',
1987    appContainerReviewsSelector:
1988      '[data-qb-selector="product-apps-reviews-items"]',
1989    star: {
1990      path: "M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z",
1991      color: {
1992        active: "#F59E0B",
1993        inactive: "#E5E7EB",
1994      },
1995    },
1996    stars: {
1997      size: "w-5 h-5",
1998      productPageSize: "w-5 h-5",
1999    },
2000    styling: {
2001      articleWidth: 85,
2002      classes: {
2003        headerContainer: "flex items-center justify-between py-6 border-b",
2004        addButton:
2005          "inline-flex items-center justify-center w-12 h-12 border hover:border-gray-500",
2006        headerRatingContainer: "flex items-center gap-3",
2007        headerText: "text-xl font-bold font-heading",
2008        ratingCount: "ml-2",
2009        reviewsContainer: "mt-4",
2010        reviewsArticleContainer: "flex gap-4 overflow-hidden",
2011        article: "pb-12 border-b",
2012        articleNameContainer: "flex items-center mb-4 space-x-4",
2013        articleName: "space-y-2 font-medium",
2014        articleRatingContainer: "flex items-center mb-2",
2015        articleContent: "mb-2 text-gray-500 dark:text-gray-400",
2016        articleShowMoreButton:
2017          "text-xs text-xs text-gray-500 ml-2 cursor-pointer",
2018        replyContainer: "border mt-3 ml-3 p-3",
2019        replyText: "text-xs text-gray-500 dark:text-gray-400",
2020        starsContainer: "flex flex-col gap-4",
2021        form: "my-4 flex flex-col gap-6",
2022        formRatingDiv: "flex flex-col gap-4",
2023        formRatingLabel: "text-sm font-bold text-gray-700",
2024        formGroup: "flex flex-col gap-4",
2025        textInput:
2026          "block w-full p-3 text-xs text-black placeholder-gray-500 transition-all bg-white border border-gray-200 focus:border-gray-700 focus:outline-none",
2027        formSubmitButton:
2028          "block w-full cursor-pointer bg-primary text-center text-white font-bold py-5 px-8 uppercase transition duration-200 hover:opacity-70",
2029        formErrorMessageContainer: "text-red-500 text-sm font-bold",
2030        formThankYouMessageContainer: "flex flex-col gap-4 my-12",
2031        formThankYouMessageLabel: "text-sm font-bold text-gray-700",
2032      },
2033      gradientOverlay: `linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 80%)`,
2034      articleFade: {
2035        background:
2036          "linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1))",
2037        classes: "h-12",
2038      },
2039      articleMaxHeight: "100px",
2040    },
2041  },
2042
2043  onMount: function ({ appData, page }) {
2044    console.debug(`${this.identifier}@onMount`);
2045
2046    const bodyFormData = new FormData();
2047    let itemSelector = "";
2048
2049    // Respect backend flag: show_in_lists
2050    const normalizeShowInLists = (val) => {
2051      if (typeof val === "boolean") return val;
2052      if (val === 1 || val === "1" || val === "true") return true;
2053      if (val === 0 || val === "0" || val === "false") return false;
2054      return true; // default to true if missing
2055    };
2056    this.data.showInLists = normalizeShowInLists(appData?.productreviews?.show_in_lists);
2057    
2058    // Store review settings
2059    this.data.reviewSectionDescription = appData?.productreviews?.review_section_description || null;
2060    this.data.reviewTextPlaceholder = appData?.productreviews?.review_text_placeholder || null;
2061    
2062    // Set star color from settings (backend resolves "primary" to actual color)
2063    if (appData?.productreviews?.star_color) {
2064      this.config.star.color.active = appData.productreviews.star_color;
2065      this.injectStarColorStyles(appData.productreviews.star_color);
2066    }
2067
2068    if (["list", "start"].includes(page)) {
2069      itemSelector = '[data-qb-selector="product-item"]';
2070    } else if (page === "product") {
2071      // Main product + related product cards
2072      itemSelector = '[data-qb-selector="product"], [data-qb-selector="product-item"]';
2073    }
2074    // If needed, we could scope to only main product:
2075    // if (page === "product") { itemSelector = '[data-qb-selector="product"]'; }
2076
2077    // On product page, render main product UI from top-level meta
2078    if (page === "product") {
2079      const mainProductEl = document.querySelector('[data-qb-selector="product"]');
2080      const mainMeta = appData?.productreviews || {};
2081      const mainProductId = mainProductEl?.getAttribute("data-product-id");
2082      if (mainProductEl && mainProductId) {
2083        const mainProductData = {
2084          id: mainProductId,
2085          rating: mainMeta.rating,
2086          rating_decimal: mainMeta.rating_decimal,
2087          amountTotal: mainMeta.amountTotal,
2088          reviews: mainMeta.reviews,
2089          has_rated: mainMeta.has_rated,
2090        };
2091        this.renderProductReviews(mainProductData, page, mainProductEl);
2092      }
2093    }
2094
2095    if ((page === "list" || page === "start") && !this.data.showInLists) {
2096      return;
2097    }
2098    const existingProducts = appData?.productreviews?.products;
2099    if (Array.isArray(existingProducts) && existingProducts.length > 0) {
2100      if (page === "product" && !this.data.showInLists) {
2101        // Do not render related items when disabled
2102        return;
2103      }
2104      existingProducts.forEach((productData) => {
2105        let productQuerySelector = "";
2106        if (["list", "start"].includes(page)) {
2107          productQuerySelector = `[data-qb-selector="product-item"][data-product-id="${productData.id}"]`;
2108        } else if (page === "product") {
2109          // Only related product cards; main product handled from top-level meta above
2110          productQuerySelector = `[data-qb-selector="product-item"][data-product-id="${productData.id}"]`;
2111        }
2112
2113        document
2114          .querySelectorAll(productQuerySelector)
2115          .forEach((product) => {
2116            this.renderProductReviews(productData, page, product);
2117          });
2118      });
2119      return;
2120    }
2121
2122    // Get all product IDs on the page (same as productribbons)
2123    const productElements = document.querySelectorAll(itemSelector);
2124    if (productElements.length === 0) {
2125      return;
2126    }
2127
2128    let productIds = [...productElements]
2129      .map((product) => product.dataset.productId)
2130      .filter((id) => id && id !== "0");
2131
2132    // On product page, exclude main product ID (we render it from top-level meta)
2133    if (page === "product") {
2134      const mainProductId = document.querySelector('[data-qb-selector="product"]')?.getAttribute('data-product-id');
2135      if (mainProductId) {
2136        productIds = productIds.filter((id) => id !== mainProductId);
2137      }
2138    }
2139
2140    if (page === 'product' && !this.data.showInLists) {
2141      return;
2142    }
2143
2144    if (productIds.length === 0) {
2145      return;
2146    }
2147
2148    bodyFormData.set("products", productIds.join(","));
2149    bodyFormData.set("currentPage", document.body.dataset?.qbPage);
2150
2151    // Fetch reviews data from endpoint (same pattern as productribbons)
2152    fetch(qs_store_url + "/apps/productreviews/fetch", {
2153      method: "POST",
2154      body: bodyFormData,
2155    })
2156      .then((response) => response.json())
2157      .then((data) => {
2158        console.debug(
2159          `${this.identifier}: Received data for ${
2160            data?.products?.length || 0
2161          } products`
2162        );
2163
2164        // Store settings if fetched
2165        if (data?.review_section_description !== undefined) {
2166          this.data.reviewSectionDescription = data.review_section_description;
2167        }
2168        if (data?.review_text_placeholder !== undefined) {
2169          this.data.reviewTextPlaceholder = data.review_text_placeholder;
2170        }
2171        
2172        // Set star color from fetched settings (backend resolves "primary" to actual color)
2173        if (data?.star_color) {
2174          this.config.star.color.active = data.star_color;
2175          this.injectStarColorStyles(data.star_color);
2176        }
2177
2178        if (data?.products) {
2179          data?.products.forEach((productData) => {
2180            let productQuerySelector = "";
2181            if (["list", "start", ].includes(page)) {
2182              productQuerySelector = `[data-qb-selector="product-item"][data-product-id="${productData.id}"]`;
2183            } else if (page === "product") {
2184              // Only related product cards; main product handled from top-level meta
2185              productQuerySelector = `[data-qb-selector="product-item"][data-product-id="${productData.id}"]`;
2186            }
2187
2188            document
2189              .querySelectorAll(productQuerySelector)
2190              .forEach((product) => {
2191                this.renderProductReviews(productData, page, product);
2192              });
2193          });
2194        }
2195      })
2196      .catch((error) => {
2197        console.error(
2198          `${this.identifier}: Error fetching product reviews:`,
2199          error
2200        );
2201      });
2202  },
2203
2204  injectStarColorStyles: function (starColor) {
2205    // Check if style element already exists
2206    const existingStyle = document.getElementById('qb-review-star-styles');
2207    if (existingStyle) {
2208      // Update existing style
2209      existingStyle.textContent = `.qb-review-star-active { color: ${starColor} !important; }`;
2210    } else {
2211      // Create new style element
2212      const styleElement = document.createElement('style');
2213      styleElement.id = 'qb-review-star-styles';
2214      styleElement.textContent = `.qb-review-star-active { color: ${starColor} !important; }`;
2215      document.head.appendChild(styleElement);
2216    }
2217  },
2218
2219  renderProductReviews: function (productData, page, product) {
2220    const { rating } = productData;
2221
2222    if (this.config.themeBasedReviews) {
2223      // Only render full UI for the main product container on product pages
2224      if (page === 'product' && product?.matches?.('[data-qb-selector="product"]')) {
2225        this.renderThemeBasedProductReviews(productData, page);
2226      }
2227    } else {
2228      // Traditional mode: only render full UI for the main product container
2229      if (page === 'product' && product?.matches?.('[data-qb-selector="product"]')) {
2230        this.renderTraditionalProductReviews(productData, page);
2231      }
2232    }
2233
2234    // Always render product item ratings for list/start pages
2235    // On product page, render ratings for related product cards as well
2236    if ((["list", "start"].includes(page) || page === 'product') && rating > 0 && product?.matches?.('[data-qb-selector="product-item"]')) {
2237      this.renderProductItemRating(productData, product, page);
2238    }
2239  },
2240
2241  renderThemeBasedProductReviews: function (productData, page) {
2242    const { rating, rating_decimal, reviews, amountTotal } = productData;
2243
2244    // For product page, render full review interface
2245    if (page === "product") {
2246      // Total review score/stars (if any, in multiple places)
2247      if (rating > 0) {
2248        const containers = document.querySelectorAll(
2249          this.config.appContainerStarsSelector
2250        );
2251        if (containers) {
2252          containers.forEach((container) => {
2253            // Only add if not already populated
2254            if (container.children.length === 0) {
2255              container.appendChild(
2256                this.renderRating(parseFloat(rating_decimal ?? rating), page)
2257              );
2258              const reviewsCount = document.createElement("div");
2259              reviewsCount.classList.add(
2260                ...this.config.styling.classes.ratingCount.split(" ")
2261              );
2262              reviewsCount.textContent = "(" + (amountTotal || 0) + ")";
2263              container.appendChild(reviewsCount);
2264            }
2265          });
2266        }
2267      }
2268
2269      // Write review form
2270      const container = document.querySelector(
2271        this.config.appContainerFormSelector
2272      );
2273      if (container && container.children.length === 0) {
2274        // Extract language tags
2275        const language = container.getAttribute("data-language")
2276          ? JSON.parse(container.getAttribute("data-language"))
2277          : {};
2278
2279        container.appendChild(this.renderForm(language));
2280      }
2281
2282      // Reviews
2283      if (reviews && reviews.length > 0) {
2284        // Reviews container
2285        const reviewsContainer = document.querySelector(
2286          this.config.appContainerReviewsSelector
2287        );
2288        if (reviewsContainer) {
2289          // Add reviews to x-data
2290          reviewsContainer.setAttribute(
2291            "x-init",
2292            "reviews = " + JSON.stringify(reviews)
2293          );
2294        }
2295      }
2296    }
2297  },
2298
2299  renderTraditionalProductReviews: function (productData, page) {
2300    // Only render for product page in traditional mode
2301    if (page === "product") {
2302      const selector = this.config.containerSelector;
2303      const container = document.querySelector(selector);
2304
2305      if (container && container.children.length === 0) {
2306        container.appendChild(this.renderProductReviewsUI(productData, page));
2307      }
2308    }
2309  },
2310
2311  renderProductReviewsUI: function (productData, page) {
2312    const { rating, rating_decimal, has_rated, reviews } = productData;
2313
2314    const mainContainer = document.createElement("div");
2315    mainContainer.setAttribute("x-data", JSON.stringify({ showForm: false }));
2316
2317    const headerContainer = document.createElement("div");
2318    headerContainer.classList.add(
2319      ...this.config.styling.classes.headerContainer.split(" ")
2320    );
2321
2322    const headerRatingContainer = document.createElement("div");
2323    headerRatingContainer.classList.add(
2324      ...this.config.styling.classes.headerRatingContainer.split(" ")
2325    );
2326
2327    const h4 = document.createElement("h4");
2328    h4.textContent = "Reviews";
2329    h4.classList.add(...this.config.styling.classes.headerText.split(" "));
2330
2331    headerRatingContainer.appendChild(h4);
2332
2333    if (rating > 0) {
2334      headerRatingContainer.appendChild(
2335        this.renderRating(parseFloat(rating_decimal ?? rating), page)
2336      );
2337    }
2338
2339    const showRatingFormButton = document.createElement("a");
2340    showRatingFormButton.classList.add(
2341      ...this.config.styling.classes.addButton.split(" ")
2342    );
2343    showRatingFormButton.setAttribute("x-on:click", "showForm = !showForm");
2344    showRatingFormButton.id = "productReviewShowFormButton";
2345
2346    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2347    svg.setAttribute("width", "12");
2348    svg.setAttribute("height", "12");
2349    svg.setAttribute("viewBox", "0 0 12 12");
2350    svg.setAttribute("fill", "none");
2351
2352    const rect1 = document.createElementNS(
2353      "http://www.w3.org/2000/svg",
2354      "rect"
2355    );
2356    rect1.setAttribute("x", "5");
2357    rect1.setAttribute("width", "2");
2358    rect1.setAttribute("height", "12");
2359    rect1.setAttribute("fill", "#161616");
2360    svg.appendChild(rect1);
2361
2362    const rect2 = document.createElementNS(
2363      "http://www.w3.org/2000/svg",
2364      "rect"
2365    );
2366    rect2.setAttribute("x", "12");
2367    rect2.setAttribute("y", "5");
2368    rect2.setAttribute("width", "2");
2369    rect2.setAttribute("height", "12");
2370    rect2.setAttribute("transform", "rotate(90 12 5)");
2371    rect2.setAttribute("fill", "#161616");
2372    svg.appendChild(rect2);
2373
2374    showRatingFormButton.appendChild(svg);
2375
2376    headerContainer.appendChild(headerRatingContainer);
2377
2378    if (!has_rated) {
2379      headerContainer.appendChild(showRatingFormButton);
2380    }
2381
2382    mainContainer.appendChild(headerContainer);
2383    mainContainer.appendChild(this.renderForm());
2384
2385    if (reviews && reviews.length > 0) {
2386      const reviewsContainer = document.createElement("div");
2387      reviewsContainer.className = "relative";
2388      reviewsContainer.classList.add(
2389        ...this.config.styling.classes.reviewsContainer.split(" ")
2390      );
2391
2392      const reviewsArticleContainer = document.createElement("div");
2393      reviewsArticleContainer.className =
2394        this.config.styling.classes.reviewsArticleContainer;
2395      reviewsArticleContainer.style.overflowX = "auto";
2396      reviewsArticleContainer.style.scrollSnapType = "x mandatory";
2397
2398      reviews.forEach((review) => {
2399        reviewsArticleContainer.appendChild(this.renderReview(review));
2400      });
2401
2402      reviewsContainer.appendChild(reviewsArticleContainer);
2403
2404      if (reviews.length > 1) {
2405        reviewsArticleContainer.lastElementChild.style.marginRight = `${
2406          100 - this.config.styling.articleWidth
2407        }%`;
2408
2409        const gradientOverlay = document.createElement("div");
2410        gradientOverlay.style.position = "absolute";
2411        gradientOverlay.style.top = "0";
2412        gradientOverlay.style.bottom = "0";
2413        gradientOverlay.style.right = "0";
2414        gradientOverlay.style.width = `${
2415          100 - this.config.styling.articleWidth
2416        }%`;
2417        gradientOverlay.style.pointerEvents = "none";
2418        gradientOverlay.style.backgroundImage =
2419          this.config.styling.gradientOverlay;
2420
2421        reviewsContainer.appendChild(gradientOverlay);
2422      }
2423
2424      mainContainer.appendChild(reviewsContainer);
2425    }
2426
2427    return mainContainer;
2428  },
2429
2430  renderReview: function ({ content, name, rating, reply }) {
2431    const article = document.createElement("article");
2432    article.classList.add(...this.config.styling.classes.article.split(" "));
2433    article.style.scrollSnapAlign = "start";
2434    article.style.flex = "0 0 auto";
2435    article.style.width = `${
2436      this.data.reviews?.length > 1 ? this.config.styling.articleWidth : 100
2437    }%`;
2438
2439    const articleNameContainer = document.createElement("div");
2440    articleNameContainer.classList.add(
2441      ...this.config.styling.classes.articleNameContainer.split(" ")
2442    );
2443
2444    const articleName = document.createElement("div");
2445    articleName.classList.add(
2446      ...this.config.styling.classes.articleName.split(" ")
2447    );
2448    const p1 = document.createElement("p");
2449    p1.textContent = name;
2450    articleName.appendChild(p1);
2451    articleNameContainer.appendChild(articleName);
2452    article.appendChild(articleNameContainer);
2453
2454    const div3 = document.createElement("div");
2455    div3.classList.add(
2456      ...this.config.styling.classes.articleRatingContainer.split(" ")
2457    );
2458    div3.appendChild(this.renderRating(rating, "product"));
2459
2460    article.appendChild(div3);
2461
2462    const contentContainer = document.createElement("div");
2463    contentContainer.setAttribute(
2464      "x-data",
2465      `{
2466  showMore: false,
2467  isOverflowing(element) {
2468    return element.scrollHeight > element.clientHeight;
2469  }
2470}`
2471    );
2472
2473    const p2 = document.createElement("p");
2474    p2.className = "relative";
2475    p2.classList.add(...this.config.styling.classes.articleContent.split(" "));
2476
2477    p2.setAttribute("x-bind:class", '{ "overflow-hidden": !showMore }');
2478    p2.setAttribute(
2479      "x-bind:style",
2480      `{ maxHeight: showMore ? "none" : "${this.config.styling.articleMaxHeight}" }`
2481    );
2482
2483    p2.innerHTML = content;
2484
2485    const showMoreLink = document.createElement("a");
2486    showMoreLink.classList.add(
2487      ...this.config.styling.classes.articleShowMoreButton.split(" ")
2488    );
2489
2490    showMoreLink.setAttribute("x-text", 'showMore ? "Show less" : "Show more"');
2491    showMoreLink.setAttribute("x-show", "isOverflowing($refs.content)");
2492    showMoreLink.setAttribute("x-on:click", "showMore = !showMore");
2493    showMoreLink.setAttribute("x-ref", "showMoreLink");
2494
2495    const fadeOverlay = document.createElement("div");
2496    fadeOverlay.className =
2497      "absolute bottom-0 left-0 right-0  pointer-events-none";
2498    fadeOverlay.classList.add(
2499      ...this.config.styling.articleFade.classes.split(" ")
2500    );
2501    fadeOverlay.style.backgroundImage =
2502      this.config.styling.articleFade.background;
2503
2504    fadeOverlay.setAttribute(
2505      "x-show",
2506      "!showMore && isOverflowing($refs.content)"
2507    );
2508
2509    p2.appendChild(fadeOverlay);
2510
2511    contentContainer.appendChild(p2);
2512    p2.setAttribute("x-ref", "content");
2513    contentContainer.appendChild(showMoreLink);
2514    article.appendChild(contentContainer);
2515
2516    if (reply) {
2517      const aside = document.createElement("aside");
2518      aside.classList.add(
2519        ...this.config.styling.classes.replyContainer.split(" ")
2520      );
2521
2522      const p3 = document.createElement("p");
2523
2524      p3.classList.add(...this.config.styling.classes.replyText.split(" "));
2525      p3.textContent = reply;
2526      aside.appendChild(p3);
2527
2528      article.appendChild(aside);
2529    }
2530
2531    return article;
2532  },
2533  renderProductItemRating: function (productData, productItem, page) {
2534    const { id, rating, rating_decimal, amountTotal } = productData;
2535
2536    try {
2537      if (rating && parseFloat(rating) > 0) {
2538        const productItemRatingContainer = document.createElement("div");
2539
2540        const containerClass =
2541          this.config.styling.classes.productItemRatingContainer;
2542        if (containerClass && containerClass.trim() !== "") {
2543          productItemRatingContainer.classList.add(
2544            ...containerClass.split(" ")
2545          );
2546        }
2547
2548        // Create rating with review count
2549        const ratingElement = this.renderRating(
2550          parseFloat(rating_decimal ?? rating),
2551          page
2552        );
2553
2554        // Add review count in parentheses if available
2555        if (amountTotal && parseInt(amountTotal) > 0) {
2556          const reviewCountSpan = document.createElement("span");
2557          reviewCountSpan.classList.add(
2558            "review-count",
2559            "text-sm",
2560            "text-gray-600",
2561            "ml-2"
2562          );
2563          reviewCountSpan.textContent = `(${amountTotal})`;
2564
2565          const ratingWrapper = document.createElement("div");
2566          ratingWrapper.classList.add("flex", "items-center");
2567          ratingWrapper.appendChild(ratingElement);
2568          ratingWrapper.appendChild(reviewCountSpan);
2569
2570          productItemRatingContainer.appendChild(ratingWrapper);
2571        } else {
2572          productItemRatingContainer.appendChild(ratingElement);
2573        }
2574
2575        // Check if listItemContainerSelector is defined and not blank
2576        let targetContainer = productItem;
2577        if (
2578          this.config.listItemContainerSelector &&
2579          this.config.listItemContainerSelector.trim() !== ""
2580        ) {
2581          const listItemContainer = productItem.querySelector(
2582            this.config.listItemContainerSelector
2583          );
2584          if (listItemContainer) {
2585            targetContainer = listItemContainer;
2586          }
2587        }
2588
2589        // Only add if not already present (avoid duplicates)
2590        if (
2591          !targetContainer.querySelector(".product-reviews-rating") &&
2592          !targetContainer.querySelector(".review-count")
2593        ) {
2594          productItemRatingContainer.classList.add("product-reviews-rating");
2595          targetContainer.appendChild(productItemRatingContainer);
2596        }
2597      }
2598    } catch (error) {
2599      console.error(
2600        `${this.identifier}: Error processing product item ${id}:`,
2601        error
2602      );
2603    }
2604  },
2605  renderRating: function (rating, page = null) {
2606    const uniqueId = Math.floor(Math.random() * 100000);
2607
2608    const div = document.createElement("div");
2609    div.classList.add(...this.config.styling.classes.starsContainer.split(" "));
2610
2611    const ratingDiv = document.createElement("div");
2612    ratingDiv.className = "flex items-center";
2613
2614    // Use productPageSize if on product page, otherwise use default size
2615    const starSize = (page === "product") ? this.config.stars.productPageSize : this.config.stars.size;
2616
2617    for (let starIndex = 1; starIndex <= 5; starIndex++) {
2618      const gradientId = `rating-gradient-${uniqueId}-${starIndex}`;
2619
2620      const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2621      svg.setAttribute("aria-hidden", "true");
2622      svg.setAttribute("class", starSize);
2623      svg.setAttribute("viewBox", "0 0 20 20");
2624      svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2625
2626      const title = document.createElementNS(
2627        "http://www.w3.org/2000/svg",
2628        "title"
2629      );
2630      title.textContent = `Star ${starIndex}`;
2631      svg.appendChild(title);
2632
2633      const defs = document.createElementNS(
2634        "http://www.w3.org/2000/svg",
2635        "defs"
2636      );
2637      const linearGradient = document.createElementNS(
2638        "http://www.w3.org/2000/svg",
2639        "linearGradient"
2640      );
2641      linearGradient.setAttribute("id", gradientId);
2642
2643      const remainder = rating - starIndex + 1;
2644      const fillPercentage =
2645        remainder > 0 && remainder < 1
2646          ? remainder * 100
2647          : starIndex <= Math.floor(rating)
2648          ? 100
2649          : 0;
2650
2651      const stop1 = document.createElementNS(
2652        "http://www.w3.org/2000/svg",
2653        "stop"
2654      );
2655      stop1.setAttribute("offset", "0%");
2656      stop1.setAttribute("stop-color", this.config.star.color.active);
2657      linearGradient.appendChild(stop1);
2658
2659      const stop2 = document.createElementNS(
2660        "http://www.w3.org/2000/svg",
2661        "stop"
2662      );
2663      stop2.setAttribute("offset", fillPercentage + "%");
2664      stop2.setAttribute("stop-color", this.config.star.color.active);
2665      linearGradient.appendChild(stop2);
2666
2667      const stop3 = document.createElementNS(
2668        "http://www.w3.org/2000/svg",
2669        "stop"
2670      );
2671      stop3.setAttribute("offset", fillPercentage + "%");
2672      stop3.setAttribute("stop-color", this.config.star.color.inactive);
2673      linearGradient.appendChild(stop3);
2674
2675      const stop4 = document.createElementNS(
2676        "http://www.w3.org/2000/svg",
2677        "stop"
2678      );
2679      stop4.setAttribute("offset", "100%");
2680      stop4.setAttribute("stop-color", this.config.star.color.inactive);
2681      linearGradient.appendChild(stop4);
2682
2683      defs.appendChild(linearGradient);
2684      svg.appendChild(defs);
2685
2686      const path = document.createElementNS(
2687        "http://www.w3.org/2000/svg",
2688        "path"
2689      );
2690      path.setAttribute("fill", `url(#${gradientId})`);
2691      path.setAttribute("d", this.config.star.path);
2692      svg.appendChild(path);
2693
2694      ratingDiv.appendChild(svg);
2695    }
2696
2697    div.appendChild(ratingDiv);
2698
2699    return div;
2700  },
2701  renderForm: function (language) {
2702    const form = document.createElement("form");
2703
2704    form.id = "productReviewForm";
2705    form.style.position = "relative";
2706
2707    form.setAttribute(
2708      "x-on:submit.prevent",
2709      `window.apps.${this.identifier}.onSubmitReview()`
2710    );
2711
2712    form.classList.add(...this.config.styling.classes.form.split(" "));
2713    form.setAttribute("x-show", "showForm");
2714    form.setAttribute("x-transition", "");
2715    form.setAttribute("x-cloak", "");
2716
2717    // Add description if available
2718    if (this.data.reviewSectionDescription) {
2719      const descriptionP = document.createElement("p");
2720      descriptionP.className = "review-section-description text-sm text-gray-700 mb-4";
2721      descriptionP.textContent = this.data.reviewSectionDescription;
2722      form.appendChild(descriptionP);
2723    }
2724
2725    const ratingDiv = document.createElement("div");
2726    ratingDiv.classList.add(
2727      ...this.config.styling.classes.formRatingDiv.split(" ")
2728    );
2729
2730    const ratingLabel = document.createElement("label");
2731    ratingLabel.classList.add(
2732      ...this.config.styling.classes.formRatingLabel.split(" ")
2733    );
2734
2735    ratingLabel.textContent = language?.yourRating ?? "Your rating";
2736    ratingDiv.appendChild(ratingLabel);
2737
2738    ratingDiv.appendChild(this.renderRatingInput());
2739
2740    form.appendChild(ratingDiv);
2741
2742    const nameDiv = document.createElement("div");
2743    nameDiv.classList.add(...this.config.styling.classes.formGroup.split(" "));
2744
2745    const nameLabel = document.createElement("label");
2746    nameLabel.setAttribute("for", "reviewName");
2747    nameLabel.className = "text-sm font-bold text-gray-700";
2748    nameLabel.textContent = language?.name ?? "Name";
2749    nameDiv.appendChild(nameLabel);
2750
2751    const nameInput = document.createElement("input");
2752    nameInput.required = true;
2753    nameInput.type = "text";
2754    nameInput.id = "reviewName";
2755    nameInput.classList.add(
2756      ...this.config.styling.classes.textInput.split(" ")
2757    );
2758    nameDiv.appendChild(nameInput);
2759
2760    form.appendChild(nameDiv);
2761
2762    const emailDiv = document.createElement("div");
2763    emailDiv.classList.add(...this.config.styling.classes.formGroup.split(" "));
2764
2765    const emailLabel = document.createElement("label");
2766    emailLabel.setAttribute("for", "reviewEmail");
2767    emailLabel.className = "text-sm font-bold text-gray-700";
2768    emailLabel.textContent = language?.email ?? "Email";
2769    emailDiv.appendChild(emailLabel);
2770
2771    const emailInput = document.createElement("input");
2772    emailInput.required = true;
2773    emailInput.type = "email";
2774    emailInput.id = "reviewEmail";
2775    emailInput.classList.add(
2776      ...this.config.styling.classes.textInput.split(" ")
2777    );
2778    emailDiv.appendChild(emailInput);
2779
2780    form.appendChild(emailDiv);
2781
2782    const messageDiv = document.createElement("div");
2783    messageDiv.classList.add(
2784      ...this.config.styling.classes.formGroup.split(" ")
2785    );
2786
2787    const messageLabel = document.createElement("label");
2788    messageLabel.setAttribute("for", "reviewMessage");
2789    messageLabel.className = "text-sm font-bold text-gray-700";
2790    messageLabel.textContent = language?.message ?? "Message";
2791    messageDiv.appendChild(messageLabel);
2792
2793    const messageTextarea = document.createElement("textarea");
2794    messageTextarea.required = true;
2795    messageTextarea.id = "reviewMessage";
2796    messageTextarea.rows = 10;
2797    messageTextarea.classList.add(
2798      ...this.config.styling.classes.textInput.split(" ")
2799    );
2800    if (this.data.reviewTextPlaceholder) {
2801      messageTextarea.placeholder = this.data.reviewTextPlaceholder;
2802    }
2803    messageDiv.appendChild(messageTextarea);
2804
2805    form.appendChild(messageDiv);
2806
2807    const submitButton = document.createElement("button");
2808    submitButton.classList.add(
2809      ...this.config.styling.classes.formSubmitButton.split(" ")
2810    );
2811    submitButton.textContent = language?.submitReview ?? "Submit review";
2812    form.appendChild(submitButton);
2813
2814    const errorMessageContainer = document.createElement("div");
2815    errorMessageContainer.classList.add(
2816      ...this.config.styling.classes.formErrorMessageContainer.split(" ")
2817    );
2818    errorMessageContainer.id = "productReviewErrorMessage";
2819
2820    form.appendChild(errorMessageContainer);
2821
2822    return form;
2823  },
2824  renderRatingInput: function () {
2825    const ratingDiv = document.createElement("div");
2826    ratingDiv.className = "flex items-center mb-2";
2827    ratingDiv.setAttribute(
2828      "x-data",
2829      "{ currentRating: null, hoverRating: null }"
2830    );
2831
2832    // Use productPageSize since this is only used on product page forms
2833    const starSize = this.config.stars.productPageSize;
2834
2835    ratingDiv.innerHTML = `
2836<template x-for="starIndex in 5">
2837  <div x-id="['reviewStar']">
2838    <input required class="opacity-0 absolute -z-50" type="radio" :id="$id('reviewStar')" name="rating" :value="starIndex" x-model="currentRating" />
2839  
2840    <label 
2841      :for="$id('reviewStar')" 
2842      @mouseover="hoverRating = starIndex"
2843      @mouseleave="hoverRating = null"
2844    >
2845    <svg aria-hidden="true" 
2846      :style="{color: !hoverRating && currentRating >= starIndex || hoverRating >= starIndex ? '${this.config.star.color.active}' : '${this.config.star.color.inactive}'}"
2847      class="${starSize}" 
2848      fill="currentColor" 
2849      viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title x-text="['One star', 'Two stars', 'Three stars', 'Four stars', 'Five stars'][starIndex - 1]"></title><path d="${this.config.star.path}"></path>
2850    </svg>
2851
2852  </label>
2853  </div>
2854</template>
2855    `;
2856
2857    return ratingDiv;
2858  },
2859  onSubmitReview: function () {
2860    const errorMessageContainer = document.getElementById(
2861      "productReviewErrorMessage"
2862    );
2863
2864    try {
2865      this.submitReview();
2866      errorMessageContainer.textContent = "";
2867    } catch (error) {
2868      this.handleError(error);
2869    }
2870  },
2871  handleError: function (error) {
2872    console.error("QuickbutikProductReviews@handleError", error);
2873
2874    const errorMessageContainer = document.getElementById(
2875      "productReviewErrorMessage"
2876    );
2877
2878    const errorMessage = `An error occurred${
2879      error ? `: ${JSON.stringify(error, null, 2)}` : ""
2880    }.`;
2881
2882    errorMessageContainer.textContent = errorMessage;
2883  },
2884  submitReview: function () {
2885    const data = [
2886      {
2887        key: "product_id",
2888        value: document
2889          .querySelector("[data-product-id]")
2890          .getAttribute("data-product-id"),
2891      },
2892      {
2893        key: "name",
2894        value: document.getElementById("reviewName").value,
2895      },
2896      {
2897        key: "email",
2898        value: document.getElementById("reviewEmail").value,
2899      },
2900      {
2901        key: "rating",
2902        value: document.querySelector('input[name="rating"]:checked').value,
2903      },
2904      {
2905        key: "message",
2906        value: document.getElementById("reviewMessage").value,
2907      },
2908    ];
2909
2910    const formData = new FormData();
2911
2912    data.forEach(({ key, value }) => {
2913      if (!value) {
2914        throw new Error(`Missing ${key}`);
2915      }
2916
2917      formData.append(key, value);
2918    });
2919
2920    fetch("/apps/productreviews/add", {
2921      method: "POST",
2922      body: formData,
2923    })
2924      .then((response) => {
2925        if (!response.ok) {
2926          this.handleError(response.statusText);
2927        }
2928        return response.json();
2929      })
2930      .then((data) => {
2931        if (data.success === 1) {
2932          this.renderThankYouMessage(data);
2933        } else {
2934          this.handleError(data?.error);
2935        }
2936      })
2937      .catch((error) => {
2938        this.handleError(error);
2939      });
2940  },
2941  renderThankYouMessage: function (data) {
2942    const { message_lbl, message_txt } = data;
2943
2944    const thankYouDiv = document.createElement("div");
2945    thankYouDiv.classList.add(
2946      ...this.config.styling.classes.formThankYouMessageContainer.split(" ")
2947    );
2948
2949    const thankYouLabel = document.createElement("label");
2950    thankYouLabel.classList.add(
2951      ...this.config.styling.classes.formThankYouMessageLabel.split(" ")
2952    );
2953    thankYouLabel.textContent = message_lbl;
2954    thankYouDiv.appendChild(thankYouLabel);
2955
2956    const thankYouMessage = document.createElement("p");
2957    thankYouMessage.classList.add(
2958      ...this.config.styling.classes.formThankYouMessageLabel.split(" ")
2959    );
2960    thankYouMessage.textContent = message_txt;
2961    thankYouDiv.appendChild(thankYouMessage);
2962
2963    const form = document.getElementById("productReviewForm");
2964
2965    form.parentNode.replaceChild(thankYouDiv, form);
2966
2967    setTimeout(() => {
2968      thankYouDiv.parentNode.removeChild(thankYouDiv);
2969
2970      const productReviewButton = document.getElementById(
2971        "productReviewShowFormButton"
2972      );
2973
2974      if (productReviewButton) {
2975        productReviewButton.parentNode.removeChild(productReviewButton);
2976      }
2977    }, 5000);
2978  },
2979  renderErrorMessage: function (error) {
2980    const errorDiv = document.querySelector("#productReviewErrorMessage");
2981    errorDiv.textContent = error || "Something went wrong"; // TODO: Translate
2982  },
2983});
2984</script>
vendor: 1 bytes, line 2984
2984
2985<script>
2986    qb_cookieconsent.run({
2987        current_lang: 'en',
2988        autoclear_cookies: true,
2989        cookie_expiration: 90,
2990        autoload_css: true,
2991        theme_css: '/assets/shopassets/cookieconsent/cookieconsent.css?v=231',
2992        languages: {
2993            en: {
2994                consent_modal: {
2995                    title: "Vi använder cookies",
2996                    description: 'Vi använder cookies för att anpassa det innehåll som visas för dig och för att du ska få bästa tänkbara upplevelse när du handlar hos oss.',
2997                    primary_btn: {
2998                        text: 'Godkänn alla',
2999                        role: 'accept_all'
3000                    },
3001                    secondary_btn: {
3002                        text: 'Endast nödvändiga',
3003                        role: 'accept_necessary'
3004                    }
3005                },
3006                settings_modal: {
3007                    title: 'Cookie inställningar',
3008                    save_settings_btn: "Spara",
3009                    accept_all_btn: "Godkänn alla",
3010                    cookie_table_headers: [
3011                        { col1: "Name" },
3012                        { col2: "Domain" },
3013                        { col3: "Expiration" },
3014                        { col4: "Description" }
3015                    ],
3016                    blocks: qb_cookieconsent_blocks
3017                }
3018            }
3019        },
3020        onAccept: function (cookie) {
3021            var event = new CustomEvent("QB_CookieConsentAccepted", {
3022                detail: {
3023                    cookie: cookie
3024                }
3025            });
3026
3027            document.dispatchEvent(event);
3028        },
3029        onChange: function (cookie) {
3030            var event = new CustomEvent("QB_CookieConsentAccepted", {
3031                detail: {
3032                    cookie: cookie
3033                }
3034            });
3035
3036            document.dispatchEvent(event);
3037        },
3038    });
3039
3040</script>
3040
3041
3042<script type="text/javascript">window.registerApp({
3043  name: 'PriceHistory',
3044  identifier: 'QuickbutikPriceHistory',
3045  description: 'Price history',
3046  version: '1.0.0',
3047  pages: ['product'],
3048  data: {},
3049  config: {
3050    appContainerSelector: '[data-qb-selector="product-price-container"]',
3051    appInsertPosition: 'beforeend',
3052    styling: {
3053      textClasses: 'text-sm opacity-50',
3054      tooltipClasses:
3055        'w-64 bg-card shadow-default rounded py-4 px-6 m-2 absolute sm:left-1/2 sm:-translate-x-1/2 mx-auto z-10 pointer-events-auto',
3056      svgIcon:
3057        '<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />',
3058      svgIconClasses: 'ml-1 w-5 h-5',
3059    },
3060  },
3061
3062  onMount: function ({ appData, page }) {
3063    const productLowestPrices = appData && appData.pricehistory ? appData.pricehistory.lowest_prices : null;
3064    const productId = window?.qs_store_apps_data?.product_id
3065    let productLowestPrice = null
3066
3067    const container = document.querySelector(this.config.appContainerSelector)
3068    if (!container) {
3069      console.log(
3070        'Could not find app container: ',
3071        this.config.appContainerSelector,
3072      )
3073      return
3074    }
3075
3076    // Return lowest price for no variants
3077    switch(typeof productLowestPrices[productId]) {
3078      case 'string':
3079        productLowestPrice = productLowestPrices[productId];
3080        break;
3081      case 'object':
3082        productLowestPrice = this.getLowestPrice(productLowestPrices, productId);
3083        break;
3084      default:
3085        return;
3086    }
3087
3088    if (productLowestPrice) {
3089      container.insertAdjacentHTML(this.config.appInsertPosition, this.renderLowestPrice(
3090        productLowestPrice,
3091        appData.pricehistory,
3092      ));
3093    }
3094  },
3095
3096  getLowestPrice(productLowestPrices, productId) {
3097    const variantPrices = productLowestPrices[productId];
3098    
3099    // Add lowest prices to qs_options
3100    Object.entries(variantPrices).forEach(([variantId, lowestPrice]) => {
3101      const variant = Object.values(qs_options).find(option => option.id === variantId);
3102      if (variant) {
3103        variant.lowest_price = lowestPrice;
3104      }
3105    });
3106
3107    // Find the appropriate variant
3108    const selectedVariant = 
3109      Object.values(qs_options).find(option => option.preselected) ||
3110      Object.values(qs_options).find(option => option.available) ||
3111      Object.values(qs_options)[0];
3112
3113    return selectedVariant ? variantPrices[selectedVariant.id] : null;
3114  },
3115
3116  renderLowestPrice(price, appData) {
3117    if (!price) {
3118      return ''; // Return empty string if no price
3119    }
3120    
3121    return `
3122    <div x-data="{ 
3123      showLowestPrice: true, 
3124      currentLowestPrice: '${price}',
3125      showTooltip: false
3126    }" 
3127    x-init="$watch('variant', (value) => {
3128      if (value && value.lowest_price) {
3129        currentLowestPrice = value.lowest_price;
3130        showLowestPrice = true;
3131      } else {
3132        showLowestPrice = false;
3133      }
3134    })">
3135      <div x-show="showLowestPrice" class="block relative">
3136        <div class="inline-block relative">
3137          <div class="flex ${this.config.styling.textClasses}">
3138            <span>${appData.texts.label}:</span>
3139            <span class="ml-1" x-text="currentLowestPrice"></span>
3140            <svg 
3141              @mouseenter="showTooltip = true" 
3142              @mouseleave="showTooltip = false"
3143              @click="showTooltip = !showTooltip"
3144              xmlns="http://www.w3.org/2000/svg" 
3145              fill="none" 
3146              viewBox="0 0 24 24" 
3147              stroke-width="1.5" 
3148              stroke="currentColor" 
3149              class="${this.config.styling.svgIconClasses} cursor-pointer">
3150              ${this.config.styling.svgIcon}
3151            </svg>
3152          </div>
3153          <div 
3154            x-show="showTooltip"
3155            @mouseenter="showTooltip = false"
3156            @click="showTooltip = false"
3157            x-transition:enter="transition ease-out duration-200"
3158            x-transition:enter-start="opacity-0"
3159            x-transition:enter-end="opacity-100"
3160            x-transition:leave="transition ease-in duration-150"
3161            x-transition:leave-start="opacity-100"
3162            x-transition:leave-end="opacity-0"
3163            class="${this.config.styling.tooltipClasses}">${appData.texts.tooltip}</div>
3164        </div>
3165      </div>
3166    </div>
3167    `
3168  },
3169
3170})
3171</script>
3171<script type="text/javascript">window.registerApp({
3172    name: 'Recaptcha',
3173    identifier: 'QuickbutikRecaptcha',
3174    description: 'Recaptcha',
3175    version: '1.1.0',
3176    pages: ['contact'],
3177    data: {},
3178    config: {},
3179    onMount: function ({ appData, page }) {
3180        console.debug(`${this.identifier}@onMount`)
3181
3182        let form = document.querySelector('form[role=form]')
3183        if (!form) {
3184            console.error('Aborting recaptcha, form not found.')
3185            return
3186        }
3187
3188        const createCaptchaDiv = () => {
3189            let captchaDiv = document.createElement('div');
3190            captchaDiv.className = 'g-recaptcha'
3191            captchaDiv.dataset.sitekey = recaptchaSiteKey
3192            captchaDiv.style.marginBottom = '15px'
3193            captchaDiv.style.overflow = 'hidden'
3194            return captchaDiv
3195        }
3196                   
3197        let button = form.querySelector('button');
3198        let inputSubmit = form.querySelector('input[type=submit]');
3199        if (button) {
3200            button.insertAdjacentElement('beforebegin', createCaptchaDiv())
3201        } else if (inputSubmit) {
3202            inputSubmit.insertAdjacentElement('beforebegin', createCaptchaDiv())
3203        }
3204
3205        // Honeypot for contact page
3206        let contactForm = document.querySelector('form[role=form]#contact-form')
3207        if (contactForm) {
3208            let pottyDiv = document.createElement('div')
3209            pottyDiv.style.position = 'absolute'
3210            pottyDiv.style.left = '-5000px'
3211            pottyDiv.setAttribute('aria-hidden', true)
3212
3213            let codeLabel = document.createElement('label')
3214            codeLabel.innerText = 'Code'
3215            codeLabel.for = 'code'
3216            codeLabel.className = contactForm.querySelector('label').className
3217            codeLabel.setAttribute('aria-hidden', true)
3218            pottyDiv.appendChild(codeLabel)
3219
3220            let codeInput = document.createElement('input')
3221            codeInput.type = 'text'
3222            codeInput.id = 'code'
3223            codeInput.name = 'code'
3224            codeInput.tabIndex = -1
3225            codeInput.autocomplete = 'off'
3226            codeInput.className = contactForm.querySelector('input[name="name"]').className
3227            codeInput.setAttribute('aria-hidden', true)
3228            pottyDiv.appendChild(codeInput)
3229
3230            contactForm.querySelector('textarea').insertAdjacentElement('afterend', pottyDiv)
3231        }
3232
3233        // Inject Recaptcha script
3234        let existingScript = document.querySelector('script[src="https://www.google.com/recaptcha/api.js"]')
3235        if (!existingScript) {
3236            let script = document.createElement('script')
3237            script.src = 'https://www.google.com/recaptcha/api.js'
3238            script.type = 'text/javascript'
3239            script.async = true
3240            document.querySelector('head').appendChild(script)
3241        }
3242
3243    },
3244})
3245</script>
3245    
3245<script src="/assets/shopassets/misc/magicbean.js?v=3" data-shop-id="100911" data-url="https://magic-bean.services.quickbutik.com/v1/sow" data-site-section="1" data-key="31379dea6afaca4fe6f7cac0111c6022b1bd493ffafbc180aa0d6f4fe8caedd0" data-timestamp="1791435917" defer></script>
3245<script>
3246    document.addEventListener('QB_CookieConsentAccepted', function(event) {
3247        const cookie = qbGetEvent(event).cookie;
3248
3249        const getConsentLevel = (level) => (cookie && cookie.level && cookie.level.includes(level)) ? 'granted' : 'denied';
3250
3251        gtag('consent', 'update', {
3252            'analytics_storage': getConsentLevel('analytics'),
3253            'ad_storage': getConsentLevel('marketing'),
3254            'ad_user_data': getConsentLevel('marketing'),
3255            'ad_personalization': getConsentLevel('marketing'),
3256        });
3257    });
3258
3259    document.addEventListener('QB_CartAdded', function(e) {
3260        const itemData = qbGetEvent(e).itemData;
3261
3262        // item.id is the CART-LINE key &mdash; an md5 of the line (see
3263        // Model_Shop_Basketnew), unique per product+variant+options and
3264        // meaningless outside this basket. item.raw_id is the product id,
3265        // which is what view_item sends, what <g:id> carries in the Google
3266        // Shopping feed, and what GA4 needs to join the item funnel.
3267        let eventData = {
3268            currency: 'SEK',
3269            value: parseFloat(itemData.item.price_raw),
3270            items: [{
3271                id: itemData.item.raw_id || itemData.item.id,
3272                item_id: itemData.item.raw_id || itemData.item.id,
3273                name: itemData.item.title.replaceAll('"', ''),
3274                item_name: itemData.item.title.replaceAll('"', ''),
3275                affiliation: `S&auml;kra forum`,
3276                ...(itemData.variant_name && {
3277                    name: itemData.item.title.replaceAll('"', '') + ' ' + itemData.variant_name.replaceAll('"', ''),
3278                    item_name: itemData.item.title.replaceAll('"', '') + ' ' + itemData.variant_name.replaceAll('"', ''),
3279                    variant: itemData.variant_name.replaceAll('"', ''),
3280                    item_variant: itemData.variant_name.replaceAll('"', ''),
3281                }),
3282                ...(itemData.item.item_category && {
3283                    item_category: itemData.item.item_category.replaceAll('"', ''),
3284                }),
3285                price: parseFloat(itemData.item.price_raw),
3286                quantity: itemData.qty
3287            }],
3288        };
3289        'undefined' !== typeof gtag && gtag('event', 'add_to_cart', eventData);
3290    });
3291</script>
3291<script>;(function () {
3292    if (window.parent === window) return;
3293
3294    const urlParams = new URLSearchParams(window.location.search);
3295    if (urlParams.has('_qbdev')) {
3296        try { sessionStorage.setItem('_qbdev', '1'); } catch(e) {}
3297    }
3298    try { if (!sessionStorage.getItem('_qbdev')) return; } catch(e) { return; }
3299
3300    const report = () => {
3301        try {
3302            const params = new URLSearchParams(window.location.search);
3303            params.delete('_qbdev');
3304            params.delete('_qbtid');
3305            const qs = params.toString();
3306            window.parent.postMessage(
3307                { type: 'navigation', path: window.location.pathname + (qs ? '?' + qs : '') },
3308                '*'
3309            );
3310        } catch (e) {}
3311    };
3312
3313    report();
3314
3315    const { pushState, replaceState } = history;
3316    history.pushState = function (...args) {
3317        const result = pushState.apply(this, args);
3318        report();
3319        return result;
3320    };
3321    history.replaceState = function (...args) {
3322        const result = replaceState.apply(this, args);
3323        report();
3324        return result;
3325    };
3326    window.addEventListener('popstate', report);
3327})()</script>
vendor: 1 bytes, line 3327
3327
3328<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v4bc70e2c01a94c73b74392e4234840661791215815920" integrity="sha512-L0ha0OXavK/8okipN9F8BtP84dg9DUhPERbBXzwI6dgTA55d2+yweo3pn5CSFYs45/r8md2+xvUPtTdvTNRfjA==" data-cf-beacon='{"version":"2024.11.0","token":"7a304e0c4e854d16bca3b20c50bbf8db","spa":2}' crossorigin="anonymous"></script>
3328
3329</body>
3330
3331</html>

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.