1<!DOCTYPE html> 2<html lang="sv"> 3 4<head> 5<link rel="alternate" href="https://vecanis.se/hundmat" hreflang="sv" /> 6<link rel="alternate" href="https://vecanis.se/en/hundmat" hreflang="en" /> 7<meta property="ix:host" content="cdn.quickbutik.com/images"> 8 <title>Växtbaserad hundmat - ett Hypoallergent och klimatsmart val - Vecanis</title> 9 <meta charset="utf-8"> 10 11 <meta name="author" content="Quickbutik"> 12 <meta name="description" content="Allergivänlig, växtbaserad och klimatvänlig hundmat som är bÃ¥de god & näringsrik- för en hälsosammare hund och planet. ð±ð¶"> 13 14 <meta property="og:description" content="Allergivänlig, växtbaserad och klimatvänlig hundmat som är bÃ¥de god & näringsrik- för en hälsosammare hund och planet. ð±ð¶"> 15 <meta property="og:title" content="Växtbaserad hundmat - ett Hypoallergent och klimatsmart val - Vecanis"> 16 <meta property="og:url" content="https://vecanis.se/hundmat"> 17 <meta property="og:site_name" content="Vecanis"> 18 <meta property="og:type" content="website"> 19 <meta property="og:image" content=""> 20 21 <meta property="twitter:card" content="summary"> 22 <meta property="twitter:title" content="Vecanis"> 23 <meta property="twitter:description" content="Allergivänlig, växtbaserad och klimatvänlig hundmat som är bÃ¥de god & näringsrik- för en hälsosammare hund och planet. ð±ð¶"> 24 <meta property="twitter:image" content=""> 25 26 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 27 28 <link rel="canonical" href="https://vecanis.se/hundmat"> 29 30 <!-- Google Fonts --> 31 32 33 34 35 36 37 38 <!-- Preload images --> 39 <link rel="preload" as="image" href="https://cdn.quickbutik.com/images/2666L/templates/lua/assets/logo.png?s=96971&auto=format&fit=clip&h=160&w=600"> 40 41 <!-- Favicon --> 42 <link rel="icon" type="image/png" href="https://cdn.quickbutik.com/images/2666L/templates/lua/assets/favicon.png?s=96971&auto=format"> 43 44 <!-- Components --> 45
45<script defer src="https://storage.quickbutik.com/templates/lua/js/settings.js?96971"></script>
45 46
46<script defer src="https://storage.quickbutik.com/templates/lua/js/tailwind-styling.js?96971"></script>
46 47 48 <!-- Quickbutik --> 49
50<script type="text/javascript">var qs_store_url = "https://vecanis.se";</script>
vendor: 1 bytes, line 50
50
51<script type="text/javascript">var qs_store_apps_data = {"location":"\/hundmat","products":"989,984,982,979,935,934,896,881,880,810,809,805,793,788,787,772,771,770,769,763,740,733,732,694,673,672,671,670,665,643,633,629,619,362,361,360,134,57,56,55,54,15,13,12,5","product_id":null,"category_id":"2","category_name":null,"fetch":"fetch","order":null,"search":null,"recentPurchaseId":null,"pagination":{"total_page_count":null,"current_page":null,"total_product_count":null,"previous_page":null,"next_page":null,"previous_page_url":null,"next_page_url":null,"current_url":"https:\/\/vecanis.se\/hundmat","has_more_pages":false,"pagination_activated":false,"sort":"pid_0"}}; var qs_store_apps = [];</script>
vendor: 1 bytes, line 51
51
52<script defer type="text/javascript" src="/assets/qb_essentials_alpine.js?v=20260819a"></script>
52 53 54
54<script defer src="https://storage.quickbutik.com/templates/lua/js/appsOverrides.js?96971"></script>
54 55 56 <!-- Libraries --> 57
57<script defer type="text/javascript" src="/assets/imgix.min.js?v=290323"></script>
57 58
58<script defer src="https://storage.quickbutik.com/templates/lua/js/swiper-bundle.min.js?96971"></script>
58 59
59<script defer src="https://storage.quickbutik.com/templates/lua/js/sliders.js?96971"></script>
59 60
60<script defer src="https://storage.quickbutik.com/templates/lua/js/photoswipe-lightbox.umd.min.js?96971"></script>
60 61
61<script defer src="https://storage.quickbutik.com/templates/lua/js/photoswipe.umd.min.js?96971"></script>
61 62 63 <!-- Default parameters for Imgix --> 64
64<script>document.addEventListener("DOMContentLoaded", () => { imgix.config.defaultParams = { auto: 'format' } })</script>
64 65 66 <!-- Scripts in head --> 67 <link rel="icon" type="image/png" href="https://s3-eu-west-1.amazonaws.com/storage.quickbutik.com/stores/2666L/files/favicon.ico"> 68 69 70 71 <!-- Stylesheet --> 72 <style>[x-cloak]{display: none !important;}img[ix-sizes="auto"]{display:block;width:100%;}img[src=""],img[src="#"],img:not([src]){visibility: hidden;}</style> 73 <link rel="preload" href="https://storage.quickbutik.com/templates/lua/css/swiper-bundle.min.css?96971" as="style" onload="this.onload=null;this.rel='stylesheet'"> 74 <noscript><link rel="stylesheet" href="https://storage.quickbutik.com/templates/lua/css/swiper-bundle.min.css?96971"></noscript> 75 <link rel="preload" href="https://storage.quickbutik.com/templates/lua/css/photoswipe.css?96971" as="style" onload="this.onload=null;this.rel='stylesheet'"> 76 <noscript><link rel="stylesheet" href="https://storage.quickbutik.com/templates/lua/css/photoswipe.css?96971"></noscript> 77 <link type="text/css" rel="stylesheet" href="https://storage.quickbutik.com/stores/2666L/templates/lua/css/theme.css?96971">
78<script type="text/javascript" defer>document.addEventListener('DOMContentLoaded', () => { 79 document.body.setAttribute('data-qb-page', 'list'); 80});</script>
80<script type="text/javascript" defer>(function () { 81 const MAX_GET_QUERY_LENGTH = 2000; 82 83 window.apps = window.apps ?? {}; 84 85 window.registerApp = function (app) { 86 console.debug('registerApp', app); 87 window.apps[app.identifier] = app; 88 }; 89 90 document.addEventListener('DOMContentLoaded', () => { 91 const page = document.body.dataset?.qbPage; 92 const bodyFormData = new FormData(); 93 94 const productIds = [ 95 ...new Set( 96 [...document.querySelectorAll('[data-qb-selector="product-item"]')] 97 .map((product) => product.dataset.productId?.trim()) 98 .filter(Boolean) 99 ), 100 ]; 101 102 bodyFormData.set('location', window.location.pathname + window.location.search); 103 bodyFormData.set('products', productIds.join(',')); 104 bodyFormData.set( 105 'category_id', 106 document.querySelector('[data-qb-category-id]')?.dataset?.qbCategoryId || '' 107 ); 108 bodyFormData.set( 109 'product_id', 110 ['product'].includes(page) && document.querySelector('[data-product-id]')?.dataset?.productId || '' 111 ); 112 113 bodyFormData.set('fetch', 'fetch'); 114 bodyFormData.set('order', ''); 115 bodyFormData.set('search', ''); 116 bodyFormData.set('recentPurchaseId', ''); 117 118 const visitedProductId = bodyFormData.get('product_id'); 119 if (visitedProductId) { 120 const visit = new URLSearchParams({ product_id: visitedProductId }); 121 if (!(navigator.sendBeacon && navigator.sendBeacon(qs_store_url + '/apps/visit', visit))) { 122 fetch(qs_store_url + '/apps/visit', { method: 'post', body: visit, keepalive: true }).catch(() => {}); 123 } 124 } 125 126 const query = [...bodyFormData.entries()] 127 .map(([key, value]) => 128 encodeURIComponent(key) + '=' + 129 (key === 'products' ? productIds.map(encodeURIComponent).join(',') : encodeURIComponent(value)) 130 ) 131 .join('&'); 132 133 const postApps = () => { 134 const postData = new FormData(); 135 bodyFormData.forEach((value, key) => postData.append(key, value)); 136 postData.set('variants', JSON.stringify(typeof qs_options !== 'undefined' ? qs_options : [])); 137 138 return fetch(qs_store_url + '/apps/fetch', { method: 'post', body: postData }).then((res) => res.json()); 139 }; 140 141 const appsResponse = 142 query.length <= MAX_GET_QUERY_LENGTH 143 ? fetch(qs_store_url + '/apps/fetch?' + query, { method: 'get' }) 144 .then((res) => { 145 if (!res.ok || !res.headers.get('X-QB-Apps-Fetch')) { 146 throw new Error('GET /apps/fetch not accepted (' + res.status + ')'); 147 } 148 149 return res.json(); 150 }) 151 .catch(postApps) 152 : postApps(); 153 154 appsResponse 155 .then((data) => { 156 console.debug('Fetch apps success', data); 157 158 if (typeof Alpine === 'undefined') { 159 document.addEventListener('alpine:init', () => { 160 initApps(data); 161 }) 162 } else { 163 initApps(data); 164 } 165 }) 166 .catch((error) => { 167 console.debug('Fetch apps error', error); 168 }); 169 }); 170 171 function initApps(appData) { 172 const page = document.querySelector('[data-qb-page]')?.dataset?.qbPage; 173 174 console.debug('initApps', appData, page, window.apps); 175 176 if (page) { 177 Object.keys(window.apps).forEach((appIdentifier) => { 178 if (window.apps[appIdentifier].pages.includes(page)) { 179 let appOverrides = window?.appsOverrides?.[appIdentifier]; 180 181 if (appOverrides) { 182 window.apps[appIdentifier] = merge( 183 window.apps[appIdentifier], 184 appOverrides 185 ); 186 } 187 188 window.apps[appIdentifier].onMount({ appData, page }); 189 } 190 }); 191 } 192 } 193 194 function isObject(obj) { 195 return obj !== null && typeof obj === 'object'; 196 } 197 198 function merge(target, source) { 199 if (!isObject(target) || !isObject(source)) { 200 throw new Error('Both target and source must be objects.'); 201 } 202 203 for (const key in source) { 204 if (source.hasOwnProperty(key)) { 205 const targetValue = target[key]; 206 const sourceValue = source[key]; 207 208 if (isObject(sourceValue) && isObject(targetValue)) { 209 target[key] = merge(targetValue, sourceValue); 210 } else { 211 target[key] = sourceValue; 212 } 213 } 214 } 215 216 return target; 217 } 218})(); 219</script>
219<script src="/assets/shopassets/cookieconsent/cookieconsent.js?v=230"></script>
219 220<style> 221 :root { 222 --cc-btn-primary-bg: #222222 !important; 223 --cc-btn-primary-text: #fff !important; 224 --cc-btn-primary-hover-bg: #222222 !important; 225 --cc-btn-primary-hover-text: #fff !important; 226 } 227</style> 228<style> 229 html.force--consent, 230 html.force--consent body{ 231 height: auto!important; 232 width: 100vw!important; 233 } 234</style> 235 236
236<script type="text/javascript"> 237 var qb_cookieconsent = initCookieConsent(); 238 var qb_cookieconsent_blocks = [{"title":"Vi anv\u00e4nder cookies","description":"Vecanis anv\u00e4nder cookies - inte fr\u00e5n ugnen, utan de digitala. Genom att forts\u00e4tta p\u00e5 sidan godk\u00e4nner du anv\u00e4ndandet av cookies."},{"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}}]; 239</script>
239 240<link rel="stylesheet" href="https://storage.quickbutik.com/stores/2666L/templates/lua/css/tailwind.css?96971" /> <!-- Google Tag Manager --> 241
241<script> 242 window.dataLayer = window.dataLayer || []; 243 function gtag(){dataLayer.push(arguments);} 244 245 gtag('consent', 'default', { 246 'analytics_storage': 'denied', 247 'ad_storage': 'denied', 248 'ad_user_data': 'denied', 249 'ad_personalization': 'denied', 250 'wait_for_update': 500, 251 }); 252 253 // Update consent if already granted (returning visitor with cc_cookie) 254 (function(){ 255 try { 256 var m = document.cookie.match(/(?:^|;\s*)cc_cookie=([^;]+)/); 257 if (m) { 258 var c = JSON.parse(decodeURIComponent(m[1])); 259 if (c && c.level) { 260 var hasAnalytics = c.level.indexOf('analytics') !== -1; 261 var hasMarketing = c.level.indexOf('marketing') !== -1; 262 if (hasAnalytics || hasMarketing) { 263 gtag('consent', 'update', { 264 'analytics_storage': hasAnalytics ? 'granted' : 'denied', 265 'ad_storage': hasMarketing ? 'granted' : 'denied', 266 'ad_user_data': hasMarketing ? 'granted' : 'denied', 267 'ad_personalization': hasMarketing ? 'granted' : 'denied', 268 }); 269 } 270 } 271 } 272 } catch(e) {} 273 })(); 274 275 // Initialize GTM
vendor: 348 bytes, lines 275-280
275 276 (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': 277 new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], 278 j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 279 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 280 })(window,document,'script','dataLayer','
280GTM-WTRG25FS
vendor: 9 bytes, lines 280-282
280'); 281 282
282// Handle consent updates (user clicks Accept) 283 document.addEventListener('QB_CookieConsentAccepted', function(event) { 284 var cookie = qbGetEvent(event).cookie; 285 var getConsentLevel = function(level) { return (cookie && cookie.level && cookie.level.includes(level)) ? 'granted' : 'denied'; }; 286 287 gtag('consent', 'update', { 288 'analytics_storage': getConsentLevel('analytics'), 289 'ad_storage': getConsentLevel('marketing'), 290 'ad_user_data': getConsentLevel('marketing'), 291 'ad_personalization': getConsentLevel('marketing'), 292 }); 293 }); 294 </script>
294 295 <!-- End Google Tag Manager --> 296 297 <!-- Google Tag Manager (noscript) -->
vendor: 72 bytes, lines 297-298
297 298 <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=
298GTM-WTRG25FS
vendor: 91 bytes, lines 298-300
298" 299 height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> 300
300<!-- End Google Tag Manager (noscript) --></head> 301 302<body class="antialiased bg-body text-body font-body"> 303 <section 304 data-qb-area="header" data-qb-element="" 305 x-data="{navMenuOpen: false}" 306 @keydown.escape="navMenuOpen = false; $store.cart.hideCart(); $store.settings.hideSettings();" 307 class="bg-header md:pb-4"> 308 309 <section x-data class="w-full" data-qb-element="notifications"> 310 <template x-for="notification in $store.notifications.notifications" :key="notification.id"> 311 <div x-show="notification.visible" x-on:click="$store.notifications.removeNotification(notification.id)" 312 x-transition:enter='transition ease-out duration-default transform' 313 x-transition:enter-start='opacity-0 -translate-y-4' 314 x-transition:enter-end='opacity-100 translate-y-0' 315 x-transition:leave='transition ease-in duration-default transform' 316 x-transition:leave-start='opacity-100 translate-y-0' 317 x-transition:leave-end='opacity-0 -translate-y-4' 318 > 319 <div class="relative w-full px-4 sm:px-8 py-4 bg-black" :style="{'background-color': notification.backgroundColor || '#000'}"> 320 <div class="relative z-10 flex items-center justify-between -m-2"> 321 <div class="flex-1 p-2" :style="{'color': notification.textColor || '#fff'}"> 322 <template x-if="notification.message"> 323 <h3 class="font-heading font-semibold text-lg text-center" x-html="notification.message"></h3> 324 </template> 325 <template x-if="notification.htmlContent"> 326 <div class="text-center" x-html="notification.htmlContent"></div> 327 </template> 328 </div> 329 <div class="p-2"> 330 <button 331 class="w-8 h-8 flex items-center justify-center rounded-full hover:opacity-75 transition-opacity duration-200 cursor-pointer" 332 :style="{'color': notification.textColor || '#fff'}" 333 x-on:click.stop="$store.notifications.removeNotification(notification.id)" 334 aria-label="Close notification" 335 > 336 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 337 <path d="M12 4L4 12M4 4L12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> 338 </svg> 339 </button> 340 </div> 341 </div> 342 </div> 343 </div> 344 </template> 345 </section> 346 347 348 <div class="flex items-center justify-between relative py-2" style=""> 349 <div class="container px-4 mx-auto"> 350 <div class="flex items-center justify-between relative"> 351 <div class="flex text-sm"> 352 353 <!-- Tax --> 354 355 </div> 356 <div class="flex text-sm"> 357 358 <!-- Settings --> 359 360 <button @click="$store.settings.showSettings()" aria-label="Inställningar" class="flex items-center opacity-70 hover:opacity-100 mr-4 focus:outline-none" style=""> 361 <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 opacity-50"> 362 <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.074.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" /> 363 <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> 364 </svg>
365 <span class="hidden sm:inline-block ml-1">Inställningar</span> 366 </button> 367 368 <!-- Login --> 369 <a href="/customer/login" class="flex items-center opacity-70 hover:opacity-100"> 370 <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 opacity-50"> 371 <path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"></path> 372 </svg> 373 <span class="hidden sm:inline-block ml-1">Logga in</span> 374 </a> 375 376 </div> 377 </div> 378 </div> 379 </div> 380 381 <hr class="border-foreground opacity-5"> 382 383 <nav> 384 <div class="container px-4 py-2 sm:py-6 mx-auto"> 385 <div class="flex items-center justify-between relative"> 386 387 <!-- Menu button --> 388 <div class="flex justify-items-start w-2/5"> 389 390 <div class="lg:hidden flex justify-items-start w-1/5"> 391 <button @click="navMenuOpen = !navMenuOpen" aria-label="Menu" 392 class="navbar-burger py-3 px-2 sm:px-4 hover:text-primary hover:bg-black/5 transition duration-default focus:outline-none"> 393 <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"> 394 <path fill-rule="evenodd" clip-rule="evenodd" 395 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" 396 fill="currentColor"></path> 397 </svg> 398 </button> 399 </div> 400 401 </div> 402 403 <!-- Logo --> 404 <a class="inline-block h-16" href="https://vecanis.se"> 405 <img ix-sizes="auto" ix-src="https://cdn.quickbutik.com/images/2666L/templates/lua/assets/logo.png?s=96971&auto=format&fit=clip&h=160&w=400&dpr=2" alt="Vecanis" class="object-center object-contain sm:px-4 h-16"> 406 </a> 407 408 <div class="flex justify-end w-2/5"> 409 410 <!-- Search --> 411 <button @click="$store.search.showSearch(); $nextTick(() => document.getElementById('searchInput').focus());" aria-label="Sök" class="py-2 px-2 rounded-full hover:bg-black/5 hover:text-primary transition duration-default focus:outline-none"> 412 <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 sm:w-6 sm:h-6"> 413 <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" /> 414 </svg> 415 </button> 416 417 <!-- Wishlist --> 418 419 <!-- Cart --> 420 <button @click="$store.cart.toggleCart()" aria-label="Varukorg" class="relative py-2 px-2 rounded-full hover:bg-black/5 hover:text-primary transition duration-default focus:outline-none"> 421 <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 sm:w-6 sm:h-6"> 422 <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" /> 423 </svg> 424 425 <!-- Cart bubble --> 426 <template x-if="$store.cart.totalQuantity > 0">
427 <span class="cart-bubble absolute flex items-center justify-center -top-0.5 -right-0.5 h-6 w-6 border-2 border-header bg-primary text-white font-bold text-xs rounded-full scale-75 md:scale-100" x-text="$store.cart.totalQuantity"></span> 428 </template> 429 430 </button> 431 </div> 432 433 </div> 434 </div> 435 <div class="hidden lg:block container px-4 mx-auto"> 436 <div> 437 438 <ul x-data="{ navbarLevelsDepth: 3 }" class="flex justify-center"> 439 440 441 442 <li x-data="dropdown" @mouseover="open = navbarLevelsDepth >= 1 && true" @mouseleave="open = false" class=" mx-2 lg:mx-4 group"> 443 <div class="relative flex items-center justify-center"> 444 <a href="/hundmat" id="menu-item-8" 445 class="flex py-2 px-1 transition ease-in-out duration-default font-bold hover:opacity-70 active"> 446 <span>Hundmat</span> 447 </a> 448 <svg x-show="navbarLevelsDepth >= 1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" 449 stroke-width="1.5" stroke="currentColor" 450 class="w-4 h-4 cursor-pointer transform duration-default rotate-90 " :class="{'rotate-0' : open}"> 451 <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"></path> 452 </svg> 453 </div> 454 455 <!-- First drop down --> 456 <ul x-cloak 457 x-show="open" 458 " 459 x-transition:enter='transition ease-out transform' 460 x-transition:enter-start='opacity-0 -translate-y-4' 461 x-transition:enter-end='opacity-100 translate-y-0' 462 x-transition:leave='transition ease-in transform' 463 x-transition:leave-start='opacity-100 translate-y-0' 464 x-transition:leave-end='opacity-0 -translate-y-4' 465 class="absolute z-50 w-full max-w-xs px-6 py-2 bg-header shadow-md roundednone duration-default" aria-labelledby="menu-item-8"> 466 467 468 <li><a href="/hundmat/topping" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Topping</a></li> 469 470 471 472 <li><a href="/hundmat/blotmat" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Blötmat</a></li> 473 474 475 476 <li><a href="/hundmat/torrfoder" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Torrfoder</a></li> 477 478 479 480 <li><a href="/hundmat/allergivanlig-hundmat" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Allergi</a></li> 481 482 483 484 <li><a href="/hundmat/glutenfritt" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Glutenfritt</a></li> 485 486 487 488 <li><a href="/hundmat/fettsnalt" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Fettsnålt</a></li> 489 490 491 492 <li><a href="/hundmat/spannmalsfritt" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Spannmålsfritt</a></li> 493 494 495 496 <li><a href="/hundmat/senior" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Senior</a></li> 497 498 499 500 <li><a href="/hundmat/valp" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Valp</a></li> 501 502 503 504 <li><a href="/hundmat/mini" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Mini</a></li> 505 506 </ul> 507 </li> 508 509 510 511 <li x-data="dropdown" @mouseover="open = navbarLevelsDepth >= 1 && true" @mouseleave="open = false" class=" mx-2 lg:mx-4 group"> 512 <div class="relative flex items-center justify-center"> 513 <a href="/hundgodis" id="menu-item-146" 514 class="flex py-2 px-1 transition ease-in-out duration-default hover:opacity-70 ">
515 <span>Hundgodis</span> 516 </a> 517 <svg x-show="navbarLevelsDepth >= 1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" 518 stroke-width="1.5" stroke="currentColor" 519 class="w-4 h-4 cursor-pointer transform duration-default rotate-90 " :class="{'rotate-0' : open}"> 520 <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"></path> 521 </svg> 522 </div> 523 524 <!-- First drop down --> 525 <ul x-cloak 526 x-show="open" 527 " 528 x-transition:enter='transition ease-out transform' 529 x-transition:enter-start='opacity-0 -translate-y-4' 530 x-transition:enter-end='opacity-100 translate-y-0' 531 x-transition:leave='transition ease-in transform' 532 x-transition:leave-start='opacity-100 translate-y-0' 533 x-transition:leave-end='opacity-0 -translate-y-4' 534 class="absolute z-50 w-full max-w-xs px-6 py-2 bg-header shadow-md roundednone duration-default" aria-labelledby="menu-item-146"> 535 536 537 <li><a href="/hundgodis/tuggben" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Tuggben</a></li> 538 539 540 541 <li><a href="/hundgodis/beloningsgodis" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Belöningsgodis</a></li> 542 543 544 545 <li><a href="/hundgodis/allergi-hundgodis" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Allergi</a></li> 546 547 548 549 <li><a href="/hundgodis/glutenfritt-hundgodis" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Glutenfritt</a></li> 550 551 552 553 <li><a href="/hundgodis/fettsnalt-hundgodis" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">FettsnÃ¥lt</a></li> 554 555 556 557 <li><a href="/hundgodis/spannmalsfritt-hundgodis" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">SpannmÃ¥lsfritt</a></li> 558 559 560 561 <li><a href="/hundgodis/dentalgodis" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Dentalgodis</a></li> 562 563 564 565 <li><a href="/hundgodis/seniorhundgodis" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Senior</a></li> 566 567 568 569 <li><a href="/hundgodis/valp-hundgodis" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Valp</a></li> 570 571 572 573 <li><a href="/hundgodis/mini" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Mini</a></li> 574 575 576 577 <li><a href="/hundgodis/jumbo" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Jumbo</a></li> 578 579 580 581 <li><a href="/ovrigt" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Ãvrigt</a></li> 582 583 </ul> 584 </li> 585 586 587 588 <li x-data="dropdown" @mouseover="open = navbarLevelsDepth >= 1 && true" @mouseleave="open = false" class=" mx-2 lg:mx-4 group"> 589 <div class="relative flex items-center justify-center"> 590 <a href="/tillskott" id="menu-item-105" 591 class="flex py-2 px-1 transition ease-in-out duration-default hover:opacity-70 ">
592 <span>Tillskott</span> 593 </a> 594 <svg x-show="navbarLevelsDepth >= 1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" 595 stroke-width="1.5" stroke="currentColor" 596 class="w-4 h-4 cursor-pointer transform duration-default rotate-90 " :class="{'rotate-0' : open}"> 597 <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"></path> 598 </svg> 599 </div> 600 601 <!-- First drop down --> 602 <ul x-cloak 603 x-show="open" 604 " 605 x-transition:enter='transition ease-out transform' 606 x-transition:enter-start='opacity-0 -translate-y-4' 607 x-transition:enter-end='opacity-100 translate-y-0' 608 x-transition:leave='transition ease-in transform' 609 x-transition:leave-start='opacity-100 translate-y-0' 610 x-transition:leave-end='opacity-0 -translate-y-4' 611 class="absolute z-50 w-full max-w-xs px-6 py-2 bg-header shadow-md roundednone duration-default" aria-labelledby="menu-item-105"> 612 613 614 <li><a href="/tillskott/26-munhygien" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Munhygien</a></li> 615 616 617 618 <li><a href="/tillskott/pals-hud" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Päls & Hud</a></li> 619 620 621 622 <li><a href="/tillskott/leder-rorlighet" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Leder & Rörlighet</a></li> 623 624 625 626 <li><a href="/tillskott/mage" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Mage & Tarm</a></li> 627 628 629 630 <li><a href="/tillskott/multivitamin" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Multivitamin</a></li> 631 632 633 634 <li><a href="/tillskott/omega" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Omega</a></li> 635 636 637 638 <li><a href="/tillskott/allmanhalsa" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Allmänhälsa</a></li> 639 640 641 642 <li><a href="/tillskott/valp-tillskott" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Valp</a></li> 643 644 645 646 <li><a href="/tillskott/lugnande" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Lugnande</a></li> 647 648 </ul> 649 </li> 650 651 652 653 <li x-data="dropdown" @mouseover="open = navbarLevelsDepth >= 1 && true" @mouseleave="open = false" class=" mx-2 lg:mx-4 group"> 654 <div class="relative flex items-center justify-center"> 655 <a href="/tillbehor" id="menu-item-107" 656 class="flex py-2 px-1 transition ease-in-out duration-default hover:opacity-70 "> 657 <span>Tillbehör</span> 658 </a> 659 <svg x-show="navbarLevelsDepth >= 1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" 660 stroke-width="1.5" stroke="currentColor" 661 class="w-4 h-4 cursor-pointer transform duration-default rotate-90 " :class="{'rotate-0' : open}"> 662 <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"></path> 663 </svg> 664 </div> 665 666 <!-- First drop down --> 667 <ul x-cloak 668 x-show="open" 669 " 670 x-transition:enter='transition ease-out transform' 671 x-transition:enter-start='opacity-0 -translate-y-4' 672 x-transition:enter-end='opacity-100 translate-y-0' 673 x-transition:leave='transition ease-in transform' 674 x-transition:leave-start='opacity-100 translate-y-0' 675 x-transition:leave-end='opacity-0 -translate-y-4' 676 class="absolute z-50 w-full max-w-xs px-6 py-2 bg-header shadow-md roundednone duration-default" aria-labelledby="menu-item-107"> 677 678 679 <li><a href="/tillbehor/hud-palsvard" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Hud & Pälsvård</a></li> 680 681 682 683 <li><a href="/tillbehor/bajspasar" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Bajspåsar</a></li> 684 685 686 687 <li><a href="/tillbehor/munhygien" class="block py-3 transition ease-in-out duration-default hover:opacity-70 ">Munhygien</a></li> 688 689 </ul> 690 </li> 691 692 693 <li class=" mx-2 lg:mx-4"> 694 <a class="block py-2 px-1 transition ease-in-out duration-default hover:opacity-70 " href="/erbjudanden" 695 title="Erbjudanden">
696 <span>Erbjudanden</span> 697 </a> 698 </li> 699 700 701 702 <li class=" mx-2 lg:mx-4"> 703 <a class="block py-2 px-1 transition ease-in-out duration-default hover:opacity-70 " href="/sidor/information" 704 title="Information"> 705 <span>Information</span> 706 </a> 707 </li> 708 709 710 711 <li class=" mx-2 lg:mx-4"> 712 <a class="block py-2 px-1 transition ease-in-out duration-default hover:opacity-70 " href="/blogg" 713 title="Blogg"> 714 <span>Blogg</span> 715 </a> 716 </li> 717 718 719 720 <li class=" mx-2 lg:mx-4"> 721 <a class="block py-2 px-1 transition ease-in-out duration-default hover:opacity-70 " href="/presentkort" 722 title="Presentkort"> 723 <span>Presentkort</span> 724 </a> 725 </li> 726 727 728 729 </ul> 730 </div> 731 </div> 732 </nav> 733 734 <!-- Menu --> 735 <div 736 x-cloak 737 @keydown.escape="navMenuOpen = false" 738 x-show="navMenuOpen" 739 class="navbar-menu fixed top-0 left-0 bottom-0 w-full sm:max-w-sm z-50"> 740 741 <!-- Overlay --> 742 <div 743 x-show='navMenuOpen' 744 @click='navMenuOpen = false' 745 x-transition:enter='transition ease-out duration-default transform' 746 x-transition:enter-start='opacity-0' 747 x-transition:enter-end='opacity-100' 748 x-transition:leave='transition ease-in duration-default transform' 749 x-transition:leave-start='opacity-100' 750 x-transition:leave-end='opacity-0' 751 class='fixed inset-0 transition-opacity bg-overlay bg-opacity-40' 752 aria-hidden='true' 753 ></div> 754 755 <!-- Menu --> 756 <nav 757 x-show='navMenuOpen' 758 @keydown.escape="navMenuOpen = false" 759 class="relative flex flex-col h-full bg-body" 760 x-transition:enter='transition ease-out duration-default transform' 761 x-transition:enter-start='opacity-0 -translate-x-10 sm:-translate-x-10' 762 x-transition:enter-end='opacity-100 translate-x-0' 763 x-transition:leave='transition ease-in duration-default transform' 764 x-transition:leave-start='opacity-100 translate-x-0' 765 x-transition:leave-end='opacity-0 -translate-x-10 sm:-translate-x-10' 766 > 767 <div class="flex items-center justify-between mb-12 p-6"> 768 769 <!-- Logo --> 770 <a class="items-center" href="https://vecanis.se"> 771 772 <img alt="Vecanis" class="img-fluid img-logo w-32" 773 ix-src="https://cdn.quickbutik.com/images/2666L/templates/lua/assets/logo.png?s=96971&auto=format"> 774 </a> 775 776 <!-- Close button --> 777 <button @click="navMenuOpen = false" class="inline-block navbar-close focus:outline-none" type="button" 778 aria-label="Close"> 779 <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" 780 stroke="currentColor" class="w-6 h-6"> 781 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path> 782 </svg></button> 783 </div> 784 785 <!-- Menu --> 786 <div class="px-6 overflow-y-auto"> 787 788 <ul> 789 790 <li x-data="{expanded:false}" class="active"> 791 <div class="flex"> 792 <a href="/hundmat" 793 class="flex flex-1 py-3 font-bold hover:text-gray-900 active"> 794 <span id="navMenuItem8">Hundmat</span> 795 </a> 796 <div class="flex h-full flex-col py-3"> 797 <svg @click="expanded = !expanded" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" 798 stroke="currentColor" class="w-6 h-6 cursor-pointer transform duration-default" :class="{'rotate-90' : expanded}"> 799 <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"></path> 800 </svg> 801 </div> 802 </div> 803 <ul x-show="expanded" 804 x-transition:enter='transition ease-out duration-default transform' 805 x-transition:enter-start='opacity-0 -translate-y-4' 806 x-transition:enter-end='opacity-100 translate-y-0' 807 x-transition:leave='transition ease-in duration-default transform' 808 x-transition:leave-start='opacity-100 translate-y-0' 809 x-transition:leave-end='opacity-0 -translate-y-4' 810 class="pl-4" aria-labelledby="navMenuItem8"> 811 812 813 <li><a href="/hundmat/topping" class="block py-3 ">Topping</a></li> 814 815 816 817 <li><a href="/hundmat/blotmat" class="block py-3 ">Blötmat</a></li> 818 819 820 821 <li><a href="/hundmat/torrfoder" class="block py-3 ">Torrfoder</a></li> 822 823 824 825 <li><a href="/hundmat/allergivanlig-hundmat" class="block py-3 ">Allergi</a></li> 826 827 828 829 <li><a href="/hundmat/glutenfritt" class="block py-3 ">Glutenfritt</a></li> 830 831 832 833 <li><a href="/hundmat/fettsnalt" class="block py-3 ">Fettsnålt</a></li> 834 835 836 837 <li><a href="/hundmat/spannmalsfritt" class="block py-3 ">Spannmålsfritt</a></li> 838 839 840 841 <li><a href="/hundmat/senior" class="block py-3 ">Senior</a></li> 842 843 844 845 <li><a href="/hundmat/valp" class="block py-3 ">Valp</a></li> 846 847 848 849 <li><a href="/hundmat/mini" class="block py-3 ">Mini</a></li> 850 851 852 </ul> 853 </li> 854 </ul> 855 856 857 <ul> 858 859 <li x-data="{expanded:false}" class=""> 860 <div class="flex"> 861 <a href="/hundgodis" 862 class="flex flex-1 py-3 font-bold hover:text-gray-900 ">
863 <span id="navMenuItem146">Hundgodis</span> 864 </a> 865 <div class="flex h-full flex-col py-3"> 866 <svg @click="expanded = !expanded" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" 867 stroke="currentColor" class="w-6 h-6 cursor-pointer transform duration-default" :class="{'rotate-90' : expanded}"> 868 <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"></path> 869 </svg> 870 </div> 871 </div> 872 <ul x-show="expanded" 873 x-transition:enter='transition ease-out duration-default transform' 874 x-transition:enter-start='opacity-0 -translate-y-4' 875 x-transition:enter-end='opacity-100 translate-y-0' 876 x-transition:leave='transition ease-in duration-default transform' 877 x-transition:leave-start='opacity-100 translate-y-0' 878 x-transition:leave-end='opacity-0 -translate-y-4' 879 class="pl-4" aria-labelledby="navMenuItem146"> 880 881 882 <li><a href="/hundgodis/tuggben" class="block py-3 ">Tuggben</a></li> 883 884 885 886 <li><a href="/hundgodis/beloningsgodis" class="block py-3 ">Belöningsgodis</a></li> 887 888 889 890 <li><a href="/hundgodis/allergi-hundgodis" class="block py-3 ">Allergi</a></li> 891 892 893 894 <li><a href="/hundgodis/glutenfritt-hundgodis" class="block py-3 ">Glutenfritt</a></li> 895 896 897 898 <li><a href="/hundgodis/fettsnalt-hundgodis" class="block py-3 ">FettsnÃ¥lt</a></li> 899 900 901 902 <li><a href="/hundgodis/spannmalsfritt-hundgodis" class="block py-3 ">SpannmÃ¥lsfritt</a></li> 903 904 905 906 <li><a href="/hundgodis/dentalgodis" class="block py-3 ">Dentalgodis</a></li> 907 908 909 910 <li><a href="/hundgodis/seniorhundgodis" class="block py-3 ">Senior</a></li> 911 912 913 914 <li><a href="/hundgodis/valp-hundgodis" class="block py-3 ">Valp</a></li> 915 916 917 918 <li><a href="/hundgodis/mini" class="block py-3 ">Mini</a></li> 919 920 921 922 <li><a href="/hundgodis/jumbo" class="block py-3 ">Jumbo</a></li> 923 924 925 926 <li><a href="/ovrigt" class="block py-3 ">Ãvrigt</a></li> 927 928 929 </ul> 930 </li> 931 </ul> 932 933 934 <ul> 935 936 <li x-data="{expanded:false}" class=""> 937 <div class="flex"> 938 <a href="/tillskott" 939 class="flex flex-1 py-3 font-bold hover:text-gray-900 "> 940 <span id="navMenuItem105">Tillskott</span> 941 </a> 942 <div class="flex h-full flex-col py-3"> 943 <svg @click="expanded = !expanded" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" 944 stroke="currentColor" class="w-6 h-6 cursor-pointer transform duration-default" :class="{'rotate-90' : expanded}"> 945 <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"></path> 946 </svg> 947 </div> 948 </div> 949 <ul x-show="expanded" 950 x-transition:enter='transition ease-out duration-default transform' 951 x-transition:enter-start='opacity-0 -translate-y-4' 952 x-transition:enter-end='opacity-100 translate-y-0' 953 x-transition:leave='transition ease-in duration-default transform' 954 x-transition:leave-start='opacity-100 translate-y-0' 955 x-transition:leave-end='opacity-0 -translate-y-4' 956 class="pl-4" aria-labelledby="navMenuItem105"> 957 958 959 <li><a href="/tillskott/26-munhygien" class="block py-3 ">Munhygien</a></li> 960 961 962 963 <li><a href="/tillskott/pals-hud" class="block py-3 ">Päls & Hud</a></li> 964 965 966 967 <li><a href="/tillskott/leder-rorlighet" class="block py-3 ">Leder & Rörlighet</a></li> 968 969 970 971 <li><a href="/tillskott/mage" class="block py-3 ">Mage & Tarm</a></li> 972 973 974 975 <li><a href="/tillskott/multivitamin" class="block py-3 ">Multivitamin</a></li> 976 977 978 979 <li><a href="/tillskott/omega" class="block py-3 ">Omega</a></li> 980 981 982 983 <li><a href="/tillskott/allmanhalsa" class="block py-3 ">Allmänhälsa</a></li> 984 985 986 987 <li><a href="/tillskott/valp-tillskott" class="block py-3 ">Valp</a></li> 988 989 990 991 <li><a href="/tillskott/lugnande" class="block py-3 ">Lugnande</a></li> 992 993 994 </ul> 995 </li> 996 </ul> 997 998 999 <ul> 1000 1001 <li x-data="{expanded:false}" class=""> 1002 <div class="flex"> 1003 <a href="/tillbehor" 1004 class="flex flex-1 py-3 font-bold hover:text-gray-900 ">
1005 <span id="navMenuItem107">Tillbehör</span> 1006 </a> 1007 <div class="flex h-full flex-col py-3"> 1008 <svg @click="expanded = !expanded" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" 1009 stroke="currentColor" class="w-6 h-6 cursor-pointer transform duration-default" :class="{'rotate-90' : expanded}"> 1010 <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"></path> 1011 </svg> 1012 </div> 1013 </div> 1014 <ul x-show="expanded" 1015 x-transition:enter='transition ease-out duration-default transform' 1016 x-transition:enter-start='opacity-0 -translate-y-4' 1017 x-transition:enter-end='opacity-100 translate-y-0' 1018 x-transition:leave='transition ease-in duration-default transform' 1019 x-transition:leave-start='opacity-100 translate-y-0' 1020 x-transition:leave-end='opacity-0 -translate-y-4' 1021 class="pl-4" aria-labelledby="navMenuItem107"> 1022 1023 1024 <li><a href="/tillbehor/hud-palsvard" class="block py-3 ">Hud & Pälsvård</a></li> 1025 1026 1027 1028 <li><a href="/tillbehor/bajspasar" class="block py-3 ">Bajspåsar</a></li> 1029 1030 1031 1032 <li><a href="/tillbehor/munhygien" class="block py-3 ">Munhygien</a></li> 1033 1034 1035 </ul> 1036 </li> 1037 </ul> 1038 1039 1040 <ul> 1041 <li class=""> 1042 <a class="block py-3 font-bold hover:text-gray-900 " href="/erbjudanden" 1043 title="Erbjudanden"> 1044 <span>Erbjudanden</span> 1045 </a> 1046 </li> 1047 1048 </ul> 1049 1050 1051 <ul> 1052 <li class=""> 1053 <a class="block py-3 font-bold hover:text-gray-900 " href="/sidor/information" 1054 title="Information"> 1055 <span>Information</span> 1056 </a> 1057 </li> 1058 1059 </ul> 1060 1061 1062 <ul> 1063 <li class=""> 1064 <a class="block py-3 font-bold hover:text-gray-900 " href="/blogg" 1065 title="Blogg"> 1066 <span>Blogg</span> 1067 </a> 1068 </li> 1069 1070 </ul> 1071 1072 1073 <ul> 1074 <li class=""> 1075 <a class="block py-3 font-bold hover:text-gray-900 " href="/presentkort" 1076 title="Presentkort"> 1077 <span>Presentkort</span> 1078 </a> 1079 </li> 1080 1081 </ul> 1082 1083 </div> 1084 1085 <div class="p-6 mt-auto"> 1086 1087 <!-- Login --> 1088 <a class="block px-6 py-3 text-center button-rounding bg-foreground text-white font-bold bg-fore hover:opacity-90" 1089 href="/customer/login">Logga in</a> 1090 1091 </div> 1092 </nav> 1093 </div> <!-- Settings modal --> 1094 <template x-teleport='body'> 1095 <div 1096 x-data 1097 x-cloak 1098 x-show='$store.settings.settingsOpen' 1099 @keydown.escape='$store.settings.hideSettings()' 1100 class='fixed inset-0 z-50 overflow-y-auto' 1101 aria-labelledby='modal-title' 1102 role='dialog' 1103 aria-modal='true' 1104 > 1105 <div class='flex items-end justify-center min-h-screen text-center md:items-center sm:block'> 1106 1107 <!-- Overlay --> 1108 <div 1109 @click='$store.settings.hideSettings()' 1110 x-show='$store.settings.settingsOpen' 1111 x-transition:enter='transition ease-out duration-default transform' 1112 x-transition:enter-start='opacity-0' 1113 x-transition:enter-end='opacity-100' 1114 x-transition:leave='transition ease-in duration-default transform' 1115 x-transition:leave-start='opacity-100' 1116 x-transition:leave-end='opacity-0' 1117 class='fixed inset-0 transition-opacity bg-overlay bg-opacity-40' 1118 aria-hidden='true'></div> 1119 1120 <!-- Modal --> 1121 <div x-show='$store.settings.settingsOpen' 1122 x-transition:enter='transition ease-out duration-default transform' 1123 x-transition:enter-start='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95' 1124 x-transition:enter-end='opacity-100 translate-y-0 sm:scale-100' 1125 x-transition:leave='transition ease-in duration-default transform' 1126 x-transition:leave-start='opacity-100 translate-y-0 sm:scale-100' 1127 x-transition:leave-end='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95' 1128 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 roundednone shadow-xl 2xl:max-w-2xl'> 1129 1130 <!-- Header --> 1131 <header class='flex items-center justify-between p-6 mb-2'> 1132 1133 <!-- Title -->
1134 <span class='text-3xl font-bold font-heading'>Inställningar</span> 1135 1136 <!-- Close button --> 1137 <button @click='$store.settings.hideSettings()' class='text-gray-600 focus:outline-none hover:text-gray-700'> 1138 <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"> 1139 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path> 1140 </svg> 1141 </button> 1142 </header> 1143 1144 <div class="px-6 pb-6 mb-6 h-full overflow-y-auto"> 1145 1146 <!-- Languages --> 1147 <div class="mb-4"> 1148 <h4 class="text-lg font-heading font-bold">Språk</h4> 1149 <div> 1150 <a href="https://vecanis.se/hundmat" class="flex mt-2 px-4 py-2 bg-primary text-white button-rounding hover:bg-gray-200 duration-default transition-shadow"> 1151 <img loading="lazy" class="w-auto" src="/assets/shopassets/flags/24/sv.png" alt="sv Flag"> 1152 <span class="pl-2">Svenska</span> 1153 </a> 1154 <a href="https://vecanis.se/en/hundmat" class="flex mt-2 px-4 py-2 bg-gray-100 button-rounding hover:bg-gray-200 duration-default transition-shadow"> 1155 <img loading="lazy" class="w-auto" src="/assets/shopassets/flags/24/en.png" alt="en Flag"> 1156 <span class="pl-2">English</span> 1157 </a> 1158 </div> 1159 </div> 1160 1161 <!-- Tax --> 1162 1163 <!-- Currencies--> 1164 <div> 1165 <h4 class="text-lg font-heading font-bold">Valuta</h4> 1166 <div class="flex flex-wrap"> 1167 <a href="https://vecanis.se/hundmat?currency=DKK" 1168 class="dropdown-item mt-2 mr-2 px-4 py-2 bg-gray-100 button-rounding hover:bg-gray-200 duration-default transition-colors">DKK</a> 1169 <a href="https://vecanis.se/hundmat?currency=EUR" 1170 class="dropdown-item mt-2 mr-2 px-4 py-2 bg-gray-100 button-rounding hover:bg-gray-200 duration-default transition-colors">EUR</a> 1171 <a href="https://vecanis.se/hundmat?currency=SEK" 1172 class="dropdown-item mt-2 mr-2 px-4 py-2 bg-primary text-white button-rounding hover:bg-gray-200 duration-default transition-colors">SEK</a> 1173 </div> 1174 </div> 1175 1176 </div> 1177 </div> 1178 </div> 1179 </div> 1180 </template> <!-- Search modal --> 1181 <template x-teleport='body'> 1182 <div 1183 x-data='appSearchModal' 1184 x-cloak 1185 x-show='$store.search.searchOpen' 1186 @keydown.escape='$store.search.hideSearch()' 1187 class='fixed inset-0 z-50 overflow-y-auto' 1188 aria-labelledby='modal-title' 1189 role='dialog' 1190 aria-modal='true' 1191 > 1192 <div class='flex items-end justify-center min-h-screen px-4 text-center md:items-center sm:block sm:p-0'> 1193 1194 <!-- Overlay --> 1195 <div 1196 @click='$store.search.hideSearch()' 1197 x-show='$store.search.searchOpen' 1198 x-transition:enter='transition ease-out duration-default transform' 1199 x-transition:enter-start='opacity-0' 1200 x-transition:enter-end='opacity-100' 1201 x-transition:leave='transition ease-in duration-default transform' 1202 x-transition:leave-start='opacity-100' 1203 x-transition:leave-end='opacity-0' 1204 class='fixed inset-0 transition-opacity bg-overlay bg-opacity-40' 1205 aria-hidden='true' 1206 ></div> 1207 1208 <!-- Modal --> 1209 <div 1210 modal="search" 1211 x-show='$store.search.searchOpen' 1212 x-transition:enter='transition ease-out duration-default transform' 1213 x-transition:enter-start='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95' 1214 x-transition:enter-end='opacity-100 translate-y-0 sm:scale-100' 1215 x-transition:leave='transition ease-in duration-default transform' 1216 x-transition:leave-start='opacity-100 translate-y-0 sm:scale-100' 1217 x-transition:leave-end='opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95' 1218 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' 1219 style='@media (min-width: 576px) { max-height: calc(80vh) }' 1220 > 1221 <div class="pointer-events-auto relative flex w-full flex-col max-h-full overflow-hidden border-none bg-body shadow-xl"> 1222 1223 <!-- Header --> 1224 <header class='flex flex-col flex-shrink-0 p-6'> 1225 1226 <div class="flex items-center justify-between pb-6"> 1227 1228 <!-- Title -->
1229 <span class='text-3xl font-bold font-heading'>Sök</span> 1230 1231 <!-- Close button --> 1232 <button 1233 @click='$store.search.hideSearch()' 1234 class='text-gray-600 focus:outline-none hover:text-gray-700'> 1235 <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'> 1236 <path stroke-linecap='round' stroke-linejoin='round' d='M6 18L18 6M6 6l12 12'></path> 1237 </svg> 1238 </button> 1239 </div> 1240 1241 <form 1242 action='/shop/search' 1243 method='get' class='flex'> 1244 <div class='flex w-full relative'> 1245 <div class="flex h-full flex-col justify-center absolute top-0 left-0 pl-2"> 1246 <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"> 1247 <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" /> 1248 </svg> 1249 </div> 1250 <input 1251 type='search' 1252 name='s' 1253 x-model='searchTerm' 1254 id='searchInput' 1255 @input.debounce.500ms="search" 1256 placeholder='Sök produkter' 1257 aria-label='Search' 1258 aria-describedby='button-search-desktop' 1259 class='flex flex-1 py-3 pr-3 pl-14 input-bg input-text input-border border input-rounding focus:outline-none' 1260 > 1261 </div> 1262 </form> 1263 </header> 1264 1265 <!-- No results found --> 1266 <div x-show='searchTerm && noResults' class="px-6 mb-8"> 1267 <p class="mt-8 text-center"> 1268 Inga sökresultat hittades 1269 </p> 1270 </div> 1271 1272 <!-- Search results --> 1273 <div x-show='!noResults && searchTerm != ""' class="relative overflow-y-auto pb-6"> 1274 <template x-for='result in results'> 1275 <a :href='result.product.url'> 1276 <div class="flex py-2 px-6 border-b border-gray-100 hover:bg-gray-100"> 1277 <div class="flex flex-0 w-16 h-16 mr-6"> 1278 <img :src="result.product.firstimage + '?w=100&h=100'" ix-sizes="auto" :alt="result.product.title" class="w-full object-contain object-center"> 1279 </div> 1280 <div class="flex flex-1 flex-col justify-center"> 1281 <span x-html='highlightTitle(result.product.title)' class=""></span> 1282 </div> 1283 <div class="flex flex-col justify-center"> 1284 <span x-text='result.product.price' class=""></span> 1285 </div> 1286 </div> 1287 </a> 1288 </template> 1289 </div> 1290 1291 </div> 1292 1293 </div> 1294 </div> 1295 </div> 1296 </template> 1297 1298 </section> 1299 1300 1301 <!-- Breadcrumbs --> 1302 <section data-qb-area="general" data-qb-setting="show_breadcrumbs" class="py-4"> 1303 <div class="container mx-auto px-4"> 1304 <ul class="flex flex-wrap justify-center"> 1305 1306 <li class="flex items-center mr-3"> 1307 1308 1309 <a class="inline-block mr-3 text-sm opacity-70" href="/">Hem</a> 1310 <span class="inline-block opacity-40"> 1311 <svg class="h-2 w-2" viewbox="0 0 6 11" fill="none" xmlns="http://www.w3.org/2000/svg"> 1312 <path d="M0.131073 0.755835C-0.0436907 0.581885 -0.0436907 0.302414 0.131073 0.129744C0.305837 -0.0429273 0.588197 -0.
13120435662 0.76296 0.129744L5.86893 5.18707C6.04369 5.35975 6.04369 5.63922 5.86893 5.81317L0.762959 10.8705C0.588196 11.0432 0.305836 11.0432 0.131072 10.8705C-0.0436916 10.6972 -0.0436916 10.4171 0.131072 10.2444L4.78774 5.4998L0.131073 0.755835Z" fill="#151515"></path> 1313 </svg> 1314 </span> 1315 1316 </li> 1317 <li class="flex items-center mr-3"> 1318 1319 <p class="inline-block text-sm opacity-70" href="#">Hundmat</p> 1320 1321 1322 </li> 1323 1324 </ul> 1325 </div> 1326 </section> 1327 1328 <section class="py-16" data-qb-category-id="2"> 1329 <div class="container mx-auto px-4"> 1330 1331 <!-- Title --> 1332 <h1 class="mb-8 font-heading text-center text-4xl"> 1333 Hundmat 1334 </h1> 1335 1336 <!-- Description --> 1337 <div class="mx-auto max-w-3xl text-lg mb-16 applyTailwindClasses"> 1338 <h1 style="text-align: center;"><span style="font-size: 28px;">Växtbaserad hundmat - Ett sunt och klimatsmart val</span></h1><p style="text-align: justify;">Utforska vårt utbud av växtbaserad klimatvänlig hundmat och ta steget mot en hållbarare och miljövänligare livsstil för din fyrbenta vän. Vår noggrant utvalda hundmat är inte bara snäll mot miljön utan erbjuder också en näringsrik och balanserad kost för din hund. Genom att välja växtbaserade alternativ kan du minska din husdjurs klimatpåverkan och främja en mer hållbar framtid. Ge din hund det bästa samtidigt som du tar ett steg mot att minska den ekologiska fotavtrycket. Upptäck våra klimatvänliga alternativ och skäm bort din hund med omtanke om både dess hälsa och vår planet.</p> 1339 </div> 1340 1341 <!-- Categories --> 1342 1343 <hr class="border-foreground opacity-20 mt-16"> 1344 1345 <!-- Products filter and sort --> 1346 <div class="w-full my-6 flex justify-between"> 1347 <div class="gap-2 flex justify-end" data-qb-selector="filter-container"></div> 1348 <div class="flex items-start"> 1349 <div class="flex justify-end" data-qb-selector="sort-container"></div> 1350 </div> 1351 </div> 1352 1353 <div class="flex flex-wrap mt-6"> 1354 <div class="w-full"> 1355 <div id="productList" class="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-4 gap-default"> 1356 1357 <!-- Products --> 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 <div x-data="{ layoutSingle: '4' == '1' }" 1372 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 1373 1374 data-qb-selector="product-item" 1375 data-product-id="989" 1376 1377 data-pid="989" 1378 data-s-title="Utgånget! VEGDOG Vuxen Linser & Hirs 800g" 1379 data-s-price="59.00" 1380 data-price="59.00" 1381 > 1382 1383 <!-- Image --> 1384 <a class="block relative w-full " href="/hundmat/22-kopia-vegdog-vuxen-linser-hirs-400g800g"> 1385 <div class="relative group overflow-hidden rounded-tnone"> 1386 <img loading="lazy" 1387 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 1388 ix-src="https://cdn.quickbutik.com/images/2666L/products/178713818815f0eec1656a138de4e49.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Utgånget! VEGDOG Vuxen Linser & Hirs 800g"> 1389 1390 <img loading="lazy" 1391 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 1392 ix-src="https://cdn.quickbutik.com/images/2666L/products/17871381893764a81b656a138de15dc.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Utgånget! VEGDOG Vuxen Linser & Hirs 800g"> 1393 1394 </div> 1395 </a> 1396 1397 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 1398 <a class="block px-6 mb-4" href="/hundmat/22-kopia-vegdog-vuxen-linser-hirs-400g800g"> 1399 1400 <!-- Title --> 1401 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 1402 Utgånget! VEGDOG Vuxen Linser & Hirs 800g 1403 </h3> 1404 1405 <!-- Reviews container --> 1406 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 1407 1408 </a> 1409 1410 <!-- Description --> 1411 <template x-if="layoutSingle"> 1412 1413 </template> 1414 1415 <div> 1416 1417 <!-- Price --> 1418 <div class="px-6"> 1419 1420 <!-- Current price -->
1421 <span> 1422 <span class="text-xl font-bold text-accent">59 SEK</span> 1423 </span> 1424 1425 <!-- Original price --> 1426 1427 </div> 1428 1429 <!-- Add to basket --> 1430 <div class="flex justify-between gap-2 px-6 mt-2"> 1431 1432 <!-- Buy button --> 1433 1434 <!-- No options --> 1435 1436 <!-- Has options --> 1437 <a href="/hundmat/22-kopia-vegdog-vuxen-linser-hirs-400g800g" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 1438 1439 <!-- Icon (arrow) --> 1440 1441 <!-- Text --> 1442 Läs mer 1443 1444 </a> 1445 1446 <!-- Sold out --> 1447 1448 1449 1450 1451 <!-- Link to product when no buy button --> 1452 1453 <!-- Wishlist --> 1454 <div data-qb-selector="product-item-wishlist" data-product-id="989" class="flex items-center"></div> 1455 1456 </div> 1457 1458 <!-- Stock status --> 1459 <div class="text-sm px-6 pt-4"> 1460 1461 <!-- Sold out --> 1462 1463 <!-- Not sold out --> 1464 <span data-in-stock-text="I lager" 1465 data-out-of-stock-text="" 1466 data-combination-not-exist-text="Variant finns ej"> 1467 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 1468 </span> 1469 1470 </div> 1471 1472 </div> 1473 </div> 1474 </div> 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 <div x-data="{ layoutSingle: '4' == '1' }" 1490 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 1491 1492 data-qb-selector="product-item" 1493 data-product-id="984" 1494 1495 data-pid="984" 1496 data-s-title="Omni Sensitive Adult" 1497 data-s-price="249.00" 1498 data-price="249.00" 1499 > 1500 1501 <!-- Image --> 1502 <a class="block relative w-full " href="/hundmat/omni-sensitive-adult-dry-food-2kg6kg10kg"> 1503 <div class="relative group overflow-hidden rounded-tnone"> 1504 <img loading="lazy" 1505 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 1506 ix-src="https://cdn.quickbutik.com/images/2666L/products/6a4621605e1e0.webp" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni Sensitive Adult"> 1507 1508 <img loading="lazy" 1509 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 1510 ix-src="https://cdn.quickbutik.com/images/2666L/products/6a462165757b9.webp" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni Sensitive Adult"> 1511 1512 </div> 1513 </a> 1514 1515 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 1516 <a class="block px-6 mb-4" href="/hundmat/omni-sensitive-adult-
1516dry-food-2kg6kg10kg"> 1517 1518 <!-- Title --> 1519 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 1520 Omni Sensitive Adult 1521 </h3> 1522 1523 <!-- Reviews container --> 1524 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 1525 1526 </a> 1527 1528 <!-- Description --> 1529 <template x-if="layoutSingle"> 1530 1531 </template> 1532 1533 <div> 1534 1535 <!-- Price --> 1536 <div class="px-6"> 1537 1538 <!-- Current price --> 1539 <span> 1540 <span class="text-xl font-bold text-accent">249 SEK</span> 1541 </span> 1542 1543 <!-- Original price --> 1544 1545 </div> 1546 1547 <!-- Add to basket --> 1548 <div class="flex justify-between gap-2 px-6 mt-2"> 1549 1550 <!-- Buy button --> 1551 1552 <!-- No options --> 1553 1554 <!-- Has options --> 1555 <a href="/hundmat/omni-sensitive-adult-dry-food-2kg6kg10kg" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 1556 1557 <!-- Icon (arrow) --> 1558 1559 <!-- Text --> 1560 Läs mer 1561 1562 </a> 1563 1564 <!-- Sold out --> 1565 1566 1567 1568 1569 <!-- Link to product when no buy button --> 1570 1571 <!-- Wishlist --> 1572 <div data-qb-selector="product-item-wishlist" data-product-id="984" class="flex items-center"></div> 1573 1574 </div> 1575 1576 <!-- Stock status --> 1577 <div class="text-sm px-6 pt-4"> 1578 1579 <!-- Sold out --> 1580 1581 <!-- Not sold out --> 1582 <span data-in-stock-text="I lager" 1583 data-out-of-stock-text="" 1584 data-combination-not-exist-text="Variant finns ej"> 1585 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 1586 </span> 1587 1588 </div> 1589 1590 </div> 1591 </div> 1592 </div> 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 <div x-data="{ layoutSingle: '4' == '1' }" 1608 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 1609 1610 data-qb-selector="product-item" 1611 data-product-id="982" 1612 1613 data-pid="982" 1614 data-s-title="Utgånget! Omni Toppers 150g" 1615 data-s-price="15.00" 1616 data-price="15.00" 1617 > 1618 1619 <!-- Image --> 1620 <a class="block relative w-full " href="/hundmat/kort-datum-omni-toppers-150g"> 1621 <div class="relative group overflow-hidden rounded-tnone"> 1622 <img loading="lazy" 1623 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 1624 ix-src="https://cdn.quickbutik.com/images/2666L/products/17821923467a6e233866e91cfeb969e.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Utgånget! Omni Toppers 150g"> 1625 1626 <img loading="lazy" 1627 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 1628 ix-src="https://cdn.quickbutik.com/images/2666L/products/1782192346e904e2e866e91d1ec37b7.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Utgånget! Omni Toppers 150g"> 1629 1630 </div> 1631 </a> 1632 1633 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 1634 <a class="block px-6 mb-4" href="/hundmat/kort-datum-omni-toppers-150g"> 1635 1636 <!-- Title --> 1637 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 1638 Utgånget! Omni Toppers 150g 1639 </h3> 1640 1641 <!-- Reviews container --> 1642 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 1643 1644 </a> 1645 1646 <!-- Description --> 1647 <template x-if="layoutSingle"> 1648 1649 </template> 1650 1651 <div> 1652 1653 <!-- Price --> 1654 <div class="px-6"> 1655 1656 <!-- Current price -->
1657 <span> 1658 <span class="text-xl font-bold text-discounted-price">15 SEK</span> 1659 </span> 1660 1661 <!-- Original price --> 1662 <span class="opacity-40 line-through mr-2"> 1663 45 SEK 1664 </span> 1665 1666 </div> 1667 1668 <!-- Add to basket --> 1669 <div class="flex justify-between gap-2 px-6 mt-2"> 1670 1671 <!-- Buy button --> 1672 1673 <!-- No options --> 1674 1675 <!-- Has options --> 1676 <a href="/hundmat/kort-datum-omni-toppers-150g" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 1677 1678 <!-- Icon (arrow) --> 1679 1680 <!-- Text --> 1681 Läs mer 1682 1683 </a> 1684 1685 <!-- Sold out --> 1686 1687 1688 1689 1690 <!-- Link to product when no buy button --> 1691 1692 <!-- Wishlist --> 1693 <div data-qb-selector="product-item-wishlist" data-product-id="982" class="flex items-center"></div> 1694 1695 </div> 1696 1697 <!-- Stock status --> 1698 <div class="text-sm px-6 pt-4"> 1699 1700 <!-- Sold out --> 1701 1702 <!-- Not sold out --> 1703 <span data-in-stock-text="I lager" 1704 data-out-of-stock-text="" 1705 data-combination-not-exist-text="Variant finns ej"> 1706 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 1707 </span> 1708 1709 </div> 1710 1711 </div> 1712 </div> 1713 </div> 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 <div x-data="{ layoutSingle: '4' == '1' }" 1729 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 1730 1731 data-qb-selector="product-item" 1732 data-product-id="979" 1733 1734 data-pid="979" 1735 data-s-title="Skadad förpackning! VEGDOG Green Crunch 10kg" 1736 data-s-price="599.00" 1737 data-price="599.00" 1738 > 1739 1740 <!-- Image --> 1741 <a class="block relative w-full " href="/hundmat/skadad-forpackning-vegdog-green-crunch-10kg"> 1742 <div class="relative group overflow-hidden rounded-tnone"> 1743 <img loading="lazy" 1744 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 1745 ix-src="https://cdn.quickbutik.com/images/2666L/products/17809207614899f69a69e9cb262c830.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Skadad förpackning! VEGDOG Green Crunch 10kg"> 1746 1747 <img loading="lazy" 1748 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 1749 ix-src="https://cdn.quickbutik.com/images/2666L/products/178092076187c645fc69e9cb1794510.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Skadad förpackning! VEGDOG Green Crunch 10kg"> 1750 1751 </div> 1752 </a> 1753 1754 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 1755 <a class="block px-6 mb-4" href="/hundmat/skadad-forpackning-vegdog-green-crunch-10kg"> 1756 1757 <!-- Title --> 1758 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 1759 Skadad förpackning! VEGDOG Green Crunch 10kg 1760 </h3> 1761 1762 <!-- Reviews container --> 1763 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 1764 1765 </a> 1766 1767 <!-- Description --> 1768 <template x-if="layoutSingle"> 1769 1770 </template> 1771 1772 <div> 1773 1774 <!-- Price --> 1775 <div class="px-6"> 1776 1777 <!-- Current price -->
1778 <span> 1779 <span class="text-xl font-bold text-accent">599 SEK</span> 1780 </span> 1781 1782 <!-- Original price --> 1783 1784 </div> 1785 1786 <!-- Add to basket --> 1787 <div class="flex justify-between gap-2 px-6 mt-2"> 1788 1789 <!-- Buy button --> 1790 1791 <!-- No options --> 1792 1793 <!-- Has options --> 1794 <a href="/hundmat/skadad-forpackning-vegdog-green-crunch-10kg" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 1795 1796 <!-- Icon (arrow) --> 1797 1798 <!-- Text --> 1799 Läs mer 1800 1801 </a> 1802 1803 <!-- Sold out --> 1804 1805 1806 1807 1808 <!-- Link to product when no buy button --> 1809 1810 <!-- Wishlist --> 1811 <div data-qb-selector="product-item-wishlist" data-product-id="979" class="flex items-center"></div> 1812 1813 </div> 1814 1815 <!-- Stock status --> 1816 <div class="text-sm px-6 pt-4"> 1817 1818 <!-- Sold out --> 1819 1820 <!-- Not sold out --> 1821 <span data-in-stock-text="I lager" 1822 data-out-of-stock-text="" 1823 data-combination-not-exist-text="Variant finns ej"> 1824 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 1825 </span> 1826 1827 </div> 1828 1829 </div> 1830 </div> 1831 </div> 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 <div x-data="{ layoutSingle: '4' == '1' }" 1847 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 1848 1849 data-qb-selector="product-item" 1850 data-product-id="935" 1851 1852 data-pid="935" 1853 data-s-title="Omni Revitalising Roast 395g" 1854 data-s-price="69" 1855 data-price="69" 1856 > 1857 1858 <!-- Image --> 1859 <a class="block relative w-full " href="/hundmat/revitalising-roast-395g"> 1860 <div class="relative group overflow-hidden rounded-tnone"> 1861 <img loading="lazy" 1862 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 1863 ix-src="https://cdn.quickbutik.com/images/2666L/products/69526d67bde09.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni Revitalising Roast 395g"> 1864 1865 <img loading="lazy" 1866 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 1867 ix-src="https://cdn.quickbutik.com/images/2666L/products/69526d6c4dd4c.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni Revitalising Roast 395g"> 1868 1869 </div> 1870 </a> 1871 1872 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 1873 <a class="block px-6 mb-4" href="/hundmat/revitalising-roast-395g"> 1874 1875 <!-- Title --> 1876 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 1877 Omni Revitalising Roast 395g 1878 </h3> 1879 1880 <!-- Reviews container --> 1881 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 1882 1883 </a> 1884 1885 <!-- Description --> 1886 <template x-if="layoutSingle"> 1887 1888 </template> 1889 1890 <div> 1891 1892 <!-- Price --> 1893 <div class="px-6"> 1894 1895 <!-- Current price -->
1896 <span> 1897 <span class="text-xl font-bold text-accent">69 SEK</span> 1898 </span> 1899 1900 <!-- Original price --> 1901 1902 </div> 1903 1904 <!-- Add to basket --> 1905 <div class="flex justify-between gap-2 px-6 mt-2"> 1906 1907 <!-- Buy button --> 1908 1909 <!-- No options --> 1910 <button @click="$store.cart.add({ 1911 productID: 935, 1912 quantity: 1 1913 })" 1914 aria-label="Lägg i korgen" 1915 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 1916 1917 <!-- Icon (cart) --> 1918 1919 <!-- Text --> 1920 Lägg i korgen 1921 1922 </button> 1923 1924 <!-- Has options --> 1925 1926 <!-- Sold out --> 1927 1928 1929 1930 1931 <!-- Link to product when no buy button --> 1932 1933 <!-- Wishlist --> 1934 <div data-qb-selector="product-item-wishlist" data-product-id="935" class="flex items-center"></div> 1935 1936 </div> 1937 1938 <!-- Stock status --> 1939 <div class="text-sm px-6 pt-4"> 1940 1941 <!-- Sold out --> 1942 1943 <!-- Not sold out --> 1944 <span data-in-stock-text="I lager" 1945 data-out-of-stock-text="" 1946 data-combination-not-exist-text="Variant finns ej"> 1947 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 1948 </span> 1949 1950 </div> 1951 1952 </div> 1953 </div> 1954 </div> 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 <div x-data="{ layoutSingle: '4' == '1' }" 1970 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 1971 1972 data-qb-selector="product-item" 1973 data-product-id="934" 1974 1975 data-pid="934" 1976 data-s-title="Omni Chickpea & Potato Paté 395g" 1977 data-s-price="69" 1978 data-price="69" 1979 > 1980 1981 <!-- Image --> 1982 <a class="block relative w-full " href="/hundmat/omni-comforting-casserole"> 1983 <div class="relative group overflow-hidden rounded-tnone"> 1984 <img loading="lazy" 1985 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 1986 ix-src="https://cdn.quickbutik.com/images/2666L/products/6a4615b07d4bc.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni Chickpea & Potato Paté 395g"> 1987 1988 <img loading="lazy" 1989 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 1990 ix-src="https://cdn.quickbutik.com/images/2666L/products/6a4615b674ccb.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni Chickpea & Potato Paté 395g"> 1991 1992 </div> 1993 </a> 1994 1995 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 1996 <a class="block px-6 mb-4" href="/hundmat/omni-comforting-casserole"> 1997 1998 <!-- Title --> 1999 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 2000 Omni Chickpea & Potato Paté 395g 2001 </h3> 2002 2003 <!-- Reviews container --> 2004 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 2005 2006 </a> 2007 2008 <!-- Description --> 2009 <template x-if="layoutSingle"> 2010 2011 </template> 2012 2013 <div> 2014 2015 <!-- Price --> 2016 <div class="px-6"> 2017 2018 <!-- Current price -->
2019 <span> 2020 <span class="text-xl font-bold text-accent">69 SEK</span> 2021 </span> 2022 2023 <!-- Original price --> 2024 2025 </div> 2026 2027 <!-- Add to basket --> 2028 <div class="flex justify-between gap-2 px-6 mt-2"> 2029 2030 <!-- Buy button --> 2031 2032 <!-- No options --> 2033 2034 <!-- Has options --> 2035 2036 <!-- Sold out --> 2037 <a href="/hundmat/omni-comforting-casserole" area-label="Ej i lager" class="block w-full button-py px-2 bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 2038 2039 <!-- Icon --> 2040 2041 <!-- Text --> 2042 Läs mer 2043 2044 </a> 2045 2046 2047 2048 2049 <!-- Link to product when no buy button --> 2050 2051 <!-- Wishlist --> 2052 <div data-qb-selector="product-item-wishlist" data-product-id="934" class="flex items-center"></div> 2053 2054 </div> 2055 2056 <!-- Stock status --> 2057 <div class="text-sm px-6 pt-4"> 2058 2059 <!-- Sold out --> 2060 <span> 2061 <span class="inline-block bg-red-600 rounded-full h-2 w-2 mr-1"></span> Ej i lager 2062 </span> 2063 2064 <!-- Not sold out --> 2065 2066 </div> 2067 2068 </div> 2069 </div> 2070 </div> 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 <div x-data="{ layoutSingle: '4' == '1' }" 2086 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 2087 2088 data-qb-selector="product-item" 2089 data-product-id="896" 2090 2091 data-pid="896" 2092 data-s-title="Presentkort" 2093 data-s-price="200.00" 2094 data-price="200.00" 2095 > 2096 2097 <!-- Image --> 2098 <a class="block relative w-full " href="/presentkort/presentkort"> 2099 <div class="relative group overflow-hidden rounded-tnone"> 2100 <img loading="lazy" 2101 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 2102 ix-src="https://cdn.quickbutik.com/images/2666L/products/68bb00eae2e5f.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Presentkort"> 2103 2104 <img loading="lazy" 2105 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 2106 ix-src="https://cdn.quickbutik.com/images/2666L/products/68bb066cd4c61.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Presentkort"> 2107 2108 </div> 2109 </a> 2110 2111 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 2112 <a class="block px-6 mb-4" href="/presentkort/presentkort"> 2113 2114 <!-- Title --> 2115 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 2116 Presentkort 2117 </h3> 2118 2119 <!-- Reviews container --> 2120 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 2121 2122 </a> 2123 2124 <!-- Description --> 2125 <template x-if="layoutSingle"> 2126 2127 </template> 2128 2129 <div> 2130 2131 <!-- Price --> 2132 <div class="px-6"> 2133 2134 <!-- Current price -->
2135 <span> 2136 <span class="text-xl font-bold text-accent">200 SEK</span> 2137 </span> 2138 2139 <!-- Original price --> 2140 2141 </div> 2142 2143 <!-- Add to basket --> 2144 <div class="flex justify-between gap-2 px-6 mt-2"> 2145 2146 <!-- Buy button --> 2147 2148 <!-- No options --> 2149 2150 <!-- Has options --> 2151 <a href="/presentkort/presentkort" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 2152 2153 <!-- Icon (arrow) --> 2154 2155 <!-- Text --> 2156 Läs mer 2157 2158 </a> 2159 2160 <!-- Sold out --> 2161 2162 2163 2164 2165 <!-- Link to product when no buy button --> 2166 2167 <!-- Wishlist --> 2168 <div data-qb-selector="product-item-wishlist" data-product-id="896" class="flex items-center"></div> 2169 2170 </div> 2171 2172 <!-- Stock status --> 2173 <div class="text-sm px-6 pt-4"> 2174 2175 <!-- Sold out --> 2176 2177 <!-- Not sold out --> 2178 <span data-in-stock-text="I lager" 2179 data-out-of-stock-text="" 2180 data-combination-not-exist-text="Variant finns ej"> 2181 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 2182 </span> 2183 2184 </div> 2185 2186 </div> 2187 </div> 2188 </div> 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 <div x-data="{ layoutSingle: '4' == '1' }" 2204 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 2205 2206 data-qb-selector="product-item" 2207 data-product-id="881" 2208 2209 data-pid="881" 2210 data-s-title="AMI DOG Wet PURPLE 400g" 2211 data-s-price="45" 2212 data-price="45" 2213 > 2214 2215 <!-- Image --> 2216 <a class="block relative w-full " href="/hundmat/ami-dog-wet-purple-400g"> 2217 <div class="relative group overflow-hidden rounded-tnone"> 2218 <img loading="lazy" 2219 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 2220 ix-src="https://cdn.quickbutik.com/images/2666L/products/688b456783cc1.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Wet PURPLE 400g"> 2221 2222 <img loading="lazy" 2223 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 2224 ix-src="https://cdn.quickbutik.com/images/2666L/products/688b463fe9614.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Wet PURPLE 400g"> 2225 2226 </div> 2227 </a> 2228 2229 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 2230 <a class="block px-6 mb-4" href="/hundmat/ami-dog-wet-purple-400g"> 2231 2232 <!-- Title --> 2233 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 2234 AMI DOG Wet PURPLE 400g 2235 </h3> 2236 2237 <!-- Reviews container --> 2238 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 2239 2240 </a> 2241 2242 <!-- Description --> 2243 <template x-if="layoutSingle"> 2244 2245 </template> 2246 2247 <div> 2248 2249 <!-- Price --> 2250 <div class="px-6"> 2251 2252 <!-- Current price -->
2253 <span> 2254 <span class="text-xl font-bold text-accent">45 SEK</span> 2255 </span> 2256 2257 <!-- Original price --> 2258 2259 </div> 2260 2261 <!-- Add to basket --> 2262 <div class="flex justify-between gap-2 px-6 mt-2"> 2263 2264 <!-- Buy button --> 2265 2266 <!-- No options --> 2267 <button @click="$store.cart.add({ 2268 productID: 881, 2269 quantity: 1 2270 })" 2271 aria-label="Lägg i korgen" 2272 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 2273 2274 <!-- Icon (cart) --> 2275 2276 <!-- Text --> 2277 Lägg i korgen 2278 2279 </button> 2280 2281 <!-- Has options --> 2282 2283 <!-- Sold out --> 2284 2285 2286 2287 2288 <!-- Link to product when no buy button --> 2289 2290 <!-- Wishlist --> 2291 <div data-qb-selector="product-item-wishlist" data-product-id="881" class="flex items-center"></div> 2292 2293 </div> 2294 2295 <!-- Stock status --> 2296 <div class="text-sm px-6 pt-4"> 2297 2298 <!-- Sold out --> 2299 2300 <!-- Not sold out --> 2301 <span data-in-stock-text="I lager" 2302 data-out-of-stock-text="" 2303 data-combination-not-exist-text="Variant finns ej"> 2304 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 2305 </span> 2306 2307 </div> 2308 2309 </div> 2310 </div> 2311 </div> 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 <div x-data="{ layoutSingle: '4' == '1' }" 2327 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 2328 2329 data-qb-selector="product-item" 2330 data-product-id="880" 2331 2332 data-pid="880" 2333 data-s-title="Benevo Puppy Original 2kg/10kg" 2334 data-s-price="269.00" 2335 data-price="269.00" 2336 > 2337 2338 <!-- Image --> 2339 <a class="block relative w-full " href="/hundmat/kopia-kort-datum-benevo-puppy-original-10kg"> 2340 <div class="relative group overflow-hidden rounded-tnone"> 2341 <img loading="lazy" 2342 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 2343 ix-src="https://cdn.quickbutik.com/images/2666L/products/175378113817506672156601838004cbd.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Benevo Puppy Original 2kg/10kg"> 2344 2345 <img loading="lazy" 2346 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 2347 ix-src="https://cdn.quickbutik.com/images/2666L/products/1753781139175066721666018381d93e2.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Benevo Puppy Original 2kg/10kg"> 2348 2349 </div> 2350 </a> 2351 2352 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 2353 <a class="block px-6 mb-4" href="/hundmat/kopia-kort-datum-benevo-puppy-original-10kg"> 2354 2355 <!-- Title --> 2356 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 2357 Benevo Puppy Original 2kg/10kg 2358 </h3> 2359 2360 <!-- Reviews container --> 2361 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 2362 2363 </a> 2364 2365 <!-- Description --> 2366 <template x-if="layoutSingle"> 2367 2368 </template> 2369 2370 <div> 2371 2372 <!-- Price --> 2373 <div class="px-6"> 2374 2375 <!-- Current price -->
2376 <span> 2377 <span class="text-xl font-bold text-accent">269 SEK</span> 2378 </span> 2379 2380 <!-- Original price --> 2381 2382 </div> 2383 2384 <!-- Add to basket --> 2385 <div class="flex justify-between gap-2 px-6 mt-2"> 2386 2387 <!-- Buy button --> 2388 2389 <!-- No options --> 2390 2391 <!-- Has options --> 2392 <a href="/hundmat/kopia-kort-datum-benevo-puppy-original-10kg" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 2393 2394 <!-- Icon (arrow) --> 2395 2396 <!-- Text --> 2397 Läs mer 2398 2399 </a> 2400 2401 <!-- Sold out --> 2402 2403 2404 2405 2406 <!-- Link to product when no buy button --> 2407 2408 <!-- Wishlist --> 2409 <div data-qb-selector="product-item-wishlist" data-product-id="880" class="flex items-center"></div> 2410 2411 </div> 2412 2413 <!-- Stock status --> 2414 <div class="text-sm px-6 pt-4"> 2415 2416 <!-- Sold out --> 2417 2418 <!-- Not sold out --> 2419 <span data-in-stock-text="I lager" 2420 data-out-of-stock-text="" 2421 data-combination-not-exist-text="Variant finns ej"> 2422 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 2423 </span> 2424 2425 </div> 2426 2427 </div> 2428 </div> 2429 </div> 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 <div x-data="{ layoutSingle: '4' == '1' }" 2445 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 2446 2447 data-qb-selector="product-item" 2448 data-product-id="810" 2449 2450 data-pid="810" 2451 data-s-title="Omni Soothing Stew 395g" 2452 data-s-price="69" 2453 data-price="69" 2454 > 2455 2456 <!-- Image --> 2457 <a class="block relative w-full " href="/hundmat/omni-soothing-stew-395g"> 2458 <div class="relative group overflow-hidden rounded-tnone"> 2459 <img loading="lazy" 2460 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 2461 ix-src="https://cdn.quickbutik.com/images/2666L/products/67c9726886631.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni Soothing Stew 395g"> 2462 2463 <img loading="lazy" 2464 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 2465 ix-src="https://cdn.quickbutik.com/images/2666L/products/67c9726e90d6c.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni Soothing Stew 395g"> 2466 2467 </div> 2468 </a> 2469 2470 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 2471 <a class="block px-6 mb-4" href="/hundmat/omni-soothing-stew-395g"> 2472 2473 <!-- Title --> 2474 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 2475 Omni Soothing Stew 395g 2476 </h3> 2477 2478 <!-- Reviews container --> 2479 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 2480 2481 </a> 2482 2483 <!-- Description --> 2484 <template x-if="layoutSingle"> 2485 2486 </template> 2487 2488 <div> 2489 2490 <!-- Price --> 2491 <div class="px-6"> 2492 2493 <!-- Current price -->
2494 <span> 2495 <span class="text-xl font-bold text-accent">69 SEK</span> 2496 </span> 2497 2498 <!-- Original price --> 2499 2500 </div> 2501 2502 <!-- Add to basket --> 2503 <div class="flex justify-between gap-2 px-6 mt-2"> 2504 2505 <!-- Buy button --> 2506 2507 <!-- No options --> 2508 2509 <!-- Has options --> 2510 2511 <!-- Sold out --> 2512 <a href="/hundmat/omni-soothing-stew-395g" area-label="Ej i lager" class="block w-full button-py px-2 bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 2513 2514 <!-- Icon --> 2515 2516 <!-- Text --> 2517 Läs mer 2518 2519 </a> 2520 2521 2522 2523 2524 <!-- Link to product when no buy button --> 2525 2526 <!-- Wishlist --> 2527 <div data-qb-selector="product-item-wishlist" data-product-id="810" class="flex items-center"></div> 2528 2529 </div> 2530 2531 <!-- Stock status --> 2532 <div class="text-sm px-6 pt-4"> 2533 2534 <!-- Sold out --> 2535 <span> 2536 <span class="inline-block bg-red-600 rounded-full h-2 w-2 mr-1"></span> Ej i lager 2537 </span> 2538 2539 <!-- Not sold out --> 2540 2541 </div> 2542 2543 </div> 2544 </div> 2545 </div> 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 <div x-data="{ layoutSingle: '4' == '1' }" 2561 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 2562 2563 data-qb-selector="product-item" 2564 data-product-id="809" 2565 2566 data-pid="809" 2567 data-s-title="Omni PUPPY" 2568 data-s-price="249.00" 2569 data-price="249.00" 2570 > 2571 2572 <!-- Image --> 2573 <a class="block relative w-full " href="/hundmat/omni-puppy-2kg6kg10kg"> 2574 <div class="relative group overflow-hidden rounded-tnone"> 2575 <img loading="lazy" 2576 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 2577 ix-src="https://cdn.quickbutik.com/images/2666L/products/67b9d36f231bf.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni PUPPY"> 2578 2579 <img loading="lazy" 2580 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 2581 ix-src="https://cdn.quickbutik.com/images/2666L/products/67b9d35e40b3a.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni PUPPY"> 2582 2583 </div> 2584 </a> 2585 2586 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 2587 <a class="block px-6 mb-4" href="/hundmat/omni-puppy-2kg6kg10kg"> 2588 2589 <!-- Title --> 2590 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 2591 Omni PUPPY 2592 </h3> 2593 2594 <!-- Reviews container --> 2595 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 2596 2597 </a> 2598 2599 <!-- Description --> 2600 <template x-if="layoutSingle"> 2601 2602 </template> 2603 2604 <div> 2605 2606 <!-- Price --> 2607 <div class="px-6"> 2608 2609 <!-- Current price -->
2610 <span> 2611 <span class="text-xl font-bold text-accent">249 SEK</span> 2612 </span> 2613 2614 <!-- Original price --> 2615 2616 </div> 2617 2618 <!-- Add to basket --> 2619 <div class="flex justify-between gap-2 px-6 mt-2"> 2620 2621 <!-- Buy button --> 2622 2623 <!-- No options --> 2624 2625 <!-- Has options --> 2626 <a href="/hundmat/omni-puppy-2kg6kg10kg" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 2627 2628 <!-- Icon (arrow) --> 2629 2630 <!-- Text --> 2631 Läs mer 2632 2633 </a> 2634 2635 <!-- Sold out --> 2636 2637 2638 2639 2640 <!-- Link to product when no buy button --> 2641 2642 <!-- Wishlist --> 2643 <div data-qb-selector="product-item-wishlist" data-product-id="809" class="flex items-center"></div> 2644 2645 </div> 2646 2647 <!-- Stock status --> 2648 <div class="text-sm px-6 pt-4"> 2649 2650 <!-- Sold out --> 2651 2652 <!-- Not sold out --> 2653 <span data-in-stock-text="I lager" 2654 data-out-of-stock-text="" 2655 data-combination-not-exist-text="Variant finns ej"> 2656 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 2657 </span> 2658 2659 </div> 2660 2661 </div> 2662 </div> 2663 </div> 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 <div x-data="{ layoutSingle: '4' == '1' }" 2679 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 2680 2681 data-qb-selector="product-item" 2682 data-product-id="805" 2683 2684 data-pid="805" 2685 data-s-title="Vegan4Dogs Greta 5kg/14kg" 2686 data-s-price="499.00" 2687 data-price="499.00" 2688 > 2689 2690 <!-- Image --> 2691 <a class="block relative w-full " href="/hundmat/vegan4dogs-greta-5kg14kg"> 2692 <div class="relative group overflow-hidden rounded-tnone"> 2693 <img loading="lazy" 2694 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 2695 ix-src="https://cdn.quickbutik.com/images/2666L/products/17393713845ba4dac8b3367.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Vegan4Dogs Greta 5kg/14kg"> 2696 2697 <img loading="lazy" 2698 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 2699 ix-src="https://cdn.quickbutik.com/images/2666L/products/173937138559ee3fc13ecfb.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Vegan4Dogs Greta 5kg/14kg"> 2700 2701 </div> 2702 </a> 2703 2704 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 2705 <a class="block px-6 mb-4" href="/hundmat/vegan4dogs-greta-5kg14kg"> 2706 2707 <!-- Title --> 2708 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 2709 Vegan4Dogs Greta 5kg/14kg 2710 </h3> 2711 2712 <!-- Reviews container --> 2713 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 2714 2715 </a> 2716 2717 <!-- Description --> 2718 <template x-if="layoutSingle"> 2719 2720 </template> 2721 2722 <div> 2723 2724 <!-- Price --> 2725 <div class="px-6"> 2726 2727 <!-- Current price -->
2728 <span> 2729 <span class="text-xl font-bold text-accent">499 SEK</span> 2730 </span> 2731 2732 <!-- Original price --> 2733 2734 </div> 2735 2736 <!-- Add to basket --> 2737 <div class="flex justify-between gap-2 px-6 mt-2"> 2738 2739 <!-- Buy button --> 2740 2741 <!-- No options --> 2742 2743 <!-- Has options --> 2744 <a href="/hundmat/vegan4dogs-greta-5kg14kg" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 2745 2746 <!-- Icon (arrow) --> 2747 2748 <!-- Text --> 2749 Läs mer 2750 2751 </a> 2752 2753 <!-- Sold out --> 2754 2755 2756 2757 2758 <!-- Link to product when no buy button --> 2759 2760 <!-- Wishlist --> 2761 <div data-qb-selector="product-item-wishlist" data-product-id="805" class="flex items-center"></div> 2762 2763 </div> 2764 2765 <!-- Stock status --> 2766 <div class="text-sm px-6 pt-4"> 2767 2768 <!-- Sold out --> 2769 2770 <!-- Not sold out --> 2771 <span data-in-stock-text="I lager" 2772 data-out-of-stock-text="" 2773 data-combination-not-exist-text="Variant finns ej"> 2774 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 2775 </span> 2776 2777 </div> 2778 2779 </div> 2780 </div> 2781 </div> 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 <div x-data="{ layoutSingle: '4' == '1' }" 2797 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 2798 2799 data-qb-selector="product-item" 2800 data-product-id="793" 2801 2802 data-pid="793" 2803 data-s-title="Vegdog Simply Crunch 1kg/5kg/10kg" 2804 data-s-price="129.00" 2805 data-price="129.00" 2806 > 2807 2808 <!-- Image --> 2809 <a class="block relative w-full " href="/hundmat/vegdog-simply-crunch-2kg5kg10kg"> 2810 <div class="relative group overflow-hidden rounded-tnone"> 2811 <img loading="lazy" 2812 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 2813 ix-src="https://cdn.quickbutik.com/images/2666L/products/6789195714239.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Vegdog Simply Crunch 1kg/5kg/10kg"> 2814 2815 <img loading="lazy" 2816 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 2817 ix-src="https://cdn.quickbutik.com/images/2666L/products/6789195e5de04.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Vegdog Simply Crunch 1kg/5kg/10kg"> 2818 2819 </div> 2820 </a> 2821 2822 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 2823 <a class="block px-6 mb-4" href="/hundmat/vegdog-simply-crunch-2
2823kg5kg10kg"> 2824 2825 <!-- Title --> 2826 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 2827 Vegdog Simply Crunch 1kg/5kg/10kg 2828 </h3> 2829 2830 <!-- Reviews container --> 2831 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 2832 2833 </a> 2834 2835 <!-- Description --> 2836 <template x-if="layoutSingle"> 2837 2838 </template> 2839 2840 <div> 2841 2842 <!-- Price --> 2843 <div class="px-6"> 2844 2845 <!-- Current price --> 2846 <span> 2847 <span class="text-xl font-bold text-accent">129 SEK</span> 2848 </span> 2849 2850 <!-- Original price --> 2851 2852 </div> 2853 2854 <!-- Add to basket --> 2855 <div class="flex justify-between gap-2 px-6 mt-2"> 2856 2857 <!-- Buy button --> 2858 2859 <!-- No options --> 2860 2861 <!-- Has options --> 2862 <a href="/hundmat/vegdog-simply-crunch-2kg5kg10kg" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 2863 2864 <!-- Icon (arrow) --> 2865 2866 <!-- Text --> 2867 Läs mer 2868 2869 </a> 2870 2871 <!-- Sold out --> 2872 2873 2874 2875 2876 <!-- Link to product when no buy button --> 2877 2878 <!-- Wishlist --> 2879 <div data-qb-selector="product-item-wishlist" data-product-id="793" class="flex items-center"></div> 2880 2881 </div> 2882 2883 <!-- Stock status --> 2884 <div class="text-sm px-6 pt-4"> 2885 2886 <!-- Sold out --> 2887 2888 <!-- Not sold out --> 2889 <span data-in-stock-text="I lager" 2890 data-out-of-stock-text="" 2891 data-combination-not-exist-text="Variant finns ej"> 2892 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 2893 </span> 2894 2895 </div> 2896 2897 </div> 2898 </div> 2899 </div> 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 <div x-data="{ layoutSingle: '4' == '1' }" 2915 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 2916 2917 data-qb-selector="product-item" 2918 data-product-id="788" 2919 2920 data-pid="788" 2921 data-s-title="Omni No-Chicken Pot Pie 400g" 2922 data-s-price="69" 2923 data-price="69" 2924 > 2925 2926 <!-- Image --> 2927 <a class="block relative w-full " href="/hundmat/omni-no-chicken-pot-pie-400g"> 2928 <div class="relative group overflow-hidden rounded-tnone"> 2929 <img loading="lazy" 2930 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 2931 ix-src="https://cdn.quickbutik.com/images/2666L/products/671a5ed542493.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni No-Chicken Pot Pie 400g"> 2932 2933 <img loading="lazy" 2934 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 2935 ix-src="https://cdn.quickbutik.com/images/2666L/products/6960c1517f323.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni No-Chicken Pot Pie 400g"> 2936 2937 </div> 2938 </a> 2939 2940 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 2941 <a class="block px-6 mb-4" href="/hundmat/omni-no-chicken-pot-pie-400g"> 2942 2943 <!-- Title --> 2944 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 2945 Omni No-Chicken Pot Pie 400g 2946 </h3> 2947 2948 <!-- Reviews container --> 2949 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 2950 2951 </a> 2952 2953 <!-- Description --> 2954 <template x-if="layoutSingle"> 2955 2956 </template> 2957 2958 <div> 2959 2960 <!-- Price --> 2961 <div class="px-6"> 2962 2963 <!-- Current price -->
2964 <span> 2965 <span class="text-xl font-bold text-accent">69 SEK</span> 2966 </span> 2967 2968 <!-- Original price --> 2969 2970 </div> 2971 2972 <!-- Add to basket --> 2973 <div class="flex justify-between gap-2 px-6 mt-2"> 2974 2975 <!-- Buy button --> 2976 2977 <!-- No options --> 2978 2979 <!-- Has options --> 2980 2981 <!-- Sold out --> 2982 <a href="/hundmat/omni-no-chicken-pot-pie-400g" area-label="Ej i lager" class="block w-full button-py px-2 bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 2983 2984 <!-- Icon --> 2985 2986 <!-- Text --> 2987 Läs mer 2988 2989 </a> 2990 2991 2992 2993 2994 <!-- Link to product when no buy button --> 2995 2996 <!-- Wishlist --> 2997 <div data-qb-selector="product-item-wishlist" data-product-id="788" class="flex items-center"></div> 2998 2999 </div> 3000 3001 <!-- Stock status --> 3002 <div class="text-sm px-6 pt-4"> 3003 3004 <!-- Sold out --> 3005 <span> 3006 <span class="inline-block bg-red-600 rounded-full h-2 w-2 mr-1"></span> Ej i lager 3007 </span> 3008 3009 <!-- Not sold out --> 3010 3011 </div> 3012 3013 </div> 3014 </div> 3015 </div> 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 <div x-data="{ layoutSingle: '4' == '1' }" 3031 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 3032 3033 data-qb-selector="product-item" 3034 data-product-id="787" 3035 3036 data-pid="787" 3037 data-s-title="Omni No-Beef Casserole 400g" 3038 data-s-price="69" 3039 data-price="69" 3040 > 3041 3042 <!-- Image --> 3043 <a class="block relative w-full " href="/hundmat/omni-no-beef-casserole-400g"> 3044 <div class="relative group overflow-hidden rounded-tnone"> 3045 <img loading="lazy" 3046 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 3047 ix-src="https://cdn.quickbutik.com/images/2666L/products/671a5ca798cc9.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni No-Beef Casserole 400g"> 3048 3049 <img loading="lazy" 3050 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 3051 ix-src="https://cdn.quickbutik.com/images/2666L/products/671b4541d1a10.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni No-Beef Casserole 400g"> 3052 3053 </div> 3054 </a> 3055 3056 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 3057 <a class="block px-6 mb-4" href="/hundmat/omni-no-beef-casserole-400g"> 3058 3059 <!-- Title --> 3060 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 3061 Omni No-Beef Casserole 400g 3062 </h3> 3063 3064 <!-- Reviews container --> 3065 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 3066 3067 </a> 3068 3069 <!-- Description --> 3070 <template x-if="layoutSingle"> 3071 3072 </template> 3073 3074 <div> 3075 3076 <!-- Price --> 3077 <div class="px-6"> 3078 3079 <!-- Current price -->
3080 <span> 3081 <span class="text-xl font-bold text-accent">69 SEK</span> 3082 </span> 3083 3084 <!-- Original price --> 3085 3086 </div> 3087 3088 <!-- Add to basket --> 3089 <div class="flex justify-between gap-2 px-6 mt-2"> 3090 3091 <!-- Buy button --> 3092 3093 <!-- No options --> 3094 3095 <!-- Has options --> 3096 3097 <!-- Sold out --> 3098 <a href="/hundmat/omni-no-beef-casserole-400g" area-label="Ej i lager" class="block w-full button-py px-2 bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 3099 3100 <!-- Icon --> 3101 3102 <!-- Text --> 3103 Läs mer 3104 3105 </a> 3106 3107 3108 3109 3110 <!-- Link to product when no buy button --> 3111 3112 <!-- Wishlist --> 3113 <div data-qb-selector="product-item-wishlist" data-product-id="787" class="flex items-center"></div> 3114 3115 </div> 3116 3117 <!-- Stock status --> 3118 <div class="text-sm px-6 pt-4"> 3119 3120 <!-- Sold out --> 3121 <span> 3122 <span class="inline-block bg-red-600 rounded-full h-2 w-2 mr-1"></span> Ej i lager 3123 </span> 3124 3125 <!-- Not sold out --> 3126 3127 </div> 3128 3129 </div> 3130 </div> 3131 </div> 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 <div x-data="{ layoutSingle: '4' == '1' }" 3147 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 3148 3149 data-qb-selector="product-item" 3150 data-product-id="772" 3151 3152 data-pid="772" 3153 data-s-title="Pawr Healthy Habits 2,5kg/8kg" 3154 data-s-price="349.00" 3155 data-price="349.00" 3156 > 3157 3158 <!-- Image --> 3159 <a class="block relative w-full " href="/hundmat/pawr-healthy-habits-25kg8kg"> 3160 <div class="relative group overflow-hidden rounded-tnone"> 3161 <img loading="lazy" 3162 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 3163 ix-src="https://cdn.quickbutik.com/images/2666L/products/66f697264d9a5.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Pawr Healthy Habits 2,5kg/8kg"> 3164 3165 <img loading="lazy" 3166 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 3167 ix-src="https://cdn.quickbutik.com/images/2666L/products/66f6972f09fd7.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Pawr Healthy Habits 2,5kg/8kg"> 3168 3169 </div> 3170 </a> 3171 3172 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 3173 <a class="block px-6 mb-4" href="/hundmat/pawr-healthy-habits-25kg8kg"> 3174 3175 <!-- Title --> 3176 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 3177 Pawr Healthy Habits 2,5kg/8kg 3178 </h3> 3179 3180 <!-- Reviews container --> 3181 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 3182 3183 </a> 3184 3185 <!-- Description --> 3186 <template x-if="layoutSingle"> 3187 3188 </template> 3189 3190 <div> 3191 3192 <!-- Price --> 3193 <div class="px-6"> 3194 3195 <!-- Current price -->
3196 <span> 3197 <span class="text-xl font-bold text-accent">349 SEK</span> 3198 </span> 3199 3200 <!-- Original price --> 3201 3202 </div> 3203 3204 <!-- Add to basket --> 3205 <div class="flex justify-between gap-2 px-6 mt-2"> 3206 3207 <!-- Buy button --> 3208 3209 <!-- No options --> 3210 3211 <!-- Has options --> 3212 3213 <!-- Sold out --> 3214 <a href="/hundmat/pawr-healthy-habits-25kg8kg" area-label="Ej i lager" class="block w-full button-py px-2 bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 3215 3216 <!-- Icon --> 3217 3218 <!-- Text --> 3219 Läs mer 3220 3221 </a> 3222 3223 3224 3225 3226 <!-- Link to product when no buy button --> 3227 3228 <!-- Wishlist --> 3229 <div data-qb-selector="product-item-wishlist" data-product-id="772" class="flex items-center"></div> 3230 3231 </div> 3232 3233 <!-- Stock status --> 3234 <div class="text-sm px-6 pt-4"> 3235 3236 <!-- Sold out --> 3237 <span> 3238 <span class="inline-block bg-red-600 rounded-full h-2 w-2 mr-1"></span> Ej i lager 3239 </span> 3240 3241 <!-- Not sold out --> 3242 3243 </div> 3244 3245 </div> 3246 </div> 3247 </div> 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 <div x-data="{ layoutSingle: '4' == '1' }" 3263 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 3264 3265 data-qb-selector="product-item" 3266 data-product-id="771" 3267 3268 data-pid="771" 3269 data-s-title="Pawr Green Glory 750g" 3270 data-s-price="129" 3271 data-price="129" 3272 > 3273 3274 <!-- Image --> 3275 <a class="block relative w-full " href="/hundmat/pawr-green-glory-750g"> 3276 <div class="relative group overflow-hidden rounded-tnone"> 3277 <img loading="lazy" 3278 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 3279 ix-src="https://cdn.quickbutik.com/images/2666L/products/66f694662ba95.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Pawr Green Glory 750g"> 3280 3281 <img loading="lazy" 3282 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 3283 ix-src="https://cdn.quickbutik.com/images/2666L/products/66f694721865f.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Pawr Green Glory 750g"> 3284 3285 </div> 3286 </a> 3287 3288 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 3289 <a class="block px-6 mb-4" href="/hundmat/pawr-green-glory-750g"> 3290 3291 <!-- Title --> 3292 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 3293 Pawr Green Glory 750g 3294 </h3> 3295 3296 <!-- Reviews container --> 3297 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 3298 3299 </a> 3300 3301 <!-- Description --> 3302 <template x-if="layoutSingle"> 3303 3304 </template> 3305 3306 <div> 3307 3308 <!-- Price --> 3309 <div class="px-6"> 3310 3311 <!-- Current price -->
3312 <span> 3313 <span class="text-xl font-bold text-accent">129 SEK</span> 3314 </span> 3315 3316 <!-- Original price --> 3317 3318 </div> 3319 3320 <!-- Add to basket --> 3321 <div class="flex justify-between gap-2 px-6 mt-2"> 3322 3323 <!-- Buy button --> 3324 3325 <!-- No options --> 3326 <button @click="$store.cart.add({ 3327 productID: 771, 3328 quantity: 1 3329 })" 3330 aria-label="Lägg i korgen" 3331 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 3332 3333 <!-- Icon (cart) --> 3334 3335 <!-- Text --> 3336 Lägg i korgen 3337 3338 </button> 3339 3340 <!-- Has options --> 3341 3342 <!-- Sold out --> 3343 3344 3345 3346 3347 <!-- Link to product when no buy button --> 3348 3349 <!-- Wishlist --> 3350 <div data-qb-selector="product-item-wishlist" data-product-id="771" class="flex items-center"></div> 3351 3352 </div> 3353 3354 <!-- Stock status --> 3355 <div class="text-sm px-6 pt-4"> 3356 3357 <!-- Sold out --> 3358 3359 <!-- Not sold out --> 3360 <span data-in-stock-text="I lager" 3361 data-out-of-stock-text="" 3362 data-combination-not-exist-text="Variant finns ej"> 3363 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 3364 </span> 3365 3366 </div> 3367 3368 </div> 3369 </div> 3370 </div> 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 <div x-data="{ layoutSingle: '4' == '1' }" 3386 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 3387 3388 data-qb-selector="product-item" 3389 data-product-id="770" 3390 3391 data-pid="770" 3392 data-s-title="Pawr Beet Booster 750g" 3393 data-s-price="129" 3394 data-price="129" 3395 > 3396 3397 <!-- Image --> 3398 <a class="block relative w-full " href="/hundmat/pawr-beet-booster-750g"> 3399 <div class="relative group overflow-hidden rounded-tnone"> 3400 <img loading="lazy" 3401 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 3402 ix-src="https://cdn.quickbutik.com/images/2666L/products/66f5a6eee6c4c.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Pawr Beet Booster 750g"> 3403 3404 <img loading="lazy" 3405 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 3406 ix-src="https://cdn.quickbutik.com/images/2666L/products/66f5a6fd1cb14.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Pawr Beet Booster 750g"> 3407 3408 </div> 3409 </a> 3410 3411 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 3412 <a class="block px-6 mb-4" href="/hundmat/pawr-beet-booster-750g"> 3413 3414 <!-- Title --> 3415 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 3416 Pawr Beet Booster 750g 3417 </h3> 3418 3419 <!-- Reviews container --> 3420 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 3421 3422 </a> 3423 3424 <!-- Description --> 3425 <template x-if="layoutSingle"> 3426 3427 </template> 3428 3429 <div> 3430 3431 <!-- Price --> 3432 <div class="px-6"> 3433 3434 <!-- Current price -->
3435 <span> 3436 <span class="text-xl font-bold text-accent">129 SEK</span> 3437 </span> 3438 3439 <!-- Original price --> 3440 3441 </div> 3442 3443 <!-- Add to basket --> 3444 <div class="flex justify-between gap-2 px-6 mt-2"> 3445 3446 <!-- Buy button --> 3447 3448 <!-- No options --> 3449 <button @click="$store.cart.add({ 3450 productID: 770, 3451 quantity: 1 3452 })" 3453 aria-label="Lägg i korgen" 3454 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 3455 3456 <!-- Icon (cart) --> 3457 3458 <!-- Text --> 3459 Lägg i korgen 3460 3461 </button> 3462 3463 <!-- Has options --> 3464 3465 <!-- Sold out --> 3466 3467 3468 3469 3470 <!-- Link to product when no buy button --> 3471 3472 <!-- Wishlist --> 3473 <div data-qb-selector="product-item-wishlist" data-product-id="770" class="flex items-center"></div> 3474 3475 </div> 3476 3477 <!-- Stock status --> 3478 <div class="text-sm px-6 pt-4"> 3479 3480 <!-- Sold out --> 3481 3482 <!-- Not sold out --> 3483 <span data-in-stock-text="I lager" 3484 data-out-of-stock-text="" 3485 data-combination-not-exist-text="Variant finns ej"> 3486 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 3487 </span> 3488 3489 </div> 3490 3491 </div> 3492 </div> 3493 </div> 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 <div x-data="{ layoutSingle: '4' == '1' }" 3509 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 3510 3511 data-qb-selector="product-item" 3512 data-product-id="769" 3513 3514 data-pid="769" 3515 data-s-title="Pawr Golden Glow 750g" 3516 data-s-price="129" 3517 data-price="129" 3518 > 3519 3520 <!-- Image --> 3521 <a class="block relative w-full " href="/hundmat/pawr-golden-glow-750g"> 3522 <div class="relative group overflow-hidden rounded-tnone"> 3523 <img loading="lazy" 3524 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 3525 ix-src="https://cdn.quickbutik.com/images/2666L/products/66f570e9949c3.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Pawr Golden Glow 750g"> 3526 3527 <img loading="lazy" 3528 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 3529 ix-src="https://cdn.quickbutik.com/images/2666L/products/66f570f2d49e8.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Pawr Golden Glow 750g"> 3530 3531 </div> 3532 </a> 3533 3534 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 3535 <a class="block px-6 mb-4" href="/hundmat/pawr-golden-glow-750g"> 3536 3537 <!-- Title --> 3538 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 3539 Pawr Golden Glow 750g 3540 </h3> 3541 3542 <!-- Reviews container --> 3543 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 3544 3545 </a> 3546 3547 <!-- Description --> 3548 <template x-if="layoutSingle"> 3549 3550 </template> 3551 3552 <div> 3553 3554 <!-- Price --> 3555 <div class="px-6"> 3556 3557 <!-- Current price -->
3558 <span> 3559 <span class="text-xl font-bold text-accent">129 SEK</span> 3560 </span> 3561 3562 <!-- Original price --> 3563 3564 </div> 3565 3566 <!-- Add to basket --> 3567 <div class="flex justify-between gap-2 px-6 mt-2"> 3568 3569 <!-- Buy button --> 3570 3571 <!-- No options --> 3572 <button @click="$store.cart.add({ 3573 productID: 769, 3574 quantity: 1 3575 })" 3576 aria-label="Lägg i korgen" 3577 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 3578 3579 <!-- Icon (cart) --> 3580 3581 <!-- Text --> 3582 Lägg i korgen 3583 3584 </button> 3585 3586 <!-- Has options --> 3587 3588 <!-- Sold out --> 3589 3590 3591 3592 3593 <!-- Link to product when no buy button --> 3594 3595 <!-- Wishlist --> 3596 <div data-qb-selector="product-item-wishlist" data-product-id="769" class="flex items-center"></div> 3597 3598 </div> 3599 3600 <!-- Stock status --> 3601 <div class="text-sm px-6 pt-4"> 3602 3603 <!-- Sold out --> 3604 3605 <!-- Not sold out --> 3606 <span data-in-stock-text="I lager" 3607 data-out-of-stock-text="" 3608 data-combination-not-exist-text="Variant finns ej"> 3609 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 3610 </span> 3611 3612 </div> 3613 3614 </div> 3615 </div> 3616 </div> 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 <div x-data="{ layoutSingle: '4' == '1' }" 3632 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 3633 3634 data-qb-selector="product-item" 3635 data-product-id="763" 3636 3637 data-pid="763" 3638 data-s-title="Omni Toppers 150g" 3639 data-s-price="45.00" 3640 data-price="45.00" 3641 > 3642 3643 <!-- Image --> 3644 <a class="block relative w-full " href="/hundmat/37-omni-no-beef-bites-toppers"> 3645 <div class="relative group overflow-hidden rounded-tnone"> 3646 <img loading="lazy" 3647 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 3648 ix-src="https://cdn.quickbutik.com/images/2666L/products/66e91cfeb969e.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni Toppers 150g"> 3649 3650 <img loading="lazy" 3651 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 3652 ix-src="https://cdn.quickbutik.com/images/2666L/products/66e91d1ec37b7.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni Toppers 150g"> 3653 3654 </div> 3655 </a> 3656 3657 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 3658 <a class="block px-6 mb-4" href="/hundmat/37-omni-no-beef-bites-toppers"> 3659 3660 <!-- Title --> 3661 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 3662 Omni Toppers 150g 3663 </h3> 3664 3665 <!-- Reviews container --> 3666 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 3667 3668 </a> 3669 3670 <!-- Description --> 3671 <template x-if="layoutSingle"> 3672 3673 </template> 3674 3675 <div> 3676 3677 <!-- Price --> 3678 <div class="px-6"> 3679 3680 <!-- Current price -->
3681 <span> 3682 <span class="text-xl font-bold text-accent">45 SEK</span> 3683 </span> 3684 3685 <!-- Original price --> 3686 3687 </div> 3688 3689 <!-- Add to basket --> 3690 <div class="flex justify-between gap-2 px-6 mt-2"> 3691 3692 <!-- Buy button --> 3693 3694 <!-- No options --> 3695 3696 <!-- Has options --> 3697 3698 <!-- Sold out --> 3699 <a href="/hundmat/37-omni-no-beef-bites-toppers" area-label="Ej i lager" class="block w-full button-py px-2 bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 3700 3701 <!-- Icon --> 3702 3703 <!-- Text --> 3704 Läs mer 3705 3706 </a> 3707 3708 3709 3710 3711 <!-- Link to product when no buy button --> 3712 3713 <!-- Wishlist --> 3714 <div data-qb-selector="product-item-wishlist" data-product-id="763" class="flex items-center"></div> 3715 3716 </div> 3717 3718 <!-- Stock status --> 3719 <div class="text-sm px-6 pt-4"> 3720 3721 <!-- Sold out --> 3722 <span> 3723 <span class="inline-block bg-red-600 rounded-full h-2 w-2 mr-1"></span> Ej i lager 3724 </span> 3725 3726 <!-- Not sold out --> 3727 3728 </div> 3729 3730 </div> 3731 </div> 3732 </div> 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 <div x-data="{ layoutSingle: '4' == '1' }" 3748 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 3749 3750 data-qb-selector="product-item" 3751 data-product-id="740" 3752 3753 data-pid="740" 3754 data-s-title="V-dog Traditional Flakes 15kg" 3755 data-s-price="999" 3756 data-price="999" 3757 > 3758 3759 <!-- Image --> 3760 <a class="block relative w-full " href="/hundmat/v-dog-traditional-flakes"> 3761 <div class="relative group overflow-hidden rounded-tnone"> 3762 <img loading="lazy" 3763 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 3764 ix-src="https://cdn.quickbutik.com/images/2666L/products/65d8805c77f98.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="V-dog Traditional Flakes 15kg"> 3765 3766 <img loading="lazy" 3767 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 3768 ix-src="https://cdn.quickbutik.com/images/2666L/products/65d884ec3e884.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="V-dog Traditional Flakes 15kg"> 3769 3770 </div> 3771 </a> 3772 3773 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 3774 <a class="block px-6 mb-4" href="/hundmat/v-dog-traditional-flakes"> 3775 3776 <!-- Title --> 3777 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 3778 V-dog Traditional Flakes 15kg 3779 </h3> 3780 3781 <!-- Reviews container --> 3782 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 3783 3784 </a> 3785 3786 <!-- Description --> 3787 <template x-if="layoutSingle"> 3788 3789 </template> 3790 3791 <div> 3792 3793 <!-- Price --> 3794 <div class="px-6"> 3795 3796 <!-- Current price -->
3797 <span> 3798 <span class="text-xl font-bold text-accent">999 SEK</span> 3799 </span> 3800 3801 <!-- Original price --> 3802 3803 </div> 3804 3805 <!-- Add to basket --> 3806 <div class="flex justify-between gap-2 px-6 mt-2"> 3807 3808 <!-- Buy button --> 3809 3810 <!-- No options --> 3811 <button @click="$store.cart.add({ 3812 productID: 740, 3813 quantity: 1 3814 })" 3815 aria-label="Lägg i korgen" 3816 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 3817 3818 <!-- Icon (cart) --> 3819 3820 <!-- Text --> 3821 Lägg i korgen 3822 3823 </button> 3824 3825 <!-- Has options --> 3826 3827 <!-- Sold out --> 3828 3829 3830 3831 3832 <!-- Link to product when no buy button --> 3833 3834 <!-- Wishlist --> 3835 <div data-qb-selector="product-item-wishlist" data-product-id="740" class="flex items-center"></div> 3836 3837 </div> 3838 3839 <!-- Stock status --> 3840 <div class="text-sm px-6 pt-4"> 3841 3842 <!-- Sold out --> 3843 3844 <!-- Not sold out --> 3845 <span data-in-stock-text="I lager" 3846 data-out-of-stock-text="" 3847 data-combination-not-exist-text="Variant finns ej"> 3848 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 3849 </span> 3850 3851 </div> 3852 3853 </div> 3854 </div> 3855 </div> 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 <div x-data="{ layoutSingle: '4' == '1' }" 3871 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 3872 3873 data-qb-selector="product-item" 3874 data-product-id="733" 3875 3876 data-pid="733" 3877 data-s-title="Omni SENIOR 2kg/6kg/10kg" 3878 data-s-price="249.00" 3879 data-price="249.00" 3880 > 3881 3882 <!-- Image --> 3883 <a class="block relative w-full " href="/hundmat/omni-senior"> 3884 <div class="relative group overflow-hidden rounded-tnone"> 3885 <img loading="lazy" 3886 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 3887 ix-src="https://cdn.quickbutik.com/images/2666L/products/65c3705bdb389.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni SENIOR 2kg/6kg/10kg"> 3888 3889 <img loading="lazy" 3890 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 3891 ix-src="https://cdn.quickbutik.com/images/2666L/products/65c3705bc9699.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni SENIOR 2kg/6kg/10kg"> 3892 3893 </div> 3894 </a> 3895 3896 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 3897 <a class="block px-6 mb-4" href="/hundmat/omni-senior"> 3898 3899 <!-- Title --> 3900 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 3901 Omni SENIOR 2kg/6kg/10kg 3902 </h3> 3903 3904 <!-- Reviews container --> 3905 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 3906 3907 </a> 3908 3909 <!-- Description --> 3910 <template x-if="layoutSingle"> 3911 3912 </template> 3913 3914 <div> 3915 3916 <!-- Price --> 3917 <div class="px-6"> 3918 3919 <!-- Current price -->
3920 <span> 3921 <span class="text-xl font-bold text-accent">249 SEK</span> 3922 </span> 3923 3924 <!-- Original price --> 3925 3926 </div> 3927 3928 <!-- Add to basket --> 3929 <div class="flex justify-between gap-2 px-6 mt-2"> 3930 3931 <!-- Buy button --> 3932 3933 <!-- No options --> 3934 3935 <!-- Has options --> 3936 3937 <!-- Sold out --> 3938 <a href="/hundmat/omni-senior" area-label="Ej i lager" class="block w-full button-py px-2 bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 3939 3940 <!-- Icon --> 3941 3942 <!-- Text --> 3943 Läs mer 3944 3945 </a> 3946 3947 3948 3949 3950 <!-- Link to product when no buy button --> 3951 3952 <!-- Wishlist --> 3953 <div data-qb-selector="product-item-wishlist" data-product-id="733" class="flex items-center"></div> 3954 3955 </div> 3956 3957 <!-- Stock status --> 3958 <div class="text-sm px-6 pt-4"> 3959 3960 <!-- Sold out --> 3961 <span> 3962 <span class="inline-block bg-red-600 rounded-full h-2 w-2 mr-1"></span> Ej i lager 3963 </span> 3964 3965 <!-- Not sold out --> 3966 3967 </div> 3968 3969 </div> 3970 </div> 3971 </div> 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 <div x-data="{ layoutSingle: '4' == '1' }" 3987 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 3988 3989 data-qb-selector="product-item" 3990 data-product-id="732" 3991 3992 data-pid="732" 3993 data-s-title="Omni ADULT 2kg/6kg/10kg" 3994 data-s-price="249.00" 3995 data-price="249.00" 3996 > 3997 3998 <!-- Image --> 3999 <a class="block relative w-full " href="/hundmat/omni-pet-adult"> 4000 <div class="relative group overflow-hidden rounded-tnone"> 4001 <img loading="lazy" 4002 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 4003 ix-src="https://cdn.quickbutik.com/images/2666L/products/65c36d70d5651.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni ADULT 2kg/6kg/10kg"> 4004 4005 <img loading="lazy" 4006 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 4007 ix-src="https://cdn.quickbutik.com/images/2666L/products/65c36d7177f61.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Omni ADULT 2kg/6kg/10kg"> 4008 4009 </div> 4010 </a> 4011 4012 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 4013 <a class="block px-6 mb-4" href="/hundmat/omni-pet-adult"> 4014 4015 <!-- Title --> 4016 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 4017 Omni ADULT 2kg/6kg/10kg 4018 </h3> 4019 4020 <!-- Reviews container --> 4021 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 4022 4023 </a> 4024 4025 <!-- Description --> 4026 <template x-if="layoutSingle"> 4027 4028 </template> 4029 4030 <div> 4031 4032 <!-- Price --> 4033 <div class="px-6"> 4034 4035 <!-- Current price -->
4036 <span> 4037 <span class="text-xl font-bold text-accent">249 SEK</span> 4038 </span> 4039 4040 <!-- Original price --> 4041 4042 </div> 4043 4044 <!-- Add to basket --> 4045 <div class="flex justify-between gap-2 px-6 mt-2"> 4046 4047 <!-- Buy button --> 4048 4049 <!-- No options --> 4050 4051 <!-- Has options --> 4052 <a href="/hundmat/omni-pet-adult" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 4053 4054 <!-- Icon (arrow) --> 4055 4056 <!-- Text --> 4057 Läs mer 4058 4059 </a> 4060 4061 <!-- Sold out --> 4062 4063 4064 4065 4066 <!-- Link to product when no buy button --> 4067 4068 <!-- Wishlist --> 4069 <div data-qb-selector="product-item-wishlist" data-product-id="732" class="flex items-center"></div> 4070 4071 </div> 4072 4073 <!-- Stock status --> 4074 <div class="text-sm px-6 pt-4"> 4075 4076 <!-- Sold out --> 4077 4078 <!-- Not sold out --> 4079 <span data-in-stock-text="I lager" 4080 data-out-of-stock-text="" 4081 data-combination-not-exist-text="Variant finns ej"> 4082 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 4083 </span> 4084 4085 </div> 4086 4087 </div> 4088 </div> 4089 </div> 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 <div x-data="{ layoutSingle: '4' == '1' }" 4105 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 4106 4107 data-qb-selector="product-item" 4108 data-product-id="694" 4109 4110 data-pid="694" 4111 data-s-title="VEGDOG Farmer's Crunch 1kg/4kg/10kg" 4112 data-s-price="149.00" 4113 data-price="149.00" 4114 > 4115 4116 <!-- Image --> 4117 <a class="block relative w-full " href="/hundmat/vegdog-farmers-crunch"> 4118 <div class="relative group overflow-hidden rounded-tnone"> 4119 <img loading="lazy" 4120 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 4121 ix-src="https://cdn.quickbutik.com/images/2666L/products/656a117c1bdba.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Farmer's Crunch 1kg/4kg/10kg"> 4122 4123 <img loading="lazy" 4124 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 4125 ix-src="https://cdn.quickbutik.com/images/2666L/products/656a117c07f68.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Farmer's Crunch 1kg/4kg/10kg"> 4126 4127 </div> 4128 </a> 4129 4130 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 4131 <a class="block px-6 mb-4" href="/hundmat/vegdog-farmers-crunch"> 4132 4133 <!-- Title --> 4134 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 4135 VEGDOG Farmer's Crunch 1kg/4kg/10kg 4136 </h3> 4137 4138 <!-- Reviews container --> 4139 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 4140 4141 </a> 4142 4143 <!-- Description --> 4144 <template x-if="layoutSingle"> 4145 4146 </template> 4147 4148 <div> 4149 4150 <!-- Price --> 4151 <div class="px-6"> 4152 4153 <!-- Current price -->
4154 <span> 4155 <span class="text-xl font-bold text-accent">149 SEK</span> 4156 </span> 4157 4158 <!-- Original price --> 4159 4160 </div> 4161 4162 <!-- Add to basket --> 4163 <div class="flex justify-between gap-2 px-6 mt-2"> 4164 4165 <!-- Buy button --> 4166 4167 <!-- No options --> 4168 4169 <!-- Has options --> 4170 <a href="/hundmat/vegdog-farmers-crunch" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 4171 4172 <!-- Icon (arrow) --> 4173 4174 <!-- Text --> 4175 Läs mer 4176 4177 </a> 4178 4179 <!-- Sold out --> 4180 4181 4182 4183 4184 <!-- Link to product when no buy button --> 4185 4186 <!-- Wishlist --> 4187 <div data-qb-selector="product-item-wishlist" data-product-id="694" class="flex items-center"></div> 4188 4189 </div> 4190 4191 <!-- Stock status --> 4192 <div class="text-sm px-6 pt-4"> 4193 4194 <!-- Sold out --> 4195 4196 <!-- Not sold out --> 4197 <span data-in-stock-text="I lager" 4198 data-out-of-stock-text="" 4199 data-combination-not-exist-text="Variant finns ej"> 4200 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 4201 </span> 4202 4203 </div> 4204 4205 </div> 4206 </div> 4207 </div> 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218 4219 4220 4221 4222 <div x-data="{ layoutSingle: '4' == '1' }" 4223 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 4224 4225 data-qb-selector="product-item" 4226 data-product-id="673" 4227 4228 data-pid="673" 4229 data-s-title="HOWND / Blueberry & Coconut Porridge with Chia & Oats 400g" 4230 data-s-price="55" 4231 data-price="55" 4232 > 4233 4234 <!-- Image --> 4235 <a class="block relative w-full " href="/hundmat/hownd-blueberry-coconut-porridge-400g"> 4236 <div class="relative group overflow-hidden rounded-tnone"> 4237 <img loading="lazy" 4238 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 4239 ix-src="https://cdn.quickbutik.com/images/2666L/products/697a5f76913be.webp" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="HOWND / Blueberry & Coconut Porridge with Chia & Oats 400g"> 4240 4241 <img loading="lazy" 4242 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 4243 ix-src="https://cdn.quickbutik.com/images/2666L/products/697a5f7be924c.webp" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="HOWND / Blueberry & Coconut Porridge with Chia & Oats 400g"> 4244 4245 </div> 4246 </a> 4247 4248 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 4249 <a class="block px-6 mb-4" href="/hundmat/hownd-blueberry-coconut-porridge-400g"> 4250 4251 <!-- Title --> 4252 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 4253 HOWND / Blueberry & Coconut Porridge with Chia & Oats 400g 4254 </h3> 4255 4256 <!-- Reviews container --> 4257 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 4258 4259 </a> 4260 4261 <!-- Description --> 4262 <template x-if="layoutSingle"> 4263 4264 </template> 4265 4266 <div> 4267 4268 <!-- Price --> 4269 <div class="px-6"> 4270 4271 <!-- Current price -->
4272 <span> 4273 <span class="text-xl font-bold text-accent">55 SEK</span> 4274 </span> 4275 4276 <!-- Original price --> 4277 4278 </div> 4279 4280 <!-- Add to basket --> 4281 <div class="flex justify-between gap-2 px-6 mt-2"> 4282 4283 <!-- Buy button --> 4284 4285 <!-- No options --> 4286 <button @click="$store.cart.add({ 4287 productID: 673, 4288 quantity: 1 4289 })" 4290 aria-label="Lägg i korgen" 4291 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 4292 4293 <!-- Icon (cart) --> 4294 4295 <!-- Text --> 4296 Lägg i korgen 4297 4298 </button> 4299 4300 <!-- Has options --> 4301 4302 <!-- Sold out --> 4303 4304 4305 4306 4307 <!-- Link to product when no buy button --> 4308 4309 <!-- Wishlist --> 4310 <div data-qb-selector="product-item-wishlist" data-product-id="673" class="flex items-center"></div> 4311 4312 </div> 4313 4314 <!-- Stock status --> 4315 <div class="text-sm px-6 pt-4"> 4316 4317 <!-- Sold out --> 4318 4319 <!-- Not sold out --> 4320 <span data-in-stock-text="I lager" 4321 data-out-of-stock-text="" 4322 data-combination-not-exist-text="Variant finns ej"> 4323 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 4324 </span> 4325 4326 </div> 4327 4328 </div> 4329 </div> 4330 </div> 4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 4342 4343 4344 4345 <div x-data="{ layoutSingle: '4' == '1' }" 4346 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 4347 4348 data-qb-selector="product-item" 4349 data-product-id="672" 4350 4351 data-pid="672" 4352 data-s-title="HOWND / Papaya Chia & Lentil Dahl 375g" 4353 data-s-price="55" 4354 data-price="55" 4355 > 4356 4357 <!-- Image --> 4358 <a class="block relative w-full " href="/hundmat/hownd-papaya-chia-lentil-dahl375g"> 4359 <div class="relative group overflow-hidden rounded-tnone"> 4360 <img loading="lazy" 4361 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 4362 ix-src="https://cdn.quickbutik.com/images/2666L/products/697a5ddca8468.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="HOWND / Papaya Chia & Lentil Dahl 375g"> 4363 4364 <img loading="lazy" 4365 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 4366 ix-src="https://cdn.quickbutik.com/images/2666L/products/697a5de71a731.webp" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="HOWND / Papaya Chia & Lentil Dahl 375g"> 4367 4368 </div> 4369 </a> 4370 4371 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 4372 <a class="block px-6 mb-4" href="/hundmat/hownd-papaya-chia-lentil-dahl375g"> 4373 4374 <!-- Title --> 4375 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 4376 HOWND / Papaya Chia & Lentil Dahl 375g 4377 </h3> 4378 4379 <!-- Reviews container --> 4380 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 4381 4382 </a> 4383 4384 <!-- Description --> 4385 <template x-if="layoutSingle"> 4386 4387 </template> 4388 4389 <div> 4390 4391 <!-- Price --> 4392 <div class="px-6"> 4393 4394 <!-- Current price -->
4395 <span> 4396 <span class="text-xl font-bold text-accent">55 SEK</span> 4397 </span> 4398 4399 <!-- Original price --> 4400 4401 </div> 4402 4403 <!-- Add to basket --> 4404 <div class="flex justify-between gap-2 px-6 mt-2"> 4405 4406 <!-- Buy button --> 4407 4408 <!-- No options --> 4409 <button @click="$store.cart.add({ 4410 productID: 672, 4411 quantity: 1 4412 })" 4413 aria-label="Lägg i korgen" 4414 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 4415 4416 <!-- Icon (cart) --> 4417 4418 <!-- Text --> 4419 Lägg i korgen 4420 4421 </button> 4422 4423 <!-- Has options --> 4424 4425 <!-- Sold out --> 4426 4427 4428 4429 4430 <!-- Link to product when no buy button --> 4431 4432 <!-- Wishlist --> 4433 <div data-qb-selector="product-item-wishlist" data-product-id="672" class="flex items-center"></div> 4434 4435 </div> 4436 4437 <!-- Stock status --> 4438 <div class="text-sm px-6 pt-4"> 4439 4440 <!-- Sold out --> 4441 4442 <!-- Not sold out --> 4443 <span data-in-stock-text="I lager" 4444 data-out-of-stock-text="" 4445 data-combination-not-exist-text="Variant finns ej"> 4446 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 4447 </span> 4448 4449 </div> 4450 4451 </div> 4452 </div> 4453 </div> 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 <div x-data="{ layoutSingle: '4' == '1' }" 4469 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 4470 4471 data-qb-selector="product-item" 4472 data-product-id="671" 4473 4474 data-pid="671" 4475 data-s-title="HOWND / Quinoa And Pumpkin Casserole 400g" 4476 data-s-price="55" 4477 data-price="55" 4478 > 4479 4480 <!-- Image --> 4481 <a class="block relative w-full " href="/hundmat/hownd-quinoa-and-pumpkin-casserole"> 4482 <div class="relative group overflow-hidden rounded-tnone"> 4483 <img loading="lazy" 4484 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 4485 ix-src="https://cdn.quickbutik.com/images/2666L/products/61768f1382525.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="HOWND / Quinoa And Pumpkin Casserole 400g"> 4486 4487 <img loading="lazy" 4488 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 4489 ix-src="https://cdn.quickbutik.com/images/2666L/products/657b6d9cd37ee.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="HOWND / Quinoa And Pumpkin Casserole 400g"> 4490 4491 </div> 4492 </a> 4493 4494 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 4495 <a class="block px-6 mb-4" href="/hundmat/hownd-quinoa-and-pumpkin-casserole"> 4496 4497 <!-- Title --> 4498 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 4499 HOWND / Quinoa And Pumpkin Casserole 400g 4500 </h3> 4501 4502 <!-- Reviews container --> 4503 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 4504 4505 </a> 4506 4507 <!-- Description --> 4508 <template x-if="layoutSingle"> 4509 4510 </template> 4511 4512 <div> 4513 4514 <!-- Price --> 4515 <div class="px-6"> 4516 4517 <!-- Current price -->
4518 <span> 4519 <span class="text-xl font-bold text-accent">55 SEK</span> 4520 </span> 4521 4522 <!-- Original price --> 4523 4524 </div> 4525 4526 <!-- Add to basket --> 4527 <div class="flex justify-between gap-2 px-6 mt-2"> 4528 4529 <!-- Buy button --> 4530 4531 <!-- No options --> 4532 4533 <!-- Has options --> 4534 4535 <!-- Sold out --> 4536 <a href="/hundmat/hownd-quinoa-and-pumpkin-casserole" area-label="Ej i lager" class="block w-full button-py px-2 bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 4537 4538 <!-- Icon --> 4539 4540 <!-- Text --> 4541 Läs mer 4542 4543 </a> 4544 4545 4546 4547 4548 <!-- Link to product when no buy button --> 4549 4550 <!-- Wishlist --> 4551 <div data-qb-selector="product-item-wishlist" data-product-id="671" class="flex items-center"></div> 4552 4553 </div> 4554 4555 <!-- Stock status --> 4556 <div class="text-sm px-6 pt-4"> 4557 4558 <!-- Sold out --> 4559 <span> 4560 <span class="inline-block bg-red-600 rounded-full h-2 w-2 mr-1"></span> Ej i lager 4561 </span> 4562 4563 <!-- Not sold out --> 4564 4565 </div> 4566 4567 </div> 4568 </div> 4569 </div> 4570 4571 4572 4573 4574 4575 4576 4577 4578 4579 4580 4581 4582 4583 4584 <div x-data="{ layoutSingle: '4' == '1' }" 4585 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 4586 4587 data-qb-selector="product-item" 4588 data-product-id="670" 4589 4590 data-pid="670" 4591 data-s-title="HOWND / Delicious Pumpkin Quinoa & Moringa Superfood 2kg/10kg" 4592 data-s-price="249.00" 4593 data-price="249.00" 4594 > 4595 4596 <!-- Image --> 4597 <a class="block relative w-full " href="/hundmat/hownd-plant-powered-superfood-with-hemp-protein-and-moringa"> 4598 <div class="relative group overflow-hidden rounded-tnone"> 4599 <img loading="lazy" 4600 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 4601 ix-src="https://cdn.quickbutik.com/images/2666L/products/660bca54cf49b.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="HOWND / Delicious Pumpkin Quinoa & Moringa Superfood 2kg/10kg"> 4602 4603 <img loading="lazy" 4604 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 4605 ix-src="https://cdn.quickbutik.com/images/2666L/products/660bca54db7e8.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="HOWND / Delicious Pumpkin Quinoa & Moringa Superfood 2kg/10kg"> 4606 4607 </div> 4608 </a> 4609 4610 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 4611 <a class="block px-6 mb-4" href="/hundmat/hownd-plant-powered-superfood-with-hemp-protein-and-moringa"> 4612 4613 <!-- Title --> 4614 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 4615 HOWND / Delicious Pumpkin Quinoa & Moringa Superfood 2kg/10kg 4616 </h3> 4617 4618 <!-- Reviews container --> 4619 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 4620 4621 </a> 4622 4623 <!-- Description --> 4624 <template x-if="layoutSingle"> 4625 4626 </template> 4627 4628 <div> 4629 4630 <!-- Price --> 4631 <div class="px-6"> 4632 4633 <!-- Current price -->
4634 <span> 4635 <span class="text-xl font-bold text-accent">249 SEK</span> 4636 </span> 4637 4638 <!-- Original price --> 4639 4640 </div> 4641 4642 <!-- Add to basket --> 4643 <div class="flex justify-between gap-2 px-6 mt-2"> 4644 4645 <!-- Buy button --> 4646 4647 <!-- No options --> 4648 4649 <!-- Has options --> 4650 4651 <!-- Sold out --> 4652 <a href="/hundmat/hownd-plant-powered-superfood-with-hemp-protein-and-moringa" area-label="Ej i lager" class="block w-full button-py px-2 bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 4653 4654 <!-- Icon --> 4655 4656 <!-- Text --> 4657 Läs mer 4658 4659 </a> 4660 4661 4662 4663 4664 <!-- Link to product when no buy button --> 4665 4666 <!-- Wishlist --> 4667 <div data-qb-selector="product-item-wishlist" data-product-id="670" class="flex items-center"></div> 4668 4669 </div> 4670 4671 <!-- Stock status --> 4672 <div class="text-sm px-6 pt-4"> 4673 4674 <!-- Sold out --> 4675 <span> 4676 <span class="inline-block bg-red-600 rounded-full h-2 w-2 mr-1"></span> Ej i lager 4677 </span> 4678 4679 <!-- Not sold out --> 4680 4681 </div> 4682 4683 </div> 4684 </div> 4685 </div> 4686 4687 4688 4689 4690 4691 4692 4693 4694 4695 4696 4697 4698 4699 4700 <div x-data="{ layoutSingle: '4' == '1' }" 4701 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 4702 4703 data-qb-selector="product-item" 4704 data-product-id="665" 4705 4706 data-pid="665" 4707 data-s-title="VEGDOG Vuxen Ãrtor & Havregryn i Tetra Pak" 4708 data-s-price="39.00" 4709 data-price="39.00" 4710 > 4711 4712 <!-- Image --> 4713 <a class="block relative w-full " href="/hundmat/vegdog-adult-200g"> 4714 <div class="relative group overflow-hidden rounded-tnone"> 4715 <img loading="lazy" 4716 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 4717 ix-src="https://cdn.quickbutik.com/images/2666L/products/656a15c113b13.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Vuxen Ãrtor & Havregryn i Tetra Pak"> 4718 4719 <img loading="lazy" 4720 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 4721 ix-src="https://cdn.quickbutik.com/images/2666L/products/656a15c147329.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Vuxen Ãrtor & Havregryn i Tetra Pak"> 4722 4723 </div> 4724 </a> 4725 4726 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 4727 <a class="block px-6 mb-4" href="/hundmat/vegdog-adult-200g"> 4728 4729 <!-- Title --> 4730 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 4731 VEGDOG Vuxen Ãrtor & Havregryn i Tetra Pak 4732 </h3> 4733 4734 <!-- Reviews container --> 4735 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 4736 4737 </a> 4738 4739 <!-- Description --> 4740 <template x-if="layoutSingle"> 4741 4742 </template> 4743 4744 <div> 4745 4746 <!-- Price --> 4747 <div class="px-6"> 4748 4749 <!-- Current price -->
4750 <span> 4751 <span class="text-xl font-bold text-accent">39 SEK</span> 4752 </span> 4753 4754 <!-- Original price --> 4755 4756 </div> 4757 4758 <!-- Add to basket --> 4759 <div class="flex justify-between gap-2 px-6 mt-2"> 4760 4761 <!-- Buy button --> 4762 4763 <!-- No options --> 4764 4765 <!-- Has options --> 4766 <a href="/hundmat/vegdog-adult-200g" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 4767 4768 <!-- Icon (arrow) --> 4769 4770 <!-- Text --> 4771 Läs mer 4772 4773 </a> 4774 4775 <!-- Sold out --> 4776 4777 4778 4779 4780 <!-- Link to product when no buy button --> 4781 4782 <!-- Wishlist --> 4783 <div data-qb-selector="product-item-wishlist" data-product-id="665" class="flex items-center"></div> 4784 4785 </div> 4786 4787 <!-- Stock status --> 4788 <div class="text-sm px-6 pt-4"> 4789 4790 <!-- Sold out --> 4791 4792 <!-- Not sold out --> 4793 <span data-in-stock-text="I lager" 4794 data-out-of-stock-text="" 4795 data-combination-not-exist-text="Variant finns ej"> 4796 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 4797 </span> 4798 4799 </div> 4800 4801 </div> 4802 </div> 4803 </div> 4804 4805 4806 4807 4808 4809 4810 4811 4812 4813 4814 4815 4816 4817 4818 <div x-data="{ layoutSingle: '4' == '1' }" 4819 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 4820 4821 data-qb-selector="product-item" 4822 data-product-id="643" 4823 4824 data-pid="643" 4825 data-s-title="AMI DOG Wet ORANGE 400g" 4826 data-s-price="45" 4827 data-price="45" 4828 > 4829 4830 <!-- Image --> 4831 <a class="block relative w-full " href="/hundmat/ami-dog-wet-orange-400g"> 4832 <div class="relative group overflow-hidden rounded-tnone"> 4833 <img loading="lazy" 4834 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 4835 ix-src="https://cdn.quickbutik.com/images/2666L/products/6544be2090f04.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Wet ORANGE 400g"> 4836 4837 <img loading="lazy" 4838 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 4839 ix-src="https://cdn.quickbutik.com/images/2666L/products/657b0d4ae16d7.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Wet ORANGE 400g"> 4840 4841 </div> 4842 </a> 4843 4844 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 4845 <a class="block px-6 mb-4" href="/hundmat/ami-dog-wet-orange-400g"> 4846 4847 <!-- Title --> 4848 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 4849 AMI DOG Wet ORANGE 400g 4850 </h3> 4851 4852 <!-- Reviews container --> 4853 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 4854 4855 </a> 4856 4857 <!-- Description --> 4858 <template x-if="layoutSingle"> 4859 4860 </template> 4861 4862 <div> 4863 4864 <!-- Price --> 4865 <div class="px-6"> 4866 4867 <!-- Current price -->
4868 <span> 4869 <span class="text-xl font-bold text-accent">45 SEK</span> 4870 </span> 4871 4872 <!-- Original price --> 4873 4874 </div> 4875 4876 <!-- Add to basket --> 4877 <div class="flex justify-between gap-2 px-6 mt-2"> 4878 4879 <!-- Buy button --> 4880 4881 <!-- No options --> 4882 <button @click="$store.cart.add({ 4883 productID: 643, 4884 quantity: 1 4885 })" 4886 aria-label="Lägg i korgen" 4887 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 4888 4889 <!-- Icon (cart) --> 4890 4891 <!-- Text --> 4892 Lägg i korgen 4893 4894 </button> 4895 4896 <!-- Has options --> 4897 4898 <!-- Sold out --> 4899 4900 4901 4902 4903 <!-- Link to product when no buy button --> 4904 4905 <!-- Wishlist --> 4906 <div data-qb-selector="product-item-wishlist" data-product-id="643" class="flex items-center"></div> 4907 4908 </div> 4909 4910 <!-- Stock status --> 4911 <div class="text-sm px-6 pt-4"> 4912 4913 <!-- Sold out --> 4914 4915 <!-- Not sold out --> 4916 <span data-in-stock-text="I lager" 4917 data-out-of-stock-text="" 4918 data-combination-not-exist-text="Variant finns ej"> 4919 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 4920 </span> 4921 4922 </div> 4923 4924 </div> 4925 </div> 4926 </div> 4927 4928 4929 4930 4931 4932 4933 4934 4935 4936 4937 4938 4939 4940 4941 <div x-data="{ layoutSingle: '4' == '1' }" 4942 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 4943 4944 data-qb-selector="product-item" 4945 data-product-id="633" 4946 4947 data-pid="633" 4948 data-s-title="AMI DOG Mini & Toys 1kg/3kg" 4949 data-s-price="159.00" 4950 data-price="159.00" 4951 > 4952 4953 <!-- Image --> 4954 <a class="block relative w-full " href="/hundmat/ami-dog-small-size"> 4955 <div class="relative group overflow-hidden rounded-tnone"> 4956 <img loading="lazy" 4957 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 4958 ix-src="https://cdn.quickbutik.com/images/2666L/products/66b0633573cf0.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Mini & Toys 1kg/3kg"> 4959 4960 <img loading="lazy" 4961 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 4962 ix-src="https://cdn.quickbutik.com/images/2666L/products/67d9390121029.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Mini & Toys 1kg/3kg"> 4963 4964 </div> 4965 </a> 4966 4967 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 4968 <a class="block px-6 mb-4" href="/hundmat/ami-dog-small-size"> 4969 4970 <!-- Title --> 4971 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 4972 AMI DOG Mini & Toys 1kg/3kg 4973 </h3> 4974 4975 <!-- Reviews container --> 4976 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 4977 4978 </a> 4979 4980 <!-- Description --> 4981 <template x-if="layoutSingle"> 4982 4983 </template> 4984 4985 <div> 4986 4987 <!-- Price --> 4988 <div class="px-6"> 4989 4990 <!-- Current price -->
4991 <span> 4992 <span class="text-xl font-bold text-accent">159 SEK</span> 4993 </span> 4994 4995 <!-- Original price --> 4996 4997 </div> 4998 4999 <!-- Add to basket --> 5000 <div class="flex justify-between gap-2 px-6 mt-2"> 5001 5002 <!-- Buy button --> 5003 5004 <!-- No options --> 5005 5006 <!-- Has options --> 5007 <a href="/hundmat/ami-dog-small-size" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 5008 5009 <!-- Icon (arrow) --> 5010 5011 <!-- Text --> 5012 Läs mer 5013 5014 </a> 5015 5016 <!-- Sold out --> 5017 5018 5019 5020 5021 <!-- Link to product when no buy button --> 5022 5023 <!-- Wishlist --> 5024 <div data-qb-selector="product-item-wishlist" data-product-id="633" class="flex items-center"></div> 5025 5026 </div> 5027 5028 <!-- Stock status --> 5029 <div class="text-sm px-6 pt-4"> 5030 5031 <!-- Sold out --> 5032 5033 <!-- Not sold out --> 5034 <span data-in-stock-text="I lager" 5035 data-out-of-stock-text="" 5036 data-combination-not-exist-text="Variant finns ej"> 5037 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 5038 </span> 5039 5040 </div> 5041 5042 </div> 5043 </div> 5044 </div> 5045 5046 5047 5048 5049 5050 5051 5052 5053 5054 5055 5056 5057 5058 5059 <div x-data="{ layoutSingle: '4' == '1' }" 5060 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 5061 5062 data-qb-selector="product-item" 5063 data-product-id="629" 5064 5065 data-pid="629" 5066 data-s-title="Vegan4Dogs Pauline 400g/800g" 5067 data-s-price="59.00" 5068 data-price="59.00" 5069 > 5070 5071 <!-- Image --> 5072 <a class="block relative w-full " href="/hundmat/vegan4dogs-pauline-400g800g"> 5073 <div class="relative group overflow-hidden rounded-tnone"> 5074 <img loading="lazy" 5075 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 5076 ix-src="https://cdn.quickbutik.com/images/2666L/products/65bbb710d2481.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Vegan4Dogs Pauline 400g/800g"> 5077 5078 <img loading="lazy" 5079 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 5080 ix-src="https://cdn.quickbutik.com/images/2666L/products/65bbb71152d6b.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Vegan4Dogs Pauline 400g/800g"> 5081 5082 </div> 5083 </a> 5084 5085 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 5086 <a class="block px-6 mb-4" href="/hundmat/vegan4dogs-pauline-400g800g"> 5087 5088 <!-- Title --> 5089 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 5090 Vegan4Dogs Pauline 400g/800g 5091 </h3> 5092 5093 <!-- Reviews container --> 5094 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 5095 5096 </a> 5097 5098 <!-- Description --> 5099 <template x-if="layoutSingle"> 5100 5101 </template> 5102 5103 <div> 5104 5105 <!-- Price --> 5106 <div class="px-6"> 5107 5108 <!-- Current price -->
5109 <span> 5110 <span class="text-xl font-bold text-accent">59 SEK</span> 5111 </span> 5112 5113 <!-- Original price --> 5114 5115 </div> 5116 5117 <!-- Add to basket --> 5118 <div class="flex justify-between gap-2 px-6 mt-2"> 5119 5120 <!-- Buy button --> 5121 5122 <!-- No options --> 5123 5124 <!-- Has options --> 5125 5126 <!-- Sold out --> 5127 <a href="/hundmat/vegan4dogs-pauline-400g800g" area-label="Ej i lager" class="block w-full button-py px-2 bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 5128 5129 <!-- Icon --> 5130 5131 <!-- Text --> 5132 Läs mer 5133 5134 </a> 5135 5136 5137 5138 5139 <!-- Link to product when no buy button --> 5140 5141 <!-- Wishlist --> 5142 <div data-qb-selector="product-item-wishlist" data-product-id="629" class="flex items-center"></div> 5143 5144 </div> 5145 5146 <!-- Stock status --> 5147 <div class="text-sm px-6 pt-4"> 5148 5149 <!-- Sold out --> 5150 <span> 5151 <span class="inline-block bg-red-600 rounded-full h-2 w-2 mr-1"></span> Ej i lager 5152 </span> 5153 5154 <!-- Not sold out --> 5155 5156 </div> 5157 5158 </div> 5159 </div> 5160 </div> 5161 5162 5163 5164 5165 5166 5167 5168 5169 5170 5171 5172 5173 5174 5175 <div x-data="{ layoutSingle: '4' == '1' }" 5176 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 5177 5178 data-qb-selector="product-item" 5179 data-product-id="619" 5180 5181 data-pid="619" 5182 data-s-title="VEGDOG Green Crunch 1kg/4kg/10kg" 5183 data-s-price="149.00" 5184 data-price="149.00" 5185 > 5186 5187 <!-- Image --> 5188 <a class="block relative w-full " href="/hundmat/vegdog-green-crunch"> 5189 <div class="relative group overflow-hidden rounded-tnone"> 5190 <img loading="lazy" 5191 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 5192 ix-src="https://cdn.quickbutik.com/images/2666L/products/69e9cb262c830.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Green Crunch 1kg/4kg/10kg"> 5193 5194 <img loading="lazy" 5195 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 5196 ix-src="https://cdn.quickbutik.com/images/2666L/products/69e9cb1794510.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Green Crunch 1kg/4kg/10kg"> 5197 5198 </div> 5199 </a> 5200 5201 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 5202 <a class="block px-6 mb-4" href="/hundmat/vegdog-green-crunch"> 5203 5204 <!-- Title --> 5205 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 5206 VEGDOG Green Crunch 1kg/4kg/10kg 5207 </h3> 5208 5209 <!-- Reviews container --> 5210 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 5211 5212 </a> 5213 5214 <!-- Description --> 5215 <template x-if="layoutSingle"> 5216 5217 </template> 5218 5219 <div> 5220 5221 <!-- Price --> 5222 <div class="px-6"> 5223 5224 <!-- Current price -->
5225 <span> 5226 <span class="text-xl font-bold text-accent">149 SEK</span> 5227 </span> 5228 5229 <!-- Original price --> 5230 5231 </div> 5232 5233 <!-- Add to basket --> 5234 <div class="flex justify-between gap-2 px-6 mt-2"> 5235 5236 <!-- Buy button --> 5237 5238 <!-- No options --> 5239 5240 <!-- Has options --> 5241 <a href="/hundmat/vegdog-green-crunch" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 5242 5243 <!-- Icon (arrow) --> 5244 5245 <!-- Text --> 5246 Läs mer 5247 5248 </a> 5249 5250 <!-- Sold out --> 5251 5252 5253 5254 5255 <!-- Link to product when no buy button --> 5256 5257 <!-- Wishlist --> 5258 <div data-qb-selector="product-item-wishlist" data-product-id="619" class="flex items-center"></div> 5259 5260 </div> 5261 5262 <!-- Stock status --> 5263 <div class="text-sm px-6 pt-4"> 5264 5265 <!-- Sold out --> 5266 5267 <!-- Not sold out --> 5268 <span data-in-stock-text="I lager" 5269 data-out-of-stock-text="" 5270 data-combination-not-exist-text="Variant finns ej"> 5271 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 5272 </span> 5273 5274 </div> 5275 5276 </div> 5277 </div> 5278 </div> 5279 5280 5281 5282 5283 5284 5285 5286 5287 5288 5289 5290 5291 5292 5293 <div x-data="{ layoutSingle: '4' == '1' }" 5294 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 5295 5296 data-qb-selector="product-item" 5297 data-product-id="362" 5298 5299 data-pid="362" 5300 data-s-title="AMI DOG Wet GREEN 400g" 5301 data-s-price="45" 5302 data-price="45" 5303 > 5304 5305 <!-- Image --> 5306 <a class="block relative w-full " href="/hundmat/ami-dog-wet-green-400g"> 5307 <div class="relative group overflow-hidden rounded-tnone"> 5308 <img loading="lazy" 5309 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 5310 ix-src="https://cdn.quickbutik.com/images/2666L/products/6544bdb8de3d9.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Wet GREEN 400g"> 5311 5312 <img loading="lazy" 5313 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 5314 ix-src="https://cdn.quickbutik.com/images/2666L/products/657b09f691c89.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Wet GREEN 400g"> 5315 5316 </div> 5317 </a> 5318 5319 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 5320 <a class="block px-6 mb-4" href="/hundmat/ami-dog-wet-green-400g"> 5321 5322 <!-- Title --> 5323 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 5324 AMI DOG Wet GREEN 400g 5325 </h3> 5326 5327 <!-- Reviews container --> 5328 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 5329 5330 </a> 5331 5332 <!-- Description --> 5333 <template x-if="layoutSingle"> 5334 5335 </template> 5336 5337 <div> 5338 5339 <!-- Price --> 5340 <div class="px-6"> 5341 5342 <!-- Current price -->
5343 <span> 5344 <span class="text-xl font-bold text-accent">45 SEK</span> 5345 </span> 5346 5347 <!-- Original price --> 5348 5349 </div> 5350 5351 <!-- Add to basket --> 5352 <div class="flex justify-between gap-2 px-6 mt-2"> 5353 5354 <!-- Buy button --> 5355 5356 <!-- No options --> 5357 <button @click="$store.cart.add({ 5358 productID: 362, 5359 quantity: 1 5360 })" 5361 aria-label="Lägg i korgen" 5362 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 5363 5364 <!-- Icon (cart) --> 5365 5366 <!-- Text --> 5367 Lägg i korgen 5368 5369 </button> 5370 5371 <!-- Has options --> 5372 5373 <!-- Sold out --> 5374 5375 5376 5377 5378 <!-- Link to product when no buy button --> 5379 5380 <!-- Wishlist --> 5381 <div data-qb-selector="product-item-wishlist" data-product-id="362" class="flex items-center"></div> 5382 5383 </div> 5384 5385 <!-- Stock status --> 5386 <div class="text-sm px-6 pt-4"> 5387 5388 <!-- Sold out --> 5389 5390 <!-- Not sold out --> 5391 <span data-in-stock-text="I lager" 5392 data-out-of-stock-text="" 5393 data-combination-not-exist-text="Variant finns ej"> 5394 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 5395 </span> 5396 5397 </div> 5398 5399 </div> 5400 </div> 5401 </div> 5402 5403 5404 5405 5406 5407 5408 5409 5410 5411 5412 5413 5414 5415 5416 <div x-data="{ layoutSingle: '4' == '1' }" 5417 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 5418 5419 data-qb-selector="product-item" 5420 data-product-id="361" 5421 5422 data-pid="361" 5423 data-s-title="AMI DOG Wet RED 400g" 5424 data-s-price="45" 5425 data-price="45" 5426 > 5427 5428 <!-- Image --> 5429 <a class="block relative w-full " href="/hundmat/ami-dog-wet-red-400g"> 5430 <div class="relative group overflow-hidden rounded-tnone"> 5431 <img loading="lazy" 5432 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 5433 ix-src="https://cdn.quickbutik.com/images/2666L/products/6544be4dd4e31.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Wet RED 400g"> 5434 5435 <img loading="lazy" 5436 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 5437 ix-src="https://cdn.quickbutik.com/images/2666L/products/657b0d9a8ee46.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Wet RED 400g"> 5438 5439 </div> 5440 </a> 5441 5442 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 5443 <a class="block px-6 mb-4" href="/hundmat/ami-dog-wet-red-400g"> 5444 5445 <!-- Title --> 5446 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 5447 AMI DOG Wet RED 400g 5448 </h3> 5449 5450 <!-- Reviews container --> 5451 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 5452 5453 </a> 5454 5455 <!-- Description --> 5456 <template x-if="layoutSingle"> 5457 5458 </template> 5459 5460 <div> 5461 5462 <!-- Price --> 5463 <div class="px-6"> 5464 5465 <!-- Current price -->
5466 <span> 5467 <span class="text-xl font-bold text-accent">45 SEK</span> 5468 </span> 5469 5470 <!-- Original price --> 5471 5472 </div> 5473 5474 <!-- Add to basket --> 5475 <div class="flex justify-between gap-2 px-6 mt-2"> 5476 5477 <!-- Buy button --> 5478 5479 <!-- No options --> 5480 <button @click="$store.cart.add({ 5481 productID: 361, 5482 quantity: 1 5483 })" 5484 aria-label="Lägg i korgen" 5485 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 5486 5487 <!-- Icon (cart) --> 5488 5489 <!-- Text --> 5490 Lägg i korgen 5491 5492 </button> 5493 5494 <!-- Has options --> 5495 5496 <!-- Sold out --> 5497 5498 5499 5500 5501 <!-- Link to product when no buy button --> 5502 5503 <!-- Wishlist --> 5504 <div data-qb-selector="product-item-wishlist" data-product-id="361" class="flex items-center"></div> 5505 5506 </div> 5507 5508 <!-- Stock status --> 5509 <div class="text-sm px-6 pt-4"> 5510 5511 <!-- Sold out --> 5512 5513 <!-- Not sold out --> 5514 <span data-in-stock-text="I lager" 5515 data-out-of-stock-text="" 5516 data-combination-not-exist-text="Variant finns ej"> 5517 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 5518 </span> 5519 5520 </div> 5521 5522 </div> 5523 </div> 5524 </div> 5525 5526 5527 5528 5529 5530 5531 5532 5533 5534 5535 5536 5537 5538 5539 <div x-data="{ layoutSingle: '4' == '1' }" 5540 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 5541 5542 data-qb-selector="product-item" 5543 data-product-id="360" 5544 5545 data-pid="360" 5546 data-s-title="AMI DOG Wet YELLOW 400g" 5547 data-s-price="45" 5548 data-price="45" 5549 > 5550 5551 <!-- Image --> 5552 <a class="block relative w-full " href="/hundmat/ami-dog-wet-yellow-400g"> 5553 <div class="relative group overflow-hidden rounded-tnone"> 5554 <img loading="lazy" 5555 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 5556 ix-src="https://cdn.quickbutik.com/images/2666L/products/6544be77c72ac.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Wet YELLOW 400g"> 5557 5558 <img loading="lazy" 5559 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 5560 ix-src="https://cdn.quickbutik.com/images/2666L/products/657b0eaf0d389.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Wet YELLOW 400g"> 5561 5562 </div> 5563 </a> 5564 5565 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 5566 <a class="block px-6 mb-4" href="/hundmat/ami-dog-wet-yellow-400g"> 5567 5568 <!-- Title --> 5569 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 5570 AMI DOG Wet YELLOW 400g 5571 </h3> 5572 5573 <!-- Reviews container --> 5574 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 5575 5576 </a> 5577 5578 <!-- Description --> 5579 <template x-if="layoutSingle"> 5580 5581 </template> 5582 5583 <div> 5584 5585 <!-- Price --> 5586 <div class="px-6"> 5587 5588 <!-- Current price -->
5589 <span> 5590 <span class="text-xl font-bold text-accent">45 SEK</span> 5591 </span> 5592 5593 <!-- Original price --> 5594 5595 </div> 5596 5597 <!-- Add to basket --> 5598 <div class="flex justify-between gap-2 px-6 mt-2"> 5599 5600 <!-- Buy button --> 5601 5602 <!-- No options --> 5603 <button @click="$store.cart.add({ 5604 productID: 360, 5605 quantity: 1 5606 })" 5607 aria-label="Lägg i korgen" 5608 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 5609 5610 <!-- Icon (cart) --> 5611 5612 <!-- Text --> 5613 Lägg i korgen 5614 5615 </button> 5616 5617 <!-- Has options --> 5618 5619 <!-- Sold out --> 5620 5621 5622 5623 5624 <!-- Link to product when no buy button --> 5625 5626 <!-- Wishlist --> 5627 <div data-qb-selector="product-item-wishlist" data-product-id="360" class="flex items-center"></div> 5628 5629 </div> 5630 5631 <!-- Stock status --> 5632 <div class="text-sm px-6 pt-4"> 5633 5634 <!-- Sold out --> 5635 5636 <!-- Not sold out --> 5637 <span data-in-stock-text="I lager" 5638 data-out-of-stock-text="" 5639 data-combination-not-exist-text="Variant finns ej"> 5640 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 5641 </span> 5642 5643 </div> 5644 5645 </div> 5646 </div> 5647 </div> 5648 5649 5650 5651 5652 5653 5654 5655 5656 5657 5658 5659 5660 5661 5662 <div x-data="{ layoutSingle: '4' == '1' }" 5663 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 5664 5665 data-qb-selector="product-item" 5666 data-product-id="134" 5667 5668 data-pid="134" 5669 data-s-title="Vegan4Dogs Greta Mini 2kg/10kg" 5670 data-s-price="259.00" 5671 data-price="259.00" 5672 > 5673 5674 <!-- Image --> 5675 <a class="block relative w-full " href="/hundmat/vegan4dogs-greta-mini"> 5676 <div class="relative group overflow-hidden rounded-tnone"> 5677 <img loading="lazy" 5678 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 5679 ix-src="https://cdn.quickbutik.com/images/2666L/products/5ba4db4e69a98.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Vegan4Dogs Greta Mini 2kg/10kg"> 5680 5681 <img loading="lazy" 5682 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 5683 ix-src="https://cdn.quickbutik.com/images/2666L/products/6542a52240e5e.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Vegan4Dogs Greta Mini 2kg/10kg"> 5684 5685 </div> 5686 </a> 5687 5688 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 5689 <a class="block px-6 mb-4" href="/hundmat/vegan4dogs-greta-mini"> 5690 5691 <!-- Title --> 5692 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 5693 Vegan4Dogs Greta Mini 2kg/10kg 5694 </h3> 5695 5696 <!-- Reviews container --> 5697 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 5698 5699 </a> 5700 5701 <!-- Description --> 5702 <template x-if="layoutSingle"> 5703 5704 </template> 5705 5706 <div> 5707 5708 <!-- Price --> 5709 <div class="px-6"> 5710 5711 <!-- Current price -->
5712 <span> 5713 <span class="text-xl font-bold text-accent">259 SEK</span> 5714 </span> 5715 5716 <!-- Original price --> 5717 5718 </div> 5719 5720 <!-- Add to basket --> 5721 <div class="flex justify-between gap-2 px-6 mt-2"> 5722 5723 <!-- Buy button --> 5724 5725 <!-- No options --> 5726 5727 <!-- Has options --> 5728 5729 <!-- Sold out --> 5730 <a href="/hundmat/vegan4dogs-greta-mini" area-label="Ej i lager" class="block w-full button-py px-2 bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 5731 5732 <!-- Icon --> 5733 5734 <!-- Text --> 5735 Läs mer 5736 5737 </a> 5738 5739 5740 5741 5742 <!-- Link to product when no buy button --> 5743 5744 <!-- Wishlist --> 5745 <div data-qb-selector="product-item-wishlist" data-product-id="134" class="flex items-center"></div> 5746 5747 </div> 5748 5749 <!-- Stock status --> 5750 <div class="text-sm px-6 pt-4"> 5751 5752 <!-- Sold out --> 5753 <span> 5754 <span class="inline-block bg-red-600 rounded-full h-2 w-2 mr-1"></span> Ej i lager 5755 </span> 5756 5757 <!-- Not sold out --> 5758 5759 </div> 5760 5761 </div> 5762 </div> 5763 </div> 5764 5765 5766 5767 5768 5769 5770 5771 5772 5773 5774 5775 5776 5777 5778 <div x-data="{ layoutSingle: '4' == '1' }" 5779 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 5780 5781 data-qb-selector="product-item" 5782 data-product-id="57" 5783 5784 data-pid="57" 5785 data-s-title="VEGDOG Sensitive lupinprotein 400g/800g" 5786 data-s-price="45.00" 5787 data-price="45.00" 5788 > 5789 5790 <!-- Image --> 5791 <a class="block relative w-full " href="/hundmat/vegdog-sensitive-lupinprotein-400g800g"> 5792 <div class="relative group overflow-hidden rounded-tnone"> 5793 <img loading="lazy" 5794 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 5795 ix-src="https://cdn.quickbutik.com/images/2666L/products/656a091ea626d.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Sensitive lupinprotein 400g/800g"> 5796 5797 <img loading="lazy" 5798 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 5799 ix-src="https://cdn.quickbutik.com/images/2666L/products/656a091ead7ce.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Sensitive lupinprotein 400g/800g"> 5800 5801 </div> 5802 </a> 5803 5804 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 5805 <a class="block px-6 mb-4" href="/hundmat/vegdog-sensitive-lupinprotein-400g800g"> 5806 5807 <!-- Title --> 5808 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 5809 VEGDOG Sensitive lupinprotein 400g/800g 5810 </h3> 5811 5812 <!-- Reviews container --> 5813 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 5814 5815 </a> 5816 5817 <!-- Description --> 5818 <template x-if="layoutSingle"> 5819 5820 </template> 5821 5822 <div> 5823 5824 <!-- Price --> 5825 <div class="px-6"> 5826 5827 <!-- Current price -->
5828 <span> 5829 <span class="text-xl font-bold text-accent">45 SEK</span> 5830 </span> 5831 5832 <!-- Original price --> 5833 5834 </div> 5835 5836 <!-- Add to basket --> 5837 <div class="flex justify-between gap-2 px-6 mt-2"> 5838 5839 <!-- Buy button --> 5840 5841 <!-- No options --> 5842 5843 <!-- Has options --> 5844 <a href="/hundmat/vegdog-sensitive-lupinprotein-400g800g" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 5845 5846 <!-- Icon (arrow) --> 5847 5848 <!-- Text --> 5849 Läs mer 5850 5851 </a> 5852 5853 <!-- Sold out --> 5854 5855 5856 5857 5858 <!-- Link to product when no buy button --> 5859 5860 <!-- Wishlist --> 5861 <div data-qb-selector="product-item-wishlist" data-product-id="57" class="flex items-center"></div> 5862 5863 </div> 5864 5865 <!-- Stock status --> 5866 <div class="text-sm px-6 pt-4"> 5867 5868 <!-- Sold out --> 5869 5870 <!-- Not sold out --> 5871 <span data-in-stock-text="I lager" 5872 data-out-of-stock-text="" 5873 data-combination-not-exist-text="Variant finns ej"> 5874 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 5875 </span> 5876 5877 </div> 5878 5879 </div> 5880 </div> 5881 </div> 5882 5883 5884 5885 5886 5887 5888 5889 5890 5891 5892 5893 5894 5895 5896 <div x-data="{ layoutSingle: '4' == '1' }" 5897 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 5898 5899 data-qb-selector="product-item" 5900 data-product-id="56" 5901 5902 data-pid="56" 5903 data-s-title="VEGDOG Senior 400g/800g" 5904 data-s-price="45.00" 5905 data-price="45.00" 5906 > 5907 5908 <!-- Image --> 5909 <a class="block relative w-full " href="/hundmat/vegdog-senior-400g"> 5910 <div class="relative group overflow-hidden rounded-tnone"> 5911 <img loading="lazy" 5912 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 5913 ix-src="https://cdn.quickbutik.com/images/2666L/products/656a0e6b14f6d.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Senior 400g/800g"> 5914 5915 <img loading="lazy" 5916 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 5917 ix-src="https://cdn.quickbutik.com/images/2666L/products/656a0e6b4a727.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Senior 400g/800g"> 5918 5919 </div> 5920 </a> 5921 5922 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 5923 <a class="block px-6 mb-4" href="/hundmat/vegdog-senior-400g"> 5924 5925 <!-- Title --> 5926 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 5927 VEGDOG Senior 400g/800g 5928 </h3> 5929 5930 <!-- Reviews container --> 5931 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 5932 5933 </a> 5934 5935 <!-- Description --> 5936 <template x-if="layoutSingle"> 5937 5938 </template> 5939 5940 <div> 5941 5942 <!-- Price --> 5943 <div class="px-6"> 5944 5945 <!-- Current price -->
5946 <span> 5947 <span class="text-xl font-bold text-accent">45 SEK</span> 5948 </span> 5949 5950 <!-- Original price --> 5951 5952 </div> 5953 5954 <!-- Add to basket --> 5955 <div class="flex justify-between gap-2 px-6 mt-2"> 5956 5957 <!-- Buy button --> 5958 5959 <!-- No options --> 5960 5961 <!-- Has options --> 5962 <a href="/hundmat/vegdog-senior-400g" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 5963 5964 <!-- Icon (arrow) --> 5965 5966 <!-- Text --> 5967 Läs mer 5968 5969 </a> 5970 5971 <!-- Sold out --> 5972 5973 5974 5975 5976 <!-- Link to product when no buy button --> 5977 5978 <!-- Wishlist --> 5979 <div data-qb-selector="product-item-wishlist" data-product-id="56" class="flex items-center"></div> 5980 5981 </div> 5982 5983 <!-- Stock status --> 5984 <div class="text-sm px-6 pt-4"> 5985 5986 <!-- Sold out --> 5987 5988 <!-- Not sold out --> 5989 <span data-in-stock-text="I lager" 5990 data-out-of-stock-text="" 5991 data-combination-not-exist-text="Variant finns ej"> 5992 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 5993 </span> 5994 5995 </div> 5996 5997 </div> 5998 </div> 5999 </div> 6000 6001 6002 6003 6004 6005 6006 6007 6008 6009 6010 6011 6012 6013 6014 <div x-data="{ layoutSingle: '4' == '1' }" 6015 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 6016 6017 data-qb-selector="product-item" 6018 data-product-id="55" 6019 6020 data-pid="55" 6021 data-s-title="VEGDOG Vuxen Linser & Hirs 400g/800g" 6022 data-s-price="45.00" 6023 data-price="45.00" 6024 > 6025 6026 <!-- Image --> 6027 <a class="block relative w-full " href="/hundmat/vegdog-adult-400g"> 6028 <div class="relative group overflow-hidden rounded-tnone"> 6029 <img loading="lazy" 6030 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 6031 ix-src="https://cdn.quickbutik.com/images/2666L/products/656a138de4e49.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Vuxen Linser & Hirs 400g/800g"> 6032 6033 <img loading="lazy" 6034 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 6035 ix-src="https://cdn.quickbutik.com/images/2666L/products/656a138de15dc.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="VEGDOG Vuxen Linser & Hirs 400g/800g"> 6036 6037 </div> 6038 </a> 6039 6040 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 6041 <a class="block px-6 mb-4" href="/hundmat/vegdog-adult-400g"> 6042 6043 <!-- Title --> 6044 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 6045 VEGDOG Vuxen Linser & Hirs 400g/800g 6046 </h3> 6047 6048 <!-- Reviews container --> 6049 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 6050 6051 </a> 6052 6053 <!-- Description --> 6054 <template x-if="layoutSingle"> 6055 6056 </template> 6057 6058 <div> 6059 6060 <!-- Price --> 6061 <div class="px-6"> 6062 6063 <!-- Current price -->
6064 <span> 6065 <span class="text-xl font-bold text-accent">45 SEK</span> 6066 </span> 6067 6068 <!-- Original price --> 6069 6070 </div> 6071 6072 <!-- Add to basket --> 6073 <div class="flex justify-between gap-2 px-6 mt-2"> 6074 6075 <!-- Buy button --> 6076 6077 <!-- No options --> 6078 6079 <!-- Has options --> 6080 <a href="/hundmat/vegdog-adult-400g" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 6081 6082 <!-- Icon (arrow) --> 6083 6084 <!-- Text --> 6085 Läs mer 6086 6087 </a> 6088 6089 <!-- Sold out --> 6090 6091 6092 6093 6094 <!-- Link to product when no buy button --> 6095 6096 <!-- Wishlist --> 6097 <div data-qb-selector="product-item-wishlist" data-product-id="55" class="flex items-center"></div> 6098 6099 </div> 6100 6101 <!-- Stock status --> 6102 <div class="text-sm px-6 pt-4"> 6103 6104 <!-- Sold out --> 6105 6106 <!-- Not sold out --> 6107 <span data-in-stock-text="I lager" 6108 data-out-of-stock-text="" 6109 data-combination-not-exist-text="Variant finns ej"> 6110 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 6111 </span> 6112 6113 </div> 6114 6115 </div> 6116 </div> 6117 </div> 6118 6119 6120 6121 6122 6123 6124 6125 6126 6127 6128 6129 6130 6131 6132 <div x-data="{ layoutSingle: '4' == '1' }" 6133 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 6134 6135 data-qb-selector="product-item" 6136 data-product-id="54" 6137 6138 data-pid="54" 6139 data-s-title="V-dog Crunchy Nuggets 15kg" 6140 data-s-price="999" 6141 data-price="999" 6142 > 6143 6144 <!-- Image --> 6145 <a class="block relative w-full " href="/hundmat/v-dog-nuggets-15kg"> 6146 <div class="relative group overflow-hidden rounded-tnone"> 6147 <img loading="lazy" 6148 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 6149 ix-src="https://cdn.quickbutik.com/images/2666L/products/62c14897a76c9.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="V-dog Crunchy Nuggets 15kg"> 6150 6151 <img loading="lazy" 6152 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 6153 ix-src="https://cdn.quickbutik.com/images/2666L/products/59f4621c9d505.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="V-dog Crunchy Nuggets 15kg"> 6154 6155 </div> 6156 </a> 6157 6158 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 6159 <a class="block px-6 mb-4" href="/hundmat/v-dog-nuggets-15kg"> 6160 6161 <!-- Title --> 6162 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 6163 V-dog Crunchy Nuggets 15kg 6164 </h3> 6165 6166 <!-- Reviews container --> 6167 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 6168 6169 </a> 6170 6171 <!-- Description --> 6172 <template x-if="layoutSingle"> 6173 6174 </template> 6175 6176 <div> 6177 6178 <!-- Price --> 6179 <div class="px-6"> 6180 6181 <!-- Current price -->
6182 <span> 6183 <span class="text-xl font-bold text-accent">999 SEK</span> 6184 </span> 6185 6186 <!-- Original price --> 6187 6188 </div> 6189 6190 <!-- Add to basket --> 6191 <div class="flex justify-between gap-2 px-6 mt-2"> 6192 6193 <!-- Buy button --> 6194 6195 <!-- No options --> 6196 <button @click="$store.cart.add({ 6197 productID: 54, 6198 quantity: 1 6199 })" 6200 aria-label="Lägg i korgen" 6201 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 6202 6203 <!-- Icon (cart) --> 6204 6205 <!-- Text --> 6206 Lägg i korgen 6207 6208 </button> 6209 6210 <!-- Has options --> 6211 6212 <!-- Sold out --> 6213 6214 6215 6216 6217 <!-- Link to product when no buy button --> 6218 6219 <!-- Wishlist --> 6220 <div data-qb-selector="product-item-wishlist" data-product-id="54" class="flex items-center"></div> 6221 6222 </div> 6223 6224 <!-- Stock status --> 6225 <div class="text-sm px-6 pt-4"> 6226 6227 <!-- Sold out --> 6228 6229 <!-- Not sold out --> 6230 <span data-in-stock-text="I lager" 6231 data-out-of-stock-text="" 6232 data-combination-not-exist-text="Variant finns ej"> 6233 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 6234 </span> 6235 6236 </div> 6237 6238 </div> 6239 </div> 6240 </div> 6241 6242 6243 6244 6245 6246 6247 6248 6249 6250 6251 6252 6253 6254 6255 <div x-data="{ layoutSingle: '4' == '1' }" 6256 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 6257 6258 data-qb-selector="product-item" 6259 data-product-id="15" 6260 6261 data-pid="15" 6262 data-s-title="Benevo Grain-Free Vegetable Feast 395g" 6263 data-s-price="59" 6264 data-price="59" 6265 > 6266 6267 <!-- Image --> 6268 <a class="block relative w-full " href="/hundmat/benevo-grain-free-vegetable-feast"> 6269 <div class="relative group overflow-hidden rounded-tnone"> 6270 <img loading="lazy" 6271 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 6272 ix-src="https://cdn.quickbutik.com/images/2666L/products/69af30b7576d5.webp" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Benevo Grain-Free Vegetable Feast 395g"> 6273 6274 <img loading="lazy" 6275 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 6276 ix-src="https://cdn.quickbutik.com/images/2666L/products/66a649d473349.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Benevo Grain-Free Vegetable Feast 395g"> 6277 6278 </div> 6279 </a> 6280 6281 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 6282 <a class="block px-6 mb-4" href="/hundmat/benevo-grain-free-vegetable-feast"> 6283 6284 <!-- Title --> 6285 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 6286 Benevo Grain-Free Vegetable Feast 395g 6287 </h3> 6288 6289 <!-- Reviews container --> 6290 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 6291 6292 </a> 6293 6294 <!-- Description --> 6295 <template x-if="layoutSingle"> 6296 6297 </template> 6298 6299 <div> 6300 6301 <!-- Price --> 6302 <div class="px-6"> 6303 6304 <!-- Current price -->
6305 <span> 6306 <span class="text-xl font-bold text-accent">59 SEK</span> 6307 </span> 6308 6309 <!-- Original price --> 6310 6311 </div> 6312 6313 <!-- Add to basket --> 6314 <div class="flex justify-between gap-2 px-6 mt-2"> 6315 6316 <!-- Buy button --> 6317 6318 <!-- No options --> 6319 <button @click="$store.cart.add({ 6320 productID: 15, 6321 quantity: 1 6322 })" 6323 aria-label="Lägg i korgen" 6324 class="qs-cart-submit px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted transition duration-default font-bold text-sm button-rounding focus:outline-none"> 6325 6326 <!-- Icon (cart) --> 6327 6328 <!-- Text --> 6329 Lägg i korgen 6330 6331 </button> 6332 6333 <!-- Has options --> 6334 6335 <!-- Sold out --> 6336 6337 6338 6339 6340 <!-- Link to product when no buy button --> 6341 6342 <!-- Wishlist --> 6343 <div data-qb-selector="product-item-wishlist" data-product-id="15" class="flex items-center"></div> 6344 6345 </div> 6346 6347 <!-- Stock status --> 6348 <div class="text-sm px-6 pt-4"> 6349 6350 <!-- Sold out --> 6351 6352 <!-- Not sold out --> 6353 <span data-in-stock-text="I lager" 6354 data-out-of-stock-text="" 6355 data-combination-not-exist-text="Variant finns ej"> 6356 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 6357 </span> 6358 6359 </div> 6360 6361 </div> 6362 </div> 6363 </div> 6364 6365 6366 6367 6368 6369 6370 6371 6372 6373 6374 6375 6376 6377 6378 <div x-data="{ layoutSingle: '4' == '1' }" 6379 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 6380 6381 data-qb-selector="product-item" 6382 data-product-id="13" 6383 6384 data-pid="13" 6385 data-s-title="Benevo Duo 354g" 6386 data-s-price="45.00" 6387 data-price="45.00" 6388 > 6389 6390 <!-- Image --> 6391 <a class="block relative w-full " href="/hundmat/benevo-duo"> 6392 <div class="relative group overflow-hidden rounded-tnone"> 6393 <img loading="lazy" 6394 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 6395 ix-src="https://cdn.quickbutik.com/images/2666L/products/65f94db2d5ba3.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Benevo Duo 354g"> 6396 6397 <img loading="lazy" 6398 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 6399 ix-src="https://cdn.quickbutik.com/images/2666L/products/65f94db4a03a0.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Benevo Duo 354g"> 6400 6401 </div> 6402 </a> 6403 6404 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 6405 <a class="block px-6 mb-4" href="/hundmat/benevo-duo"> 6406 6407 <!-- Title --> 6408 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 6409 Benevo Duo 354g 6410 </h3> 6411 6412 <!-- Reviews container --> 6413 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 6414 6415 </a> 6416 6417 <!-- Description --> 6418 <template x-if="layoutSingle"> 6419 6420 </template> 6421 6422 <div> 6423 6424 <!-- Price --> 6425 <div class="px-6"> 6426 6427 <!-- Current price -->
6428 <span> 6429 <span class="text-xl font-bold text-accent">45 SEK</span> 6430 </span> 6431 6432 <!-- Original price --> 6433 6434 </div> 6435 6436 <!-- Add to basket --> 6437 <div class="flex justify-between gap-2 px-6 mt-2"> 6438 6439 <!-- Buy button --> 6440 6441 <!-- No options --> 6442 6443 <!-- Has options --> 6444 <a href="/hundmat/benevo-duo" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 6445 6446 <!-- Icon (arrow) --> 6447 6448 <!-- Text --> 6449 Läs mer 6450 6451 </a> 6452 6453 <!-- Sold out --> 6454 6455 6456 6457 6458 <!-- Link to product when no buy button --> 6459 6460 <!-- Wishlist --> 6461 <div data-qb-selector="product-item-wishlist" data-product-id="13" class="flex items-center"></div> 6462 6463 </div> 6464 6465 <!-- Stock status --> 6466 <div class="text-sm px-6 pt-4"> 6467 6468 <!-- Sold out --> 6469 6470 <!-- Not sold out --> 6471 <span data-in-stock-text="I lager" 6472 data-out-of-stock-text="" 6473 data-combination-not-exist-text="Variant finns ej"> 6474 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 6475 </span> 6476 6477 </div> 6478 6479 </div> 6480 </div> 6481 </div> 6482 6483 6484 6485 6486 6487 6488 6489 6490 6491 6492 6493 6494 6495 6496 <div x-data="{ layoutSingle: '4' == '1' }" 6497 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 6498 6499 data-qb-selector="product-item" 6500 data-product-id="12" 6501 6502 data-pid="12" 6503 data-s-title="Benevo Dog Original" 6504 data-s-price="199.00" 6505 data-price="199.00" 6506 > 6507 6508 <!-- Image --> 6509 <a class="block relative w-full " href="/hundmat/benevo-dog-original"> 6510 <div class="relative group overflow-hidden rounded-tnone"> 6511 <img loading="lazy" 6512 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 6513 ix-src="https://cdn.quickbutik.com/images/2666L/products/66018090c6104.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Benevo Dog Original"> 6514 6515 <img loading="lazy" 6516 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 6517 ix-src="https://cdn.quickbutik.com/images/2666L/products/66018092cc5a4.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="Benevo Dog Original"> 6518 6519 </div> 6520 </a> 6521 6522 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 6523 <a class="block px-6 mb-4" href="/hundmat/benevo-dog-original"> 6524 6525 <!-- Title --> 6526 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 6527 Benevo Dog Original 6528 </h3> 6529 6530 <!-- Reviews container --> 6531 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 6532 6533 </a> 6534 6535 <!-- Description --> 6536 <template x-if="layoutSingle"> 6537 6538 </template> 6539 6540 <div> 6541 6542 <!-- Price --> 6543 <div class="px-6"> 6544 6545 <!-- Current price -->
6546 <span> 6547 <span class="text-xl font-bold text-accent">199 SEK</span> 6548 </span> 6549 6550 <!-- Original price --> 6551 6552 </div> 6553 6554 <!-- Add to basket --> 6555 <div class="flex justify-between gap-2 px-6 mt-2"> 6556 6557 <!-- Buy button --> 6558 6559 <!-- No options --> 6560 6561 <!-- Has options --> 6562 <a href="/hundmat/benevo-dog-original" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 6563 6564 <!-- Icon (arrow) --> 6565 6566 <!-- Text --> 6567 Läs mer 6568 6569 </a> 6570 6571 <!-- Sold out --> 6572 6573 6574 6575 6576 <!-- Link to product when no buy button --> 6577 6578 <!-- Wishlist --> 6579 <div data-qb-selector="product-item-wishlist" data-product-id="12" class="flex items-center"></div> 6580 6581 </div> 6582 6583 <!-- Stock status --> 6584 <div class="text-sm px-6 pt-4"> 6585 6586 <!-- Sold out --> 6587 6588 <!-- Not sold out --> 6589 <span data-in-stock-text="I lager" 6590 data-out-of-stock-text="" 6591 data-combination-not-exist-text="Variant finns ej"> 6592 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 6593 </span> 6594 6595 </div> 6596 6597 </div> 6598 </div> 6599 </div> 6600 6601 6602 6603 6604 6605 6606 6607 6608 6609 6610 6611 6612 6613 6614 <div x-data="{ layoutSingle: '4' == '1' }" 6615 class="product flex items-center h-full flex-1 relative bg-card shadow roundednone flex-col overflow-hidden" 6616 6617 data-qb-selector="product-item" 6618 data-product-id="5" 6619 6620 data-pid="5" 6621 data-s-title="AMI DOG Medium & Large 3kg/12kg" 6622 data-s-price="349.00" 6623 data-price="349.00" 6624 > 6625 6626 <!-- Image --> 6627 <a class="block relative w-full " href="/hundmat/ami-dog"> 6628 <div class="relative group overflow-hidden rounded-tnone"> 6629 <img loading="lazy" 6630 class="w-full object-contain object-center aspect-1/1 group-hover:opacity-0 transition duration-default" 6631 ix-src="https://cdn.quickbutik.com/images/2666L/products/66b0638482069.png" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Medium & Large 3kg/12kg"> 6632 6633 <img loading="lazy" 6634 class="w-full absolute top-0 left-0 object-contain object-center aspect-1/1 opacity-0 group-hover:opacity-100 transition duration-default" 6635 ix-src="https://cdn.quickbutik.com/images/2666L/products/67d938b2248ce.jpeg" ix-params='{"auto":"format,compress"}' ix-sizes="auto" alt="AMI DOG Medium & Large 3kg/12kg"> 6636 6637 </div> 6638 </a> 6639 6640 <div class="flex flex-1 flex-col justify-between md:h-full w-full py-6" :class="layoutSingle && 'md:w-3/4'"> 6641 <a class="block px-6 mb-4" href="/hundmat/ami-dog"> 6642 6643 <!-- Title --> 6644 <h3 class="text-base sm:text-xl font-heading leading-6 font-bold"> 6645 AMI DOG Medium & Large 3kg/12kg 6646 </h3> 6647 6648 <!-- Reviews container --> 6649 <div class="d-flex mt-2" data-qb-selector="product-apps-reviews-item-reviews"></div> 6650 6651 </a> 6652 6653 <!-- Description --> 6654 <template x-if="layoutSingle"> 6655 6656 </template> 6657 6658 <div> 6659 6660 <!-- Price --> 6661 <div class="px-6"> 6662 6663 <!-- Current price -->
6664 <span> 6665 <span class="text-xl font-bold text-accent">349 SEK</span> 6666 </span> 6667 6668 <!-- Original price --> 6669 6670 </div> 6671 6672 <!-- Add to basket --> 6673 <div class="flex justify-between gap-2 px-6 mt-2"> 6674 6675 <!-- Buy button --> 6676 6677 <!-- No options --> 6678 6679 <!-- Has options --> 6680 <a href="/hundmat/ami-dog" area-label="Läs mer" class="block px-2 w-full button-py bg-foreground border border-foreground hover:opacity-80 text-inverted text-center transition duration-default font-bold text-sm button-rounding focus:outline-none"> 6681 6682 <!-- Icon (arrow) --> 6683 6684 <!-- Text --> 6685 Läs mer 6686 6687 </a> 6688 6689 <!-- Sold out --> 6690 6691 6692 6693 6694 <!-- Link to product when no buy button --> 6695 6696 <!-- Wishlist --> 6697 <div data-qb-selector="product-item-wishlist" data-product-id="5" class="flex items-center"></div> 6698 6699 </div> 6700 6701 <!-- Stock status --> 6702 <div class="text-sm px-6 pt-4"> 6703 6704 <!-- Sold out --> 6705 6706 <!-- Not sold out --> 6707 <span data-in-stock-text="I lager" 6708 data-out-of-stock-text="" 6709 data-combination-not-exist-text="Variant finns ej"> 6710 <span class="inline-block bg-green-600 rounded-full h-2 w-2 mr-1"></span> I lager 6711 </span> 6712 6713 </div> 6714 6715 </div> 6716 </div> 6717 </div> 6718 6719 6720 </div> 6721 </div> 6722 </div> 6723 6724 <!-- Pagination --> 6725 6726 <!-- Description (2) --> 6727 6728 </div> 6729 </section> 6730 <section data-qb-area="footer" class="bg-footer text-center lg:text-left"> 6731 <div class="container px-4 py-8 md:py-12 mx-auto "> 6732 <div class="flex flex-wrap justify-between"> 6733 <div class="w-full lg:w-1/3 mb-10 lg:mb-0 px-4"> 6734 6735 <!-- Logotype --> 6736 <a data-qb-setting="use_footer_logo" class="inline-block mb-6 w-full h-8" href="/"> 6737 <img src="https://cdn.quickbutik.com/images/2666L/templates/lua/assets/logo.png?s=96971&auto=format&fit=clip&h=160&w=600" alt="Vecanis" class="h-8 w-full max-w-xs mx-auto lg:mx-0 object-contain object-center lg:object-left" > 6738 </a> 6739 6740 <!-- Text below logotype --> 6741 6742 <!-- Social media link --> 6743 <div data-qb-setting="show_footer_socialicons" class="text-footer opacity-70 mb-3"> 6744 6745 <a class="inline-block mr-4 hover:opacity-70" aria-label="Facebook" href="https://www.facebook.com/vecanis.se/" target="_blank"> 6746 <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24"> 6747 <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> 6748 </svg> 6749 </a> 6750 <a 6751 class="inline-block mr-4 hover:opacity-70" 6752 aria-label="Instagram" 6753 href="https://instagram.com/vecanis/" target="_blank"> 6754 <svg 6755 xmlns="http://www.w3.org/2000/svg" 6756 class="w-4 h-4"
6757 viewBox="0 0 24 24" 6758 > 6759 <path 6760 fill="currentColor" 6761 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" 6762 ></path> 6763 </svg> 6764 </a> 6765 </div> 6766 </div> 6767 6768 <div class="w-full lg:w-1/3 lg:pl-24 px-4 mb-10 lg:mb-0"> 6769 6770 <!-- Footer menu --> 6771 <h5 data-qb-setting="footer_menu_heading" class="text-lg font-bold mb-5 text-footer"> 6772 Läs mer 6773 </h5> 6774 6775 <ul data-qb-setting="use_footer_menu" class="opacity-70"> 6776 <li class="mb-2"> 6777 <a class="inline-block text-footer hover:opacity-70" href="/contact">Kontakt</a> 6778 </li> 6779 <li class="mb-2"> 6780 <a class="inline-block text-footer hover:opacity-70" href="/sidor/terms-and-conditions">Köpvillkor</a> 6781 </li> 6782 <li class="mb-2"> 6783 <a class="inline-block text-footer hover:opacity-70" href="/sidor/faq">Vanliga frågor</a> 6784 </li> 6785 <li class="mb-2"> 6786 <a class="inline-block text-footer hover:opacity-70" href="/sidor/cookies">Cookies</a> 6787 </li> 6788 <li class="mb-2"> 6789 <a class="inline-block text-footer hover:opacity-70" href="/sidor/gdpr">GDPR</a> 6790 </li> 6791 </ul> 6792 </div> 6793 6794 <!-- Newsletter signup --> 6795 <div class="w-full lg:w-1/3 px-4"> 6796 <h5 data-qb-setting="footer_newsletter_header" class="text-lg font-bold mb-5 text-footer">Sugen på rabatter och uppdateringar? Prenumerera på vårt nyhetsbrev</h5> 6797 6798 <form id="footer_newsletter_form" action="/shop/newsletter" 6799 data-txt-failed='Ange din e-postadress' 6800 data-txt-error='Ett fel uppstod, försök igen senare' 6801 data-txt-success='Du har nu anmält dig till vårt nyhetsbrev!'> 6802 6803 <div class="flex items-center pl-3 pr-2 py-2 bg-white button-rounding"> 6804 <input 6805 data-qb-setting="footer_newsletter_placeholder_text" 6806 class="w-full px-2 inline-block bg-transparent border-none text-base outline-none" 6807 type="email" name="email" placeholder="Ange e-postadress" 6808 aria-label="Ange e-postadress"> 6809 <button data-qb-setting="footer_newsletter_button_text" type="submit" class="ml-auto inline-block px-3 py-2 bg-black hover:opacity-90 text-white font-bold transition duration-default button-rounding" href="#">Prenumerera</button> 6810 </div> 6811 </form> 6812 <p id="footer_newsletter_status" class="text-sm text-footer" role="status" aria-live="polite"></p> 6813 6814
6814<script> 6815 function showNewsletterStatus(text) { 6816 const line = document.createElement('span') 6817 line.className = 'block mt-4' 6818 line.textContent = text 6819 newsletterStatus.replaceChildren(line) 6820 } 6821 6822 function logSubmit(event) { 6823 event.preventDefault() 6824 newsletterStatus.replaceChildren() 6825 6826 fetch(event.target.action, { 6827 method: 'POST', 6828 body: new FormData(event.target), 6829 }) 6830 .then(function(response) { 6831 if (response.ok) { 6832 return response.json() 6833 } 6834 return Promise.reject(response) 6835 }) 6836 .then(function(data) { 6837 if (data.success) { 6838 form.querySelector('input[name="email"]').value = '' 6839 6840 showNewsletterStatus(event.target.dataset.txtSuccess); 6841 } else if (data.error) { 6842 showNewsletterStatus(event.target.dataset.txtFailed); 6843 } else { 6844 showNewsletterStatus(event.target.dataset.txtError); 6845 } 6846 }).catch(function (error) { 6847 showNewsletterStatus(event.target.dataset.txtError); 6848 }); 6849 } 6850 6851 const form = document.getElementById("footer_newsletter_form"); 6852 const newsletterStatus = document.getElementById("footer_newsletter_status"); 6853 form.addEventListener("submit", logSubmit); 6854 </script>
6854 6855 6856 </div> 6857 6858 </div> 6859 6860 <div> 6861 <!-- Payment methods --> 6862 <div data-qb-setting="use_footer_paymentmethods" class="flex justify-center items-center gap-default flex-wrap mt-10"> 6863 6864 6865 6866 <img loading="lazy" class="h-10 p-2 w-auto" src="/assets/shopassets/paylogos/payment-visa-blue.svg" alt="Payment with Visa"> 6867 6868 6869 <img loading="lazy" class="h-10 p-2 w-auto" src="/assets/shopassets/paylogos/payment-mastercard.svg" alt="Payment with Mastercard"> 6870 6871 <img loading="lazy" class="h-10 p-2 w-auto" src="/assets/shopassets/paylogos/payment-klarna.svg" alt="Payment with Klarna"> 6872 6873 6874 6875 6876 6877 6878 <img loading="lazy" class="h-10 p-2 w-auto" src="/assets/shopassets/paylogos/payment-paypal-color.svg" alt="Payment with Paypal"> 6879 6880 6881 6882 <img loading="lazy" class="h-10 p-2 w-auto" src="/assets/shopassets/paylogos/payment-swish.png" alt="Payment with Swish"> 6883 6884 </div> 6885 </div> 6886 </div> 6887 6888 <div> 6889 <hr class="border-foreground opacity-40"> 6890 <div class="container px-4 mx-auto"> 6891 <div class="py-8 text-center"> 6892 <p class="text-footer text-sm opacity-50"> 6893 <!-- Copyright --> 6894 © 2026 Vecanis 6895 <span data-qb-setting="use_footer_poweredby">– <a style="text-decoration: none;" target="_blank" rel="nofollow sponsored" title="Powered by Quickbutik" href="https://quickbutik.com">Powered by Quickbutik</a></span> 6896 </p> 6897 </div> 6898 </div> 6899 </div> 6900 </section> 6901 <div x-data x-cloak 6902 x-show="$store.cart.cartOpen" 6903 @keydown.escape="$store.cart.hideCart()" 6904 @pageshow.window="$store.cart.init()" 6905 class="navbar-menu fixed top-0 right-0 bottom-0 w-full sm:max-w-sm z-50"> 6906 6907 <!-- Overlay --> 6908 <div 6909 x-show='$store.cart.cartOpen' 6910 @click='$store.cart.hideCart()' 6911 x-transition:enter='transition ease-out duration-default transform' 6912 x-transition:enter-start='opacity-0' 6913 x-transition:enter-end='opacity-100' 6914 x-transition:leave='transition ease-in duration-default transform' 6915 x-transition:leave-start='opacity-100' 6916 x-transition:leave-end='opacity-0' 6917 class='fixed inset-0 transition-opacity bg-overlay bg-opacity-40' 6918 aria-hidden='true' 6919 ></div> 6920 6921 <!-- Cart --> 6922 <div 6923 x-show='$store.cart.cartOpen' 6924 @keydown.escape="$store.cart.hideCart()" 6925 class="relative flex flex-col h-full bg-body" 6926 x-transition:enter='transition ease-out duration-default transform' 6927 x-transition:enter-start='opacity-0 translate-x-10 sm:translate-x-10' 6928 x-transition:enter-end='opacity-100 translate-x-0' 6929 x-transition:leave='transition ease-in duration-default transform' 6930 x-transition:leave-start='opacity-100 translate-x-0' 6931 x-transition:leave-end='opacity-0 translate-x-10 sm:translate-x-10' 6932 > 6933 <header class="flex items-center justify-between p-6 mb-4"> 6934 6935 <!-- Title --> 6936 <span class="items-center font-heading text-3xl"> 6937 Varukorg 6938 </span> 6939 6940 <div class="flex"> 6941 6942 <!-- Close button --> 6943 <button 6944 @click="$store.cart.hideCart()" 6945 class="inline-block cart-close cursor-pointer focus:outline-none hover:opacity-50" 6946 type="button" 6947 aria-label="Close" 6948 > 6949 <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"> 6950 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path> 6951 </svg> 6952 </button> 6953 6954 </div> 6955 6956 </header> 6957 6958 <!-- Cart contents --> 6959 <div class="h-full p-6 overflow-y-auto"> 6960 6961 <!-- Cart messages --> 6962 <div class="qs-cart-message"></div> 6963 6964 <!-- Empty cart snippet--> 6965 <!-- In case of empty cart - display this snippet + remove .d-none--> 6966 <div class="hidden qs-cart-empty flex items-center h-full opacity-50 text-center"> 6967 <div> 6968 <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"> 6969 <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.5
6969h12.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> 6970 </svg> 6971 <p> 6972 Varukorgen är tom 6973 </p> 6974 </div> 6975 </div> 6976 6977 <!-- Cart items --> 6978 <template 6979 x-for="product in $store.cart.products" 6980 :key="`${product.item.id}-${product.qty}-${product.uid}`" 6981 > 6982 <div class="mb-6"> 6983 6984 <div class="flex w-full"> 6985 6986 <!-- Product image --> 6987 <div class="flex justify-center items-center pr-4 w-24"> 6988 <a :href="product.item.url"> 6989 <img loading="lazy" :src="product.item.firstimage + '?w=100&h=100'" class="object-contain object-center w-full h-full roundednone" :alt="product.item.title"> 6990 </a> 6991 </div> 6992 <div class="flex flex-1 flex-col"> 6993 6994 <!-- Product title --> 6995 <a :href="product.item.url"> 6996 <h4 class="font-body font-bold" x-text="product.item.title"></h4> 6997 </a> 6998 6999 <!-- Product variant --> 7000 <template x-if="product.variant_name"> 7001 <small x-text="product.variant_name" class="opacity-50"></small> 7002 </template> 7003 7004 <!-- Product propertiers --> 7005 <template x-if="product.properties"> 7006 <template x-for="property in Object.values(product.properties)"> 7007 <small class="break-words" x-text="property"></small> 7008 </template> 7009 </template> 7010 7011 <!-- Price --> 7012 <!-- Current price --> 7013 <span class="text-sm"> 7014 <span 7015 :class="product.item.has_discount ? 'text-discounted-price' : ''" 7016 x-text="product.item.price" 7017 ></span> 7018 7019 <!-- Original price (only show if has discount) --> 7020 <template x-if="product.item.has_discount"> 7021 <span 7022 class="opacity-50 line-through ml-1" 7023 x-text="product.item.original_price" 7024 ></span> 7025 </template> 7026 </span> 7027 7028 <div 7029 class="flex-1 flex items-center justify-between mt-2" 7030 x-data="{ 7031 quantity: product.qty, 7032 }" 7033 x-init="$watch('quantity', Alpine.debounce((value) => { 7034 if (value && value > 0) { 7035 $store.cart.update({ 7036 productID: product.item.id, 7037 quantity: value 7038 }) 7039 } 7040 }, 500))" 7041 > 7042 <div class="flex border border-gray-200 input-rounding overflow-hidden"> 7043 7044 <!-- Decrease quantity --> 7045 <button 7046 type="button" 7047 class="w-8 font-bold cursor-pointer input-bg focus:outline-none hover:bg-gray-100 py-1 px-2" 7048 @click="quantity = Math.max(1, quantity - 1)" 7049 > 7050 - 7051 </button> 7052 7053 <!-- Quantity input --> 7054 <input 7055 required 7056 min="1" 7057 step="1" 7058 type="number" 7059 class="w-12 qs-cart-qty navbar-cart-product-qty input-bg border-0 input-items text-center focus:outline-none" 7060 x-model="quantity"
7061 @input="quantity = Math.floor(Number(quantity))" 7062 > 7063 7064 <!-- Increase quantity --> 7065 <button 7066 type="button" 7067 class="w-8 font-bold cursor-pointer input-bg focus:outline-none hover:bg-gray-100 py-1 px-2" 7068 @click="quantity = Number(quantity) + 1" 7069 > 7070 + 7071 </button> 7072 </div> 7073 7074 <!-- Remove from cart --> 7075 <button 7076 type="button" 7077 class="font-medium text-baseThird qs-cart-delete btn hover:text-red-700" 7078 data-pid="" 7079 @click="$store.cart.delete({ 7080 productID: product.item.id, 7081 })" 7082 > 7083 <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"> 7084 <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" /> 7085 </svg> 7086 </button> 7087 </div> 7088 7089 </div> 7090 </div> 7091 7092 </div> 7093 </template> 7094 </div> 7095 7096 <!-- Footer --> 7097 7098 <!-- Tax --> 7099 7100 <footer class="mt-auto p-8 bg-gray-400 bg-opacity-5"> 7101 7102 <!-- Total discount (only show if there's a discount) --> 7103 <div class="flex items-end mb-2" x-show="$store.cart.totalDiscountRaw && $store.cart.totalDiscountRaw > 0"> 7104 <div class="flex-1"> 7105 <span class="text-sm font-medium text-discounted-price">Rabatt</span> 7106 </div> 7107 <div class="flex-1 text-right"> 7108 <span 7109 class="text-sm font-medium text-discounted-price" 7110 x-text="'-' + $store.cart.totalDiscount" 7111 ></span> 7112 </div> 7113 </div> 7114 7115 <div class="flex mb-4"> 7116 <div class="flex-1"> 7117 <span class="text-lg font-bold">Total summa:</span> 7118 </div> 7119 <div class="flex-1 text-right"> 7120 <span 7121 class="text-lg font-bold text-primary" 7122 x-text="$store.cart.totalAmount" 7123 ></span> 7124 </div> 7125 </div> 7126 <div> 7127 <button @click='$store.cart.hideCart()' class="block w-full button-px button-py mb-2 text-center text-primary font-bold hover:text-body button-rounding focus:outline-none"> 7128 Fortsätt att handla 7129 </button> 7130 <a href="/cart/index" class="block button-px button-py text-center text-white font-bold bg-primary hover:bg-gray-900 button-rounding focus:outline-none"> 7131 Till kassan 7132 </a> 7133 </div> 7134 </footer> 7135 7136 </div> 7137 </div> 7138 <section 7139 x-data 7140 x-cloak 7141 x-show="$store.modal.modalVisible" 7142 x-transition.opacity 7143 class="fixed inset-0 py-24 bg-black bg-opacity-50 z-50 h-full overflow-y-scroll"> 7144 <div class="container px-4 mx-auto relative"> 7145 <div x-on:click.outside="$store.modal.closeModal()" :style="{'background-color': $store.modal.backgroundColor || '#fff'}" 7146 class="relative max-w-xl mx-auto text-center py-12 md:py-16 px-6 md:px-14 roundednone"> 7147 <a class="absolute top-4 right-4 inline-block ml-auto cursor-pointer hover:opacity-50" x-on:click="$store.modal.closeModal()" href="#" rel="nofollow"> 7148 <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"> 7149 <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path> 7150 </svg> 7151 </a> 7152 <div x-html="$store.modal.content"></div> 7153 </div> 7154 </div> 7155 </section>
7156<script type="text/javascript"> var recaptchaSiteKey="6Lc-2GcUAAAAAMXG8Lz5S_MpVshTUeESW6iJqK0_"; </script>
7156<script type="text/javascript">window.registerApp({ 7157 name: 'Store Notifications', 7158 identifier: 'QuickbutikStoreNotifications', 7159 description: 'Show store notifications', 7160 version: '1.0.1', 7161 pages: ['list', 'start', 'product'], 7162 data: {}, 7163 config: {}, 7164 onMount: function ({ appData, page }) { 7165 console.debug(`${this.identifier}@onMount`) 7166 7167 const content = appData?.storenotices?.text 7168 const nid = appData?.storenotices?.nid 7169 7170 if (content) { 7171 Alpine.store('notifications').addNotification({ 7172 type: 'html', 7173 htmlContent: content, 7174 lifeTime: appData.storenotices.timer_seconds === "0" || appData.storenotices.timer_seconds === 0 7175 ? null 7176 : appData.storenotices.timer_seconds 7177 ? appData.storenotices.timer_seconds * 1000 7178 : 5000, 7179 backgroundColor: appData.storenotices.bg_color, 7180 textColor: appData.storenotices.text_color, 7181 onRemove: () => { 7182 if (nid) { 7183 const formData = new FormData() 7184 formData.append('nid', nid) 7185 7186 fetch('/apps/storenotices/shown', { 7187 method: 'POST', 7188 body: formData, 7189 keepalive: true, 7190 }) 7191 } 7192 }, 7193 }) 7194 } 7195 }, 7196}) 7197</script>
7197<script type="text/javascript">window.registerApp({ 7198 name: 'Popups', 7199 identifier: 'QuickbutikPopups', 7200 description: 'Display popups when page loads', 7201 version: '2.0.0', 7202 pages: ['list', 'start', 'product'], 7203 data: {}, 7204 config: { 7205 style: { 7206 popup: { 7207 containerClasses: 'qb-popup-container', 7208 titleClasses: 'qb-popup-title', 7209 textClasses: 'qb-popup-text', 7210 buttonClasses: 'qb-popup-button', 7211 inputClasses: 'qb-popup-input', 7212 imageClasses: 'qb-popup-image', 7213 }, 7214 }, 7215 renderPopup: null, 7216 renderSecondPage: null, 7217 }, 7218 _cssInjected: false, 7219 _minimizeTab: null, 7220 _goalCompleted: false, 7221 onMount: function ({ appData, page }) { 7222 if (new URLSearchParams(window.location.search).has('_langtags_scan')) return 7223 7224 this._injectStyles() 7225 this._attachSubmitHandler() 7226 7227 appData.storepopups = appData.storepopups 7228 .sort((a, b) => a.on_exit - b.on_exit) 7229 .filter((p) => !sessionStorage.getItem('qb_popup_shown_' + p.ppid)) 7230 7231 if (appData?.storepopups?.length) { 7232 this.data.popup = appData.storepopups[0] 7233 const { on_exit } = this?.data?.popup ?? {} 7234 7235 var ppid = this.data.popup.ppid 7236 var popupData = this._buildPopupData(this.data.popup) 7237 7238 if (popupData.minimizeOnClose && sessionStorage.getItem('qb_popup_minimized_' + ppid)) { 7239 this._showMinimizeTab(popupData, ppid) 7240 return 7241 } 7242 7243 const openModalIfNeeded = () => { 7244 if (on_exit === true && !this.mouseLeftDocument) { 7245 return 7246 } 7247 7248 if (this.data.popup) { 7249 this._openPopup(false, popupData) 7250 } 7251 } 7252 7253 if (on_exit === true) { 7254 this.attachMouseLeaveListener(openModalIfNeeded) 7255 } else { 7256 openModalIfNeeded() 7257 } 7258 } 7259 }, 7260 _openPopup: function (skipDelay, cachedPopupData) { 7261 var popup = this.data.popup 7262 if (!popup) return 7263 7264 var delay_seconds = popup.delay_seconds 7265 var p_bg_color = popup.p_bg_color 7266 var ppid = popup.ppid 7267 7268 var popupData = cachedPopupData || this._buildPopupData(popup) 7269 7270 Alpine.store('modal').openPopup( 7271 popupData, 7272 (data, state) => this._render(data, state), 7273 { 7274 delay: skipDelay ? 0 : parseInt(delay_seconds ?? 0) * 1000, 7275 backgroundColor: p_bg_color, 7276 position: popupData.position, 7277 modalSize: popupData.modalSize, 7278 imagePosition: popupData.imagePosition, 7279 appearEffect: popupData.appearEffect, 7280 image: popupData.image, 7281 onClose: () => { 7282 if (popupData.minimizeOnClose && !this._goalCompleted) { 7283 this._showMinimizeTab(popupData, ppid) 7284 } else { 7285 this._goalCompleted = false 7286 sessionStorage.removeItem('qb_popup_minimized_' + ppid) 7287 this._markShown(ppid) 7288 this.data.popup = null 7289 } 7290 }, 7291 } 7292 ) 7293 }, 7294 _markShown: function (ppid) { 7295 var formData = new FormData() 7296 formData.append('ppid', ppid) 7297 fetch('/apps/storepopups/shown', { method: 'POST', body: formData }) 7298 }, 7299 _showMinimizeTab: function (popupData, ppid) { 7300 this._hideMinimizeTab() 7301 sessionStorage.setItem('qb_popup_minimized_' + ppid, '1') 7302 7303 var tab = document.createElement('button') 7304 tab.className = 'qb-popup-minimize-tab' 7305 tab.setAttribute('data-qb-minimize-tab', '') 7306 tab.style.backgroundColor = popupData.bgColor || '#fff' 7307 tab.style.color = popupData.textColor || '#000' 7308 7309 var chevron = document.createElement('span') 7310 chevron.className = 'qb-popup-minimize-tab__chevron' 7311 chevron.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="2 9 7 4 12 9"/></svg>' 7312 7313 var textSpan = document.createElement('span') 7314 textSpan.textContent = popupData.minimizeText || popupData.title || '' 7315 7316 var closeBtn = document.createElement('span') 7317 closeBtn.className = 'qb-popup-minimize-tab__close' 7318 closeBtn.setAttribute('role', 'button') 7319 closeBtn.setAttribute('aria-label', 'Close') 7320 closeBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="2" y1="2" x2="12" y2="12"/><line x1="12" y1="2" x2="2" y2="12"/></svg>' 7321 closeBtn.addEventListener('click', function (e) { 7322 e.stopPropagation() 7323 this._dismissPopup(ppid) 7324 }.bind(this)) 7325 7326 tab.appendChild(chevron) 7327 tab.appendChild(textSpan) 7328 tab.appendChild(closeBtn) 7329 7330 tab.addEventListener('click', function () { 7331 this._reopenPopup() 7332 }.bind(this)) 7333 7334 document.body.appendChild(tab) 7335 this._minimizeTab = tab 7336 }, 7337 _hideMinimizeTab: function () { 7338 if (this._minimizeTab) { 7339 this._minimizeTab.remove() 7340 this._minimizeTab = null 7341 } 7342 }, 7343 _reopenPopup: function () { 7344 this._hideMinimizeTab() 7345 this._openPopup(true) 7346 }, 7347 _dismissPopup: function (ppid) { 7348 var tab = this._minimizeTab 7349 this._minimizeTab = null 7350 if (tab) { 7351 tab.classList.add('qb-popup-minimize-tab--exit') 7352 setTimeout(function () { tab.remove() }, 200) 7353 } 7354 7355 sessionStorage.removeItem('qb_popup_minimized_' + ppid) 7356 this._goalCompleted = false 7357 this._markShown(ppid) 7358 this.data.popup = null 7359 }, 7360 attachMouseLeaveListener: function (callback) { 7361 document.addEventListener('mouseleave', () => { 7362 this.mouseLeftDocument = true 7363 callback() 7364 }) 7365 }, 7366 _buildPopupData: function (popup) { 7367 return { 7368 title: popup.p_title, 7369 text: popup.p_text, 7370 textColor: popup.p_text_color, 7371 bgColor: popup.p_bg_color, 7372 btnText: popup.p_btn_text, 7373 btnBgColor: popup.p_btn_bg_color, 7374 btnTextColor: popup.p_btn_text_color, 7375 hasGoal: popup.has_goal, 7376 goalType: popup.goal_type, 7377 textGoal: popup.p_text_goal, 7378 image: popup.p_image || null, 7379 position: popup.position || 'center', 7380 modalSize: popup.modal_size || 'medium', 7381 imagePosition: popup.image_position || 'top', 7382 imageWidth: popup.image_width || 50,
7383 textAlign: popup.text_align || 'center', 7384 showLogo: popup.show_logo == 1, 7385 logoUrl: popup.p_logo_url || null, 7386 appearEffect: popup.appear_effect || 'fade-up', 7387 showImageOnSuccess: popup.show_image_on_success != 0 && popup.show_image_on_success !== false, 7388 minimizeOnClose: popup.minimize_on_close == true || popup.minimize_on_close == 1, 7389 minimizeText: popup.minimize_text || null, 7390 finePrint: popup.fine_print || null, 7391 } 7392 }, 7393 _render: function (popupData, state) { 7394 if (state === 'success') { 7395 if (this.config.renderSecondPage) { 7396 return this.config.renderSecondPage(popupData) 7397 } 7398 return this._renderSuccess(popupData) 7399 } 7400 7401 if (this.config.renderPopup) { 7402 return this.config.renderPopup(popupData) 7403 } 7404 return this._renderPopup(popupData) 7405 }, 7406 _renderPopup: function (popupData) { 7407 const { 7408 title, 7409 text, 7410 textColor, 7411 btnText, 7412 btnBgColor, 7413 btnTextColor, 7414 hasGoal, 7415 goalType, 7416 image, 7417 imagePosition, 7418 imageWidth, 7419 } = popupData 7420 7421 const cls = this.config.style.popup 7422 const isHorizontal = image && (imagePosition === 'left' || imagePosition === 'right') 7423 const isTopImage = image && imagePosition === 'top' 7424 const isTakeover = image && imagePosition === 'background' 7425 const isCover = image && imagePosition === 'cover' 7426 const align = popupData.textAlign || 'center' 7427 const imgW = imageWidth || 40 7428 7429 let html = `<div class="${cls.containerClasses}${isHorizontal ? ' qb-popup-horizontal' : ''}${isTopImage ? ' qb-popup-has-top-image' : ''}${isTakeover ? ' qb-popup-takeover' : ''}${isCover ? ' qb-popup-cover' : ''}" data-qb-popup data-qb-image-position="${imagePosition || 'top'}" style="color: ${textColor || '#000'}; text-align: ${align}${isCover ? '; background-image: url("' + image + '"); background-size: cover; background-position: center' : ''}">` 7430 7431 if (isTopImage) { 7432 html += `<img class="${cls.imageClasses} qb-popup-image--top" data-qb-popup-part="image" src="${image}" alt="" style="max-height: ${imgW * 0.4}rem" />` 7433 } 7434 7435 if (image && imagePosition === 'left') { 7436 html += `<img class="${cls.imageClasses} qb-popup-image--side" data-qb-popup-part="image" src="${image}" alt="" style="width: ${imageWidth}%" />` 7437 } 7438 7439 html += `<div class="qb-popup-body">` 7440 7441 if (popupData.showLogo && popupData.logoUrl) { 7442 const logoAlign = align === 'left' ? 'flex-start' : align === 'right' ? 'flex-end' : 'center' 7443 html += `<img class="qb-popup-logo" data-qb-popup-part="logo" src="${popupData.logoUrl}" alt="" style="align-self: ${logoAlign}" />` 7444 } 7445 7446 html += `<h2 class="${cls.titleClasses}" data-qb-popup-part="title">${this._escapeHtml(title || '')}</h2>` 7447 html += `<div class="${cls.textClasses}" data-qb-popup-part="text">${text || ''}</div>` 7448 7449 if (hasGoal) { 7450 var formAlign = align === 'left' ? 'flex-start' : align === 'right' ? 'flex-end' : 'center' 7451 html += `<form class="qb-popup-form" data-qb-popup-part="form" style="align-items: ${formAlign}">` 7452 7453 if (goalType === '1') { 7454 html += `<input class="${cls.inputClasses}" data-qb-popup-part="input" type="email" id="popupNewsletterEmail" placeholder="${this.config?.modal?.inputPlaceholder ?? 'Email'}" required />` 7455 } 7456 7457 html += `<button class="${cls.buttonClasses}" data-qb-popup-part="button" type="submit" style="background-color: ${btnBgColor || '#000'}; color: ${btnTextColor || '#fff'}">${this._escapeHtml(btnText || '')}</button>` 7458 html += `</form>` 7459 } 7460 7461 if (popupData.finePrint) { 7462 html += `<div class="qb-popup-fine-print" data-qb-popup-part="fine-print">${this._escapeHtml(popupData.finePrint)}</div>` 7463 } 7464 7465 html += `</div>` 7466 7467 if (image && imagePosition === 'right') { 7468 html += `<img class="${cls.imageClasses} qb-popup-image--side" data-qb-popup-part="image" src="${image}" alt="" style="width: ${imageWidth}%" />` 7469 } 7470 7471 html += `</div>` 7472 7473 return html 7474 }, 7475 _renderSuccess: function (popupData) { 7476 const { title, textGoal, textColor, image, imagePosition, imageWidth, showImageOnSuccess } = popupData 7477 const cls = this.config.style.popup 7478 const showImage = image && showImageOnSuccess 7479 const isHorizontal = showImage && (imagePosition === 'left' || imagePosition === 'right') 7480 const isTopImage = showImage && imagePosition === 'top' 7481 const isTakeover = showImage && imagePosition === 'background' 7482 const isCover = showImage && imagePosition === 'cover' 7483 const align = popupData.textAlign || 'center' 7484 const imgW = imageWidth || 40 7485 7486 let html = `<div class="${cls.containerClasses} qb-popup-success${isHorizontal ? ' qb-popup-horizontal' : ''}${isTopImage ? ' qb-popup-has-top-image' : ''}${isTakeover ? ' qb-popup-takeover' : ''}${isCover ? ' qb-popup-cover' : ''}" data-qb-popup data-qb-popup-state="success" data-qb-image-position="${imagePosition || 'top'}" style="color: ${textColor || '#000'}; text-align: ${align}${isCover ? ';
7486 background-image: url("' + image + '"); background-size: cover; background-position: center' : ''}">` 7487 7488 if (isTopImage) { 7489 html += `<img class="${cls.imageClasses} qb-popup-image--top" data-qb-popup-part="image" src="${image}" alt="" style="max-height: ${imgW * 0.4}rem" />` 7490 } 7491 7492 if (showImage && imagePosition === 'left') { 7493 html += `<img class="${cls.imageClasses} qb-popup-image--side" data-qb-popup-part="image" src="${image}" alt="" style="width: ${imageWidth}%" />` 7494 } 7495 7496 html += `<div class="qb-popup-body">` 7497 7498 html += `<div class="qb-popup-success-icon" data-qb-popup-part="success-icon">` 7499 html += `<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="24" cy="24" r="24" fill="currentColor" opacity="0.1"/><path d="M15 24.5L21 30.5L33 18.5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>` 7500 html += `</div>` 7501 7502 html += `<h2 class="${cls.titleClasses}" data-qb-popup-part="title">${this._escapeHtml(title || '')}</h2>` 7503 html += `<div class="${cls.textClasses}" data-qb-popup-part="text">${textGoal || ''}</div>` 7504 7505 html += `</div>` 7506 7507 if (showImage && imagePosition === 'right') { 7508 html += `<img class="${cls.imageClasses} qb-popup-image--side" data-qb-popup-part="image" src="${image}" alt="" style="width: ${imageWidth}%" />` 7509 } 7510 7511 html += `</div>` 7512 7513 return html 7514 }, 7515 _escapeHtml: function (str) { 7516 const div = document.createElement('div') 7517 div.textContent = str 7518 return div.innerHTML 7519 }, 7520 _attachSubmitHandler: function () { 7521 document.addEventListener('submit', (e) => { 7522 if (e.target.classList.contains('qb-popup-form')) { 7523 e.preventDefault() 7524 this.onButtonClick(e.target) 7525 } 7526 }) 7527 7528 document.addEventListener('click', (e) => { 7529 var link = e.target.closest('[data-qb-popup-part="text"] a') 7530 if (link && this.data.popup) { 7531 var ppid = this.data.popup.ppid 7532 if (ppid) { 7533 sessionStorage.setItem('qb_popup_shown_' + ppid, '1') 7534 var formData = new FormData() 7535 formData.append('ppid', ppid) 7536 navigator.sendBeacon('/apps/storepopups/shown', formData) 7537 } 7538 } 7539 }) 7540 }, 7541 onButtonClick: function (form) { 7542 const { goal_type } = this?.data?.popup ?? {} 7543 7544 if (goal_type !== '1') { 7545 this._showSuccess() 7546 return 7547 } 7548 7549 const input = form.querySelector('input[data-qb-popup-part="input"]') 7550 const email = input.value.trim() 7551 7552 input.style.border = '' 7553 form.querySelector('[data-qb-popup-part="error"]')?.remove() 7554 7555 if (!email) { 7556 this._markInvalid(input) 7557 return 7558 } 7559 7560 const formData = new FormData() 7561 formData.append('email_address', email) 7562 7563 fetch('/apps/storepopups/add', { method: 'POST', body: formData }) 7564 .then((res) => res.json()) 7565 .then((data) => { 7566 if (data && data.success) { 7567 this._showSuccess() 7568 } else if (data && data.reason === 'invalid_email') { 7569 this._markInvalid(input) 7570 } else { 7571 this._showError(form) 7572 } 7573 }) 7574 .catch(() => this._showError(form)) 7575 }, 7576 _markInvalid: function (input) { 7577 if (input) input.style.border = '1px solid red' 7578 }, 7579 _showError: function (form) { 7580 let error = form.querySelector('[data-qb-popup-part="error"]') 7581 7582 if (!error) { 7583 error = document.createElement('div') 7584 error.className = 'qb-popup-error' 7585 error.setAttribute('data-qb-popup-part', 'error') 7586 form.appendChild(error) 7587 } 7588 7589 error.textContent = 7590 this?.data?.popup?.signup_error_text || 7591 'The signup could not be completed. Please try again later.' 7592 }, 7593 _showSuccess: function () { 7594 this._goalCompleted = true 7595 Alpine.store('modal').setPopupState( 7596 'success', 7597 (data, state) => this._render(data, state) 7598 ) 7599 }, 7600 _injectStyles: function () { 7601 if (this._cssInjected) return 7602 this._cssInjected = true 7603 7604 const style = document.createElement('style') 7605 style.setAttribute('data-qb-popup-styles', '') 7606 style.textContent = ` 7607 :root { 7608 --qb-popup-radius: 1rem; 7609 --qb-popup-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); 7610 --qb-popup-max-width: 28rem; 7611 --qb-popup-padding: 2rem 2.5rem; 7612 --qb-popup-enter-duration: 0.3s; 7613 --qb-popup-enter-easing: cubic-bezier(0.16, 1, 0.3, 1); 7614 --qb-popup-btn-radius: 0.5rem; 7615 --qb-popup-btn-padding: 0.75rem 2rem; 7616 --qb-popup-btn-font-weight: 600; 7617 --qb-popup-input-radius: 0.5rem; 7618 --qb-popup-input-padding: 0.75rem 1rem; 7619 --qb-popup-input-border: 1px solid #e5e7eb; 7620 --qb-popup-gap: 1.25rem; 7621 --qb-popup-title-size: clamp(1.5em, 4vw, 2em); 7622 --qb-popup-text-size: 1em; 7623 --qb-popup-content-enter: qb-popup-content-enter; 7624 } 7625 7626 .qb-popup-logo { 7627 max-height: 1.75rem; 7628 max-width: 8rem; 7629 object-fit: contain; 7630 } 7631 7632 /* Anchor popup font-size so children using em units do not inherit
7633 theme-level html font-size quirks (e.g. Charme sets html to 10px). */ 7634 .qb-popup-container, 7635 .qb-popup-cover, 7636 .qb-popup-horizontal { 7637 font-size: 16px; 7638 } 7639 7640 .qb-popup-container { 7641 text-align: center; 7642 padding: var(--qb-popup-padding); 7643 animation-name: var(--qb-popup-content-enter); 7644 animation-duration: var(--qb-popup-enter-duration); 7645 animation-timing-function: var(--qb-popup-enter-easing); 7646 animation-fill-mode: both; 7647 } 7648 7649 .qb-popup-title { 7650 font-size: var(--qb-popup-title-size); 7651 font-weight: 700; 7652 line-height: 1.2; 7653 margin: 0; 7654 } 7655 7656 .qb-popup-text { 7657 font-size: var(--qb-popup-text-size); 7658 line-height: 1.6; 7659 margin: 0; 7660 opacity: 0.85; 7661 } 7662 7663 .qb-popup-text a { text-decoration: underline; } 7664 .qb-popup-text .fr-green { color: #38a169; } 7665 .qb-popup-text .fr-strong { font-weight: bold; } 7666 7667 .qb-popup-form { 7668 display: flex; 7669 flex-direction: column; 7670 gap: 0.75rem; 7671 } 7672 7673 .qb-popup-input { 7674 width: 100%; 7675 padding: var(--qb-popup-input-padding); 7676 border: var(--qb-popup-input-border); 7677 border-radius: var(--qb-popup-input-radius); 7678 font-size: 1em; 7679 text-align: inherit; 7680 background: rgba(0, 0, 0, 0.03); 7681 color: inherit; 7682 outline: none; 7683 transition: border-color 0.2s, box-shadow 0.2s; 7684 box-sizing: border-box; 7685 } 7686 7687 .qb-popup-input:focus { 7688 border-color: currentColor; 7689 box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05); 7690 } 7691 7692 .qb-popup-button { 7693 display: inline-block; 7694 padding: var(--qb-popup-btn-padding); 7695 border: none; 7696 border-radius: var(--qb-popup-btn-radius); 7697 font-size: 1em; 7698 font-weight: var(--qb-popup-btn-font-weight); 7699 cursor: pointer; 7700 transition: opacity 0.2s, transform 0.1s; 7701 line-height: 1.4; 7702 } 7703 7704 .qb-popup-button:hover { 7705 opacity: 0.85; 7706 } 7707 7708 .qb-popup-button:active { 7709 transform: scale(0.98); 7710 } 7711 7712 .qb-popup-body { 7713 display: flex; 7714 flex-direction: column; 7715 gap: var(--qb-popup-gap); 7716 flex: 1; 7717 min-width: 0; 7718 } 7719 7720 .qb-popup-image { 7721 width: 100%; 7722 border-radius: calc(var(--qb-popup-radius) - 0.25rem); 7723 margin-bottom: var(--qb-popup-gap); 7724 object-fit: contain; 7725 max-height: var(--qb-popup-image-max-height, none); 7726 } 7727 7728 .qb-popup-has-top-image { 7729 padding: 0; 7730 } 7731 7732 .qb-popup-image--top { 7733 width: 100%; 7734 display: block; 7735 border-radius: 0; 7736 margin-bottom: 0; 7737 object-fit: cover; 7738 max-height: none; 7739 } 7740 7741 .qb-popup-has-top-image .qb-popup-body { 7742 padding: var(--qb-popup-padding); 7743 } 7744 7745 .qb-popup-cover { 7746 position: relative; 7747 display: flex; 7748 align-items: center; 7749 justify-content: center; 7750 text-align: center; 7751 min-height: 16rem; 7752 } 7753 7754 .qb-popup-cover .qb-popup-body { 7755 position: relative; 7756 z-index: 1; 7757 padding: var(--qb-popup-padding); 7758 text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); 7759 } 7760 7761 .qb-popup-horizontal { 7762 display: flex; 7763 padding: 0; 7764 gap: 0; 7765 } 7766 7767 .qb-popup-horizontal .qb-popup-body { 7768 padding: var(--qb-popup-padding); 7769 justify-content: center; 7770 } 7771 7772 .qb-popup-image--side { 7773 width: auto; 7774 margin: 0; 7775 border-radius: 0; 7776 flex-shrink: 0; 7777 object-fit: cover; 7778 max-height: none; 7779 } 7780 7781 .qb-popup-horizontal[data-qb-image-position="left"] .qb-popup-image--side { 7782 border-radius: calc(var(--qb-popup-radius) - 0.125rem) 0 0 calc(var(--qb-popup-radius) - 0.125rem); 7783 } 7784 7785 .qb-popup-horizontal[data-qb-image-position="right"] .qb-popup-image--side { 7786 border-radius: 0 calc(var(--qb-popup-radius) - 0.125rem) calc(var(--qb-popup-radius) - 0.125rem) 0; 7787 } 7788 7789 @media (max-width: 480px) { 7790 .qb-popup-horizontal { 7791 flex-direction: column; 7792 padding: 0; 7793 } 7794 .qb-popup-image--side { 7795 width: 100% !important; 7796 max-width: none !important; 7797 max-height: 12rem; 7798 order: -1; 7799 object-fit: cover; 7800 border-radius: 0 !important; 7801 } 7802 .qb-popup-horizontal .qb-popup-body { 7803 padding: var(--qb-popup-padding); 7804 } 7805 } 7806 7807 .qb-popup-success { 7808 animation-name: var(--qb-popup-content-enter); 7809 animation-duration: var(--qb-popup-enter-duration); 7810 animation-timing-function: var(--qb-popup-enter-easing); 7811 animation-fill-mode: both; 7812 } 7813 7814 .qb-popup-success-icon { 7815 margin-bottom: 1rem; 7816 color: #38a169; 7817 } 7818 7819 .qb-popup-success-icon svg { 7820 display: inline-block; 7821 } 7822 7823 .qb-popup-fine-print { 7824 font-size: 0.8125em; 7825 line-height: 1.4; 7826 opacity: 0.5; 7827 margin-top: 0.25rem; 7828 color: inherit; 7829 } 7830 7831 .qb-popup-fine-print a { text-decoration: underline; } 7832 7833 .qb-popup-error { 7834 font-size: 0.875em; 7835 color: #dc2626; 7836 margin-top: 0.25rem; 7837 } 7838 7839 @keyframes qb-popup-content-enter { 7840 from { 7841 opacity: 0; 7842 transform: translateY(8px); 7843 } 7844 to { 7845 opacity: 1; 7846 transform: translateY(0); 7847 } 7848 } 7849 7850 @media (max-width: 640px) { 7851 :root { 7852 --qb-popup-padding: 1.5rem 1.25rem; 7853 --qb-popup-max-width: 100%; 7854 } 7855 } 7856 7857 /* Minimize tab */ 7858 .qb-popup-minimize-tab { 7859 position: fixed; 7860 bottom: 1rem; 7861 left: 50%; 7862 transform: translateX(-50%); 7863 z-index: 9999; 7864 display: flex; 7865 align-items: center; 7866 gap: 0.5rem; 7867 padding: 0.625rem 1.25rem; 7868 border-radius: 9999px; 7869 cursor: pointer; 7870 font-size: 0.875rem; 7871 font-weight: 600; 7872 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 7873 animation: qb-tab-slide-up 0.3s cubic-bezier(0.16, 1, 0.3, 1) both; 7874 border: none; 7875 font-family: inherit; 7876 line-height: 1; 7877 white-space: nowrap; 7878 max-width: calc(100% - 2rem); 7879 } 7880 7881 .qb-popup-minimize-tab:hover { 7882 filter: brightness(1.1); 7883 } 7884 7885 .qb-popup-minimize-tab--exit { 7886 animation: qb-tab-slide-down 0.2s ease forwards !important; 7887 } 7888 7889 .qb-popup-minimize-tab__chevron { 7890 display: flex; 7891 align-items: center; 7892 flex-shrink: 0; 7893 } 7894 7895 .qb-popup-minimize-tab__close { 7896 display: flex; 7897 align-items: center; 7898 justify-content: center; 7899 background: none; 7900 border: none; 7901 cursor: pointer; 7902 color: inherit; 7903 opacity: 0.6; 7904 padding: 0.125rem; 7905 margin-left: 0.25rem; 7906 flex-shrink: 0; 7907 transition: opacity 0.15s; 7908 } 7909 7910 .qb-popup-minimize-tab__close:hover { 7911 opacity: 1; 7912 } 7913 7914 @keyframes qb-tab-slide-up { 7915 from { transform: translateX(-50%) translateY(calc(100% + 1rem)); } 7916 to { transform: translateX(-50%) translateY(0); } 7917 } 7918 7919 @keyframes qb-tab-slide-down { 7920 from { transform: translateX(-50%) translateY(0); } 7921 to { transform: translateX(-50%) translateY(calc(100% + 1rem)); } 7922 } 7923 ` 7924 document.head.prepend(style) 7925 }, 7926}) 7927</script>
7927<script type="text/javascript">window.registerApp({ 7928 name: 'Free Shipping Timer', 7929 identifier: 'QuickbutikFreeShippingTimer', 7930 description: 'Show free shipping timer', 7931 version: '1.0.0', 7932 pages: ['list', 'start', 'product'], 7933 data: {}, 7934 config: {}, 7935 onMount: function ({ appData, page }) { 7936 console.debug(`${this.identifier}@onMount`) 7937 7938 if (appData?.freeshippingtimer) { 7939 setTimeout(() => { 7940 Alpine.effect(() => { 7941 const currentAmount = Alpine.store('cart').totalAmountRaw 7942 const previousAmount = this.data.amount 7943 7944 // Skip if cart data hasn't loaded yet (null = initial state) 7945 if (currentAmount === null) { 7946 return 7947 } 7948 7949 // Detect if this is initial load (previous was null/undefined) or a cart change 7950 const isInitialLoad = previousAmount === null || previousAmount === undefined 7951 const hasCartChanged = previousAmount !== currentAmount 7952 7953 // Update stored amount 7954 this.data.amount = currentAmount 7955 7956 // Show notification on initial load (if cart has items) or when cart changes 7957 if ((isInitialLoad && currentAmount > 0) || (!isInitialLoad && hasCartChanged)) { 7958 fetch(qs_store_url + '/apps/freeshippingtimer/fetch', { 7959 method: 'POST', 7960 }) 7961 .then((response) => response.json()) 7962 .then((response) => { 7963 if (response?.message) { 7964 Alpine.store('notifications').addNotification({ 7965 type: 'info', 7966 category: 'freeshippingtimer', 7967 clearCategory: true, 7968 message: response.message, 7969 lifeTime: 5000, 7970 backgroundColor: appData?.freeshippingtimer?.bg_color, 7971 textColor: appData?.freeshippingtimer?.text_color, 7972 }) 7973 } 7974 }) 7975 } 7976 }) 7977 }, 2000) 7978 } 7979 }, 7980}) 7981</script>
7981<script type="text/javascript">window.registerApp({ 7982 name: 'Product Sort', 7983 identifier: 'QuickbutikProductSort', 7984 description: 'Product sort', 7985 version: '1.1.1', 7986 pages: ['list'], 7987 data: {}, 7988 config: { 7989 productElementSelector: '[data-qb-selector="product-item"]', 7990 sortElementTargetSelector: '[data-qb-selector="sort-container"]', 7991 styling: { 7992 sortElementClasses: '', 7993 }, 7994 }, 7995 onMount: function ({ appData, page }) { 7996 console.debug(`${this.identifier}@onMount`) 7997 7998 if (appData?.productsort) { 7999 if (appData.productsort?.sortoptions) { 8000 this.config.options = appData.productsort.sortoptions 8001 } 8002 8003 if (this.config.options) { 8004 this.injectSortElement({appData}) 8005 } 8006 } 8007 }, 8008 injectSortElement: function ({ appData }) { 8009 const container = document.querySelector( 8010 this.config.sortElementTargetSelector 8011 ) 8012 8013 if (container) { 8014 container.appendChild(this.renderSortElement({appData})) 8015 } 8016 }, 8017 renderSortElement: function ({ appData }) { 8018 const sortElement = document.createElement('select') 8019 8020 if (this.config.styling.sortElementClasses) { 8021 sortElement.classList.add( 8022 ...this.config.styling.sortElementClasses.split(' ') 8023 ) 8024 } 8025 8026 // Get sort param from URL 8027 var preselectedSort = new URL(location.href).searchParams.get('sort') 8028 8029 // If no sort param from URL, use category's default sorting 8030 if (preselectedSort === null && appData.productsort?.set_default != '') { 8031 var preselectedSort = appData.productsort?.set_default 8032 } 8033 8034 Object.keys(this.config.options).forEach((optionKey) => { 8035 const optionElement = document.createElement('option') 8036 8037 const option = { 8038 value: optionKey, 8039 label: this.config.options[optionKey], 8040 } 8041 8042 optionElement.value = option.value 8043 optionElement.textContent = option.label 8044 8045 // Select default option (from category or URL) 8046 if (option.value == preselectedSort) { 8047 optionElement.defaultSelected = true 8048 } 8049 8050 sortElement.appendChild(optionElement) 8051 }) 8052 8053 sortElement.addEventListener('change', (event) => { 8054 this.onSort(event.target.value) 8055 }) 8056 8057 return sortElement 8058 }, 8059 onSort: function (sort) { 8060 8061 // Redirect when paginating 8062 if (qs_store_apps_data.pagination.pagination_activated) { 8063 var url = new URL(location.href); 8064 url.searchParams.set('sort', sort); 8065 url.searchParams.delete('page'); 8066 8067 window.location.href = url.href; 8068 8069 // Normal on-page filtering without pagination 8070 } else { 8071 const sortBy = sort.split('_')[0] 8072 const sortDirection = sort.split('_')[1] 8073 8074 const containerElement = document.getElementById('productList') 8075 const childElements = Array.from( 8076 containerElement.querySelectorAll(this.config?.productElementSelector) 8077 ) 8078 8079 const bucket = [] 8080 8081 childElements.forEach((child) => { 8082 const sortValue = child.getAttribute(`data-${sortBy}`) 8083 8084 bucket.push({ 8085 element: child, 8086 sortValue: sortValue 8087 }) 8088 }) 8089 8090 const isNumeric = sortBy.includes('price') 8091 8092 const locale = document.querySelector('html').getAttribute('lang') || 'en' 8093 8094 const sortedKeys = bucket.sort((a, b) => { 8095 return sortDirection === '1' 8096 ? a.sortValue.localeCompare(b.sortValue, locale, { 8097 numeric: isNumeric 8098 }) 8099 : b.sortValue.localeCompare(a.sortValue, locale, { 8100 numeric: isNumeric 8101 }) 8102 }) 8103 8104 const sortedElements = sortedKeys.map((item) => item.element) 8105 8106 sortedElements.forEach((child) => containerElement.appendChild(child)) 8107 8108 } 8109 }, 8110 8111}) 8112</script>
8112<script type="text/javascript">window.registerApp({ 8113 name: "ProductWatch", 8114 identifier: "QuickbutikProductWatch", 8115 description: "Add products to watchlist", 8116 version: "1.0.0", 8117 pages: ["product"], 8118 data: {}, 8119 config: { 8120 showProductWatchButton: true, 8121 productSelector: "[data-qb-selector='product']", 8122 button: { 8123 appsContainer: '[data-qb-selector="product-apps"]', 8124 insertPosition: "beforeend", // 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' 8125 buttonTag: "a", 8126 buttonClasses: 8127 "flex-shrink-0 flex w-full flex-wrap items-center justify-center w-16 h-16 font-bold bg-white text-black border", 8128 }, 8129 }, 8130 onMount: function ({ appData, page }) { 8131 console.debug(`${this.identifier}@onMount`); 8132 8133 if (page === "product" && appData?.productwatchers) { 8134 this.data = appData?.productwatchers ?? {}; 8135 const productId = document.querySelector(this.config.productSelector) 8136 ?.dataset?.productId; 8137 8138 if (productId) { 8139 this.insertWatchProductButton({ productId }); 8140 } 8141 } 8142 }, 8143 insertWatchProductButton: function ({ productId }) { 8144 const { appsContainer, buttonTag, buttonClasses, insertPosition } = 8145 this.config.button; 8146 8147 const { btn_text, btn_bg_color, btn_text_color } = this.data ?? {}; 8148 8149 const { showProductWatchButton } = this.config; 8150 8151 if (!showProductWatchButton) { 8152 return; 8153 } 8154 8155 const template = document.createElement("template"); 8156 template.setAttribute("x-if", "!available"); 8157 8158 const watchProductButton = document.createElement(buttonTag); 8159 8160 watchProductButton.classList.add(...buttonClasses.split(" ")); 8161 watchProductButton.innerHTML = btn_text; 8162 8163 watchProductButton.style.backgroundColor = btn_bg_color; 8164 watchProductButton.style.color = btn_text_color; 8165 8166 watchProductButton.setAttribute( 8167 "x-on:click", 8168 `window.apps.${this.identifier}.onButtonClick($data.variant?.id)` 8169 ); 8170 8171 template.content.appendChild(watchProductButton); 8172 8173 let container = document.querySelector(appsContainer); 8174 if (container) { 8175 container.insertAdjacentElement(insertPosition, template); 8176 } 8177 }, 8178 onButtonClick: function (variantId) { 8179 const { popup_bg_color } = this.data ?? {}; 8180 8181 Alpine.store("modal").openModal(this.renderModal(variantId), { 8182 backgroundColor: popup_bg_color, 8183 }); 8184 8185 setTimeout(() => { 8186 document.querySelector("#watchlistEmail").focus(); 8187 }, 500); 8188 }, 8189 renderModal: function (variantId) { 8190 const { 8191 popup_title, 8192 popup_subtitle, 8193 btn_text, 8194 popup_text_color, 8195 btn_text_color, 8196 btn_bg_color, 8197 } = this.data ?? {}; 8198 8199 const form = document.createElement("form"); 8200 form.classList.add(); 8201 form.style.color = popup_text_color ?? "#000"; 8202 8203 const h2 = document.createElement("h2"); 8204 8205 const titleClasses = this.config?.modal?.titleClasses; 8206 if (titleClasses) { 8207 h2.classList.add(...titleClasses.split(" ")); 8208 } 8209 8210 h2.textContent = popup_title; 8211 form.appendChild(h2); 8212 8213 const paragraph = document.createElement("p"); 8214 paragraph.innerHTML = popup_subtitle; 8215 form.appendChild(paragraph); 8216 8217 const emailInput = document.createElement("input"); 8218 emailInput.type = "email"; 8219 emailInput.id = "watchlistEmail"; 8220 8221 const inputClasses = this.config?.modal?.inputClasses; 8222 if (inputClasses) { 8223 emailInput.classList.add(...inputClasses.split(" ")); 8224 } 8225 8226 emailInput.placeholder = this.config?.modal?.inputPlaceholder ?? "Email"; 8227 form.appendChild(emailInput); 8228 8229 const submitButton = document.createElement("button"); 8230 8231 const submitButtonClasses = this.config?.modal?.submitButtonClasses; 8232 if (submitButtonClasses) { 8233 submitButton.classList.add(...submitButtonClasses.split(" ")); 8234 } 8235 8236 submitButton.style.backgroundColor = btn_bg_color ?? "#000"; 8237 submitButton.style.color = btn_text_color ?? "#fff"; 8238 submitButton.textContent = btn_text; 8239 submitButton.type = "submit"; 8240 8241 form.appendChild(submitButton); 8242 8243 form.setAttribute( 8244 "x-on:submit.prevent", 8245 `window.apps.QuickbutikProductWatch.submitEmail(document.querySelector('#watchlistEmail').value, ${variantId})` 8246 ); 8247 8248 return form.outerHTML; 8249 }, 8250 submitEmail: function (email, variantId) { 8251 const productId = document.querySelector(this.config.productSelector) 8252 .dataset.productId; 8253 8254 const UNKNOWKN_ERROR_MESSAGE = 8255 this.config?.unknowknErrorMessage ?? 8256 "<p>Vänligen försö
8256k igen senare.</p>"; 8257 8258 const bodyFormData = new FormData(); 8259 bodyFormData.set("email_address", email); 8260 bodyFormData.set("product_id", productId); 8261 8262 if (variantId) { 8263 bodyFormData.set("variant_id", variantId); 8264 } 8265 8266 fetch( 8267 this.config?.productPage?.addProductWatchUrl ?? 8268 "/apps/productwatchers/add", 8269 { 8270 method: "POST", 8271 body: bodyFormData, 8272 } 8273 ) 8274 .then((response) => response.json()) 8275 .then((response) => { 8276 if (response?.success) { 8277 Alpine.store("modal").setContent( 8278 this.renderSuccessPage(response?.text_thankyou || "<p>Tack</p>") 8279 ); 8280 8281 setTimeout(() => { 8282 Alpine.store("modal").closeModal(); 8283 }, 3000); 8284 } else { 8285 console.error(response); 8286 8287 Alpine.store("modal").setContent( 8288 this.renderErrorPage(response?.text_error || UNKNOWKN_ERROR_MESSAGE) 8289 ); 8290 } 8291 }) 8292 .catch((error) => { 8293 console.error("Submit email error", error); 8294 8295 Alpine.store("modal").setContent( 8296 this.renderErrorPage(error?.text_error || UNKNOWKN_ERROR_MESSAGE) 8297 ); 8298 }); 8299 }, 8300 renderSuccessPage: function (message) { 8301 const { popup_title, popup_text_color } = this.data ?? {}; 8302 8303 const h2 = document.createElement("h2"); 8304 8305 const titleClasses = this.config?.modal?.titleClasses; 8306 if (titleClasses) { 8307 h2.classList.add(...titleClasses.split(" ")); 8308 } 8309 8310 h2.textContent = popup_title; 8311 8312 const paragraph = document.createElement("p"); 8313 paragraph.innerHTML = message; 8314 8315 const container = document.createElement("div"); 8316 8317 const containerClasses = this.config?.modal?.containerClasses; 8318 if (containerClasses) { 8319 container.classList.add(...containerClasses.split(" ")); 8320 } 8321 8322 container.appendChild(h2); 8323 container.appendChild(paragraph); 8324 container.style.color = popup_text_color || "#000"; 8325 8326 return container.outerHTML; 8327 }, 8328 renderErrorPage: function (message) { 8329 const h2 = document.createElement("h2"); 8330 h2.classList.add("font-heading", "text-5xl", "mb-4"); 8331 h2.textContent = this.config?.modal?.errorTitle ?? "Something went wrong"; 8332 8333 const paragraph = document.createElement("p"); 8334 paragraph.innerHTML = message; 8335 8336 const container = document.createElement("div"); 8337 8338 const containerClasses = this.config?.modal?.containerClasses; 8339 if (containerClasses) { 8340 container.classList.add(...containerClasses.split(" ")); 8341 } 8342 8343 container.appendChild(h2); 8344 container.appendChild(paragraph); 8345 8346 return container.outerHTML; 8347 }, 8348}); 8349</script>
8349<script type="text/javascript">window.registerApp({ 8350 name: 'Product Filter', 8351 identifier: 'QuickbutikProductFilter', 8352 description: 'Product filter', 8353 version: '2.0.0', 8354 pages: ['list'], 8355 data: { 8356 filters: {}, 8357 products: [], 8358 initialized: false, 8359 }, 8360 8361 // XSS Protection: Sanitize HTML content 8362 sanitizeHTML: function(html) { 8363 if (!html) return '' 8364 8365 // Create a temporary div to parse and sanitize HTML 8366 const temp = document.createElement('div') 8367 temp.textContent = html 8368 return temp.innerHTML 8369 }, 8370 8371 // XSS Protection: Safe HTML insertion 8372 safeSetHTML: function(element, html) { 8373 if (!element || !html) return 8374 8375 // For trusted content (like SVG icons), we can use innerHTML 8376 // For user content, we sanitize it 8377 if (this.isTrustedContent(html)) { 8378 element.innerHTML = html 8379 } else { 8380 element.innerHTML = this.sanitizeHTML(html) 8381 } 8382 }, 8383 8384 // Check if content is trusted (like SVG icons, static HTML) 8385 isTrustedContent: function(html) { 8386 // Trust SVG icons and static HTML templates 8387 return html.includes('<svg') || 8388 html.includes('class=') && !html.includes('onclick') && 8389 !html.includes('onload') && !html.includes('onerror') && 8390 !html.includes('javascript:') && !html.includes('data:') 8391 }, 8392 config: { 8393 filterElementTargetSelector: '[data-qb-selector="filter-container"]', 8394 styling: { 8395 filterElementClasses: '', 8396 classes: { 8397 // Main containers 8398 filterButtonContainer: 'pf-filter-button-container', 8399 filtersWrapper: 'pf-inline-filters-wrapper relative z-10 flex flex-col items-start', 8400 filtersContainer: 'pf-inline-filters flex flex-wrap items-start justify-start w-full', 8401 activeFiltersSection: 'pf-active-filters-section w-full', 8402 activeFiltersRow: 'pf-active-filters-row flex flex-wrap gap-2 items-start', 8403 activeFiltersContainer: 'pf-active-filters flex gap-2 items-center flex-1', 8404 8405 // Filter dropdowns 8406 filterDropdown: 'pf-filter-dropdown relative min-w-48', 8407 filterButton: 'pf-filter-button relative flex items-center justify-between cursor-pointer border px-4 py-3 button-bg button-text button-py button-px button-border button-rounding', 8408 filterButtonText: 'pf-filter-button-text flex-1 text-left input-text whitespace-nowrap overflow-hidden text-ellipsis', 8409 filterButtonCount: 'pf-filter-button-count rounded-full px-2 py-2 text-center mr-2', 8410 filterButtonBadge: 'pf-filter-button-badge text-white text-xs rounded-full w-5 h-5 flex items-center justify-center ml-2', 8411 filterArrow: 'pf-filter-arrow ml-2 text-xs transition-transform duration-200', 8412 8413 // Dropdown content 8414 dropdownContent: 'pf-filter-dropdown-content absolute top-full left-0 bg-white border border-gray-300 shadow-lg z-50 max-h-75 overflow-y-auto mt-1 hidden min-w-full w-max', 8415 8416 // Filter options 8417 filterGroup: 'pf-filter-group pb-4', 8418 filterOption: 'pf-filter-option block p-0 transition-colors', 8419 filterOptionCheckbox: 'sr-only', 8420 filterOptionLabel: 'pf-filter-option-label flex items-center justify-between cursor-pointer px-4 py-1 relative transition-all whitespace-nowrap', 8421 8422 // Active filter badges 8423 filterBadge: 'pf-filter-badge inline-flex items-center text-xs', 8424 filterBadgeRemove: 'pf-filter-badge-remove ml-2 inline-flex items-center justify-center cursor-pointer rounded-full text-sm transition-colors hover:bg-opacity-30', 8425 8426 // Reset button 8427 resetButton: 'pf-reset-filters-btn cursor-pointer px-4 py-2 text-sm transition-all whitespace-nowrap', 8428 clearFiltersButton: 'pf-clear-filters-btn cursor-pointer px-4 py-2 text-sm font-medium transition-all whitespace-nowrap text-gray-600 hover:text-gray-800', 8429 8430 // No results message 8431 noResults: 'pf-no-results', 8432 noResultsContainer: 'pf-no-results-container', 8433 }, 8434 }, 8435 }, 8436 onMount: function ({ appData }) { 8437 // Cleanup any existing listeners before reinitializing 8438 if (this.data.initialized) { 8439 this.cleanup() 8440 } 8441 8442 if (appData?.productfilters && appData.productfilters?.products) { 8443 this.config.filters = [] 8444 this.data.products = appData.productfilters.products 8445 this.applyDataTagsToProducts() 8446 8447 // Ensure the data is available globally 8448 window.qs_store_apps = window.qs_store_apps || {} 8449 window.qs_store_apps.productfilters = appData.productfilters 8450 8451 // Check if sidebar container exists, otherwise use drawer 8452 const sidebarContainer = document.querySelector('[data-qb-selector="filter-sidebar-container"]') 8453 if (sidebarContainer) { 8454 this.createSidebarFilters(appData.productfilters, sidebarContainer) 8455 } else { 8456 this.createDrawerFilters(appData.productfilters) 8457 } 8458 8459 // Add pageshow event listener only when we use client-side filtering 8460 if (this.shouldUseClientSideFiltering()) { 8461 this.handlePageShow = () => { 8462 const filterElements = document.querySelectorAll('[data-filter-id]') 8463 for (const element of filterElements) { 8464 const originalOnchange = element.onchange 8465 element.onchange = null 8466 element.onchange = originalOnchange 8467 } 8468 } 8469 window.addEventListener('pageshow', this.handlePageShow) 8470 } 8471 8472 // Mark as initialized 8473 this.data.initialized = true 8474 8475 // Add beforeunload cleanup to ensure listeners are removed on page navigation 8476 this.handleBeforeUnload = () => { 8477 this.cleanup() 8478 } 8479 window.addEventListener('beforeunload', this.handleBeforeUnload) 8480 } 8481 }, 8482 8483 onUnmount: function () { 8484 // Cleanup event listeners when component is unmounted 8485 this.cleanup() 8486 this.data.initialized = false 8487 }, 8488 8489 // Global cleanup function that can be called externally 8490 globalCleanup: function() { 8491 if (window.QuickbutikProductFilter && window.QuickbutikProductFilter.cleanup) { 8492 window.QuickbutikProductFilter.cleanup() 8493 } 8494 }, 8495 8496 // Helper function to determine if we should use client-side filtering 8497 // Returns true only if pagination is not activated 8498 shouldUseClientSideFiltering: function () { 8499 const pagination = qs_store_apps_data?.pagination || {} 8500 8501 // If pagination is not activated, use client-side filtering 8502 if (!pagination.pagination_activated) { 8503 return true 8504 } 8505 8506 // If pagination is activated, always use server-side filtering (reload page) 8507 return false 8508 }, 8509 applyDataTagsToProducts: function () { 8510 // Index the product elements once instead of querying the document per product. 8511 const productElementsById = new Map() 8512 8513 document 8514 .querySelectorAll('[data-qb-selector="product-item"][data-product-id]') 8515 .forEach((element) => { 8516 const productId = element.getAttribute('data-product-id') 8517 8518 if (!productElementsById.has(productId)) { 8519 productElementsById.set(productId, element) 8520 } 8521 }) 8522
8523 Object.keys(this.data.products).forEach((productKey) => { 8524 const productFilter = this.data.products[productKey] 8525 const productElement = productElementsById.get(String(productFilter.id)) 8526 8527 if (productElement) { 8528 Object.keys(productFilter?.filters).forEach((filterKey) => { 8529 const filterData = productFilter.filters[filterKey] 8530 // Parse composite filter id to get filter group and value 8531 const compositeIdParts = filterData.id ? filterData.id.split('-') : [] 8532 8533 if (compositeIdParts.length === 2) { 8534 const actualFilterGroupId = compositeIdParts[0] 8535 const actualFilterValueId = compositeIdParts[1] 8536 8537 // Set specific filter attribute: data-filter-${filterGroupId}-${filterValueId} 8538 productElement.setAttribute( 8539 `data-filter-${actualFilterGroupId}-${actualFilterValueId}`, 8540 '1' 8541 ) 8542 } 8543 }) 8544 } 8545 8546 }) 8547 }, 8548 injectFilter: function (renderedFilterElement) { 8549 const container = document.querySelector( 8550 this.config.filterElementTargetSelector 8551 ) 8552 8553 if (container) { 8554 container.appendChild(renderedFilterElement) 8555 } 8556 }, 8557 renderFilterElement: function (filter) { 8558 const filterElement = document.createElement('select') 8559 filterElement.setAttribute('data-filter-id', filter.id) 8560 filterElement.setAttribute('data-hide-on-soldout', filter.variant_outofstock_check) 8561 8562 const url = new URL(location.href) 8563 const preselectedFilter = url.searchParams.get('filter_' + filter.id); 8564 8565 if (this.config.styling.filterElementClasses) { 8566 filterElement.classList.add( 8567 ...this.config.styling.filterElementClasses.split(' ') 8568 ) 8569 } 8570 8571 const options = [ 8572 { 8573 id: '', 8574 title: `-- ${filter.title} --`, 8575 }, 8576 ...filter.values, 8577 ] 8578 8579 options.forEach((option) => { 8580 const optionElement = document.createElement('option') 8581 8582 optionElement.value = option.id 8583 optionElement.textContent = option.title 8584 8585 // Select default option (from category or URL) 8586 if (option.id === preselectedFilter) { 8587 optionElement.defaultSelected = true 8588 } 8589 8590 filterElement.appendChild(optionElement) 8591 }) 8592 8593 filterElement.addEventListener('change', (event) => { 8594 this.onFilter(event.target) 8595 }) 8596 8597 return filterElement 8598 }, 8599 onFilter: function (filter) { 8600 8601 // Use client-side filtering or redirect based on pagination state 8602 if (this.shouldUseClientSideFiltering()) { 8603 // Normal on-page filtering without pagination or with single page 8604 if (filter.value === '') { 8605 delete this.data.filters[filter.dataset.filterId] 8606 } else { 8607 this.data.filters[filter.dataset.filterId] = filter.value 8608 } 8609 8610 this.updateProductList() 8611 } else { 8612 // Redirect when paginating with multiple pages 8613 const url = new URL(location.href) 8614 if (filter.value === '') { 8615 url.searchParams.delete('filter_' + filter.dataset.filterId) 8616 } else { 8617 url.searchParams.set('filter_' + filter.dataset.filterId, filter.value) 8618 } 8619 url.searchParams.delete('page') 8620 8621 window.location.href = url.href; 8622 } 8623 8624 }, 8625 updateProductList: function () { 8626 const containerElement = document.getElementById('productList') 8627 const childElements = Array.from( 8628 containerElement.querySelectorAll('[data-qb-selector="product-item"]') 8629 ) 8630 8631 childElements.forEach((child) => { 8632 let show = true 8633 8634 Object.keys(this.data.filters).forEach((filterKey) => { 8635 const targetDataKey = `filter-${filterKey}-${this.data.filters[filterKey]}` 8636 8637 if (!Object.keys(child.dataset).includes(targetDataKey)) { 8638 show = false 8639 } 8640 }) 8641 8642 if (show) { 8643 child.classList.remove('hidden') 8644 } else { 8645 child.classList.add('hidden') 8646 } 8647 }) 8648 }, 8649 8650 // Drawer Filter System Functions 8651 createDrawerFilters: function (data) { 8652 if (!data || !data.filters) { 8653 return 8654 } 8655 8656 // Create the main drawer filter container 8657 const drawerContainer = this.renderDrawerFilterContainer(data) 8658 8659 // Inject into the target container 8660 this.injectDrawerFilters(drawerContainer) 8661 8662 // Create drawer elements directly in body for highest stacking context 8663 this.createDrawerElements(data) 8664 8665 // Set up drawer filter functionality 8666 this.setupDrawerFilterHandlers(data) 8667 }, 8668 8669 // Sidebar Filter System Functions 8670 createSidebarFilters: function (data, sidebarContainer) { 8671 if (!data || !data.filters) { 8672 return 8673 } 8674 8675 // Create sidebar filter content 8676 const sidebarContent = this.renderSidebarFilterContent(data) 8677 8678 // Inject into the sidebar container 8679 sidebarContainer.appendChild(sidebarContent) 8680 8681 // First create the drawer content (temporarily) 8682 this.createDrawerFilters(data) 8683 8684 // Then populate the sidebar with the drawer content 8685 this.populateSidebarContent(data.filters) 8686 8687 // Hide the filter button since we have sidebar 8688 this.hideFilterButton() 8689 8690 // Set up sidebar filter functionality 8691 this.setupSidebarFilterHandlers(data) 8692 }, 8693 8694 setupSidebarFilterHandlers: function (data) { 8695 // Initialize state variables 8696 this.currentSelections = this.parseUrlFilters() 8697 this.pendingSelections = { ...this.currentSelections } 8698 8699 // Populate sidebar content 8700 this.populateSidebarContent(data.filters) 8701 8702 // Set up event handlers 8703 this.setupSidebarCheckboxHandlers() 8704 this.setupSidebarActionButtons() 8705 8706 // Apply initial selections from URL 8707 this.applyInitialSelections() 8708 this.updateActiveFiltersDisplay() 8709 8710 // Apply filtering if we have selections and should use client-side filtering 8711 if (this.shouldUseClientSideFiltering() && Object.keys(this.currentSelections).length > 0) { 8712 this.applyClientSideFiltering() 8713 } 8714 }, 8715 8716 renderDrawerFilterContainer: function (data) { 8717 const container = document.createElement('div') 8718 container.className = this.config.styling.classes.filtersWrapper 8719 8720 // Add CSS styles 8721 const style = document.createElement('style') 8722 style.textContent = this.getDrawerFilterCSS() 8723 container.appendChild(style) 8724 8725 // Create filter button container 8726 const buttonContainer = document.createElement('div') 8727 buttonContainer.className = this.config.styling.classes.filterButtonContainer 8728 buttonContainer.innerHTML = ` 8729 <button id="pfFilterButton" class="${this.config.styling.classes.filterButton}" type="button" aria-expanded="false" aria-haspopup="dialog" aria-controls="pfFilterDrawer" aria-describedby="pfFilterDescription">
8730 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="pf-filter-icon" aria-hidden="true"> 8731 <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" stroke="currentColor" stroke-width="1.5"/> 8732 </svg> 8733 <span class="flex items-center"> 8734 ${data.i18n?.filter || 'Filtrera'} 8735 <span id="pfFilterButtonBadge" class="${this.config.styling.classes.filterButtonBadge}" style="display: none;" aria-label="Antal aktiva filter"></span> 8736 </span> 8737 </button> 8738 <div id="pfFilterDescription" class="sr-only">${data.i18n?.filterBy || 'Filtrera efter'} produkter. Klicka för att öppna filteralternativ.</div> 8739 <div class="${this.config.styling.classes.activeFiltersContainer}" id="pfActiveFilters" role="region" aria-label="Aktiva filter" aria-live="polite" aria-atomic="true"></div> 8740 <div class="${this.config.styling.classes.resetButton}" id="pfResetAllFilters" style="display: none;" role="button" tabindex="0" aria-label="Rensa alla filter" aria-describedby="pfResetDescription">${data.i18n?.clearAllFilters || 'Rensa alla filter'}</div> 8741 <div id="pfResetDescription" class="sr-only">Tar bort alla aktiva filter och visar alla produkter.</div> 8742 ` 8743 container.appendChild(buttonContainer) 8744 8745 return container 8746 }, 8747 8748 renderSidebarFilterContent: function (data) { 8749 const container = document.createElement('div') 8750 container.className = 'pf-sidebar-filters' 8751 8752 // Add CSS styles 8753 const style = document.createElement('style') 8754 style.textContent = this.getSidebarFilterCSS() 8755 container.appendChild(style) 8756 8757 // Create sidebar header 8758 const header = document.createElement('div') 8759 header.className = 'pf-sidebar-header mb-6' 8760 header.innerHTML = ` 8761 <h3 class="text-lg font-semibold mb-4">${data.i18n?.filterBy || 'Filtrera efter'}</h3> 8762 <div class="pf-active-filters mb-4" id="pfActiveFilters" role="region" aria-label="Aktiva filter" aria-live="polite" aria-atomic="true"></div> 8763 <div class="pf-reset-all mb-4" id="pfResetAllFilters" style="display: none;" role="button" tabindex="0" aria-label="Rensa alla filter">${data.i18n?.clearAllFilters || 'Rensa alla filter'}</div> 8764 ` 8765 container.appendChild(header) 8766 8767 // Create filter content 8768 const content = document.createElement('div') 8769 content.className = 'pf-sidebar-content' 8770 content.id = 'pfSidebarContent' 8771 container.appendChild(content) 8772 8773 return container 8774 }, 8775 8776 populateSidebarContent: function (filters) { 8777 const sidebarContent = document.getElementById('pfSidebarContent') 8778 if (!sidebarContent) return 8779 8780 // Use the existing populateDrawerContent function with sidebar-specific container 8781 this.populateDrawerContent(filters) 8782 8783 // Move the content to sidebar container 8784 const drawerContent = document.getElementById('pfDrawerContent') 8785 if (drawerContent) { 8786 // XSS Protection: Use safe HTML transfer instead of direct innerHTML 8787 this.safeSetHTML(sidebarContent, drawerContent.innerHTML) 8788 drawerContent.innerHTML = '' 8789 8790 // Set up accordion functionality for the sidebar content 8791 this.setupSidebarAccordion() 8792 } 8793 }, 8794 8795 hideFilterButton: function () { 8796 // Hide the filter button and related elements when sidebar is active 8797 const filterButton = document.getElementById('pfFilterButton') 8798 const filterButtonContainer = document.querySelector('.pf-filter-button-container') 8799 const filtersWrapper = document.querySelector('.pf-inline-filters-wrapper') 8800 8801 if (filterButton) { 8802 filterButton.style.display = 'none' 8803 } 8804 8805 if (filterButtonContainer) { 8806 filterButtonContainer.style.display = 'none' 8807 } 8808 8809 if (filtersWrapper) { 8810 filtersWrapper.style.display = 'none' 8811 } 8812 }, 8813 8814 setupSidebarAccordion: function () { 8815 const sidebarContent = document.getElementById('pfSidebarContent') 8816 if (!sidebarContent) return 8817 8818 // Add accordion functionality to sidebar filter group titles 8819 sidebarContent.querySelectorAll('.pf-filter-group-title').forEach(title => { 8820 title.addEventListener('click', function () { 8821 const optionsContainer = this.nextElementSibling 8822 const icon = this.querySelector('.pf-accordion-icon') 8823 const isExpanded = this.getAttribute('aria-expanded') === 'true' 8824 8825 if (optionsContainer && optionsContainer.classList.contains('pf-filter-options-container')) { 8826 if (isExpanded) { 8827 // Collapse 8828 optionsContainer.style.display = 'none' 8829 this.setAttribute('aria-expanded', 'false') 8830 // Show plus icon (collapsed) 8831 if (icon) { 8832 icon.innerHTML = `
8833 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 8834 <path d="M12 4.5v15m7.5-7.5h-15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 8835 </svg> 8836 ` 8837 } 8838 } else { 8839 // Expand 8840 optionsContainer.style.display = 'block' 8841 this.setAttribute('aria-expanded', 'true') 8842 // Show minus icon (expanded) 8843 if (icon) { 8844 icon.innerHTML = ` 8845 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 8846 <path d="M5 12h14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 8847 </svg> 8848 ` 8849 } 8850 } 8851 } 8852 }) 8853 }) 8854 }, 8855 8856 setupSidebarCheckboxHandlers: function () { 8857 // Use the existing drawer checkbox handler logic 8858 this.setupDrawerCheckboxHandlers() 8859 }, 8860 8861 setupSidebarActionButtons: function () { 8862 // Use the existing drawer action buttons logic 8863 this.setupDrawerActionButtons() 8864 }, 8865 8866 resetSidebarFilters: function () { 8867 // Use the existing drawer reset logic 8868 this.resetDrawerFilters() 8869 }, 8870 8871 getSidebarFilterCSS: function () { 8872 return ` 8873 /* Sidebar-specific overrides - leverages existing drawer CSS */ 8874 .pf-sidebar-filters { 8875 background: white; 8876 border-radius: 8px; 8877 padding: 20px; 8878 } 8879 8880 .pf-sidebar-header h3 { 8881 margin: 0 0 16px 0; 8882 font-size: 18px; 8883 font-weight: 600; 8884 color: #333; 8885 } 8886 8887 /* Override drawer styles for sidebar */ 8888 .pf-sidebar-filters .pf-filter-group { 8889 margin-bottom: 8px; 8890 padding-bottom: 8px; 8891 } 8892 8893 .pf-sidebar-filters .pf-filter-group:last-child { 8894 border-bottom: none; 8895 margin-bottom: 0; 8896 } 8897 8898 .pf-sidebar-filters .pf-filter-group-title { 8899 font-size: 14px; 8900 padding: 8px 0; 8901 } 8902 8903 .pf-sidebar-filters .pf-filter-option { 8904 padding: 0.25rem 1rem; 8905 } 8906 8907 .pf-sidebar-filters .pf-filter-options-container { 8908 padding-top: 0.5rem; 8909 } 8910 8911 .pf-sidebar-filters .pf-accordion-icon { 8912 width: 20px; 8913 height: 20px; 8914 margin-left: 8px; 8915 } 8916 8917 /* Handle long checkbox labels with proper text wrapping */ 8918 .pf-sidebar-filters .pf-filter-option label { 8919 white-space: normal; 8920 word-wrap: break-word; 8921 overflow-wrap: break-word; 8922 hyphens: auto; 8923 line-height: 1.4; 8924 padding-right: 8px; 8925 } 8926 ` 8927 }, 8928 8929 createDrawerElements: function (data) { 8930 // Create drawer HTML with backdrop 8931 const drawerHTML = ` 8932 <div id="pfDrawerBackdrop" class="pf-drawer-backdrop" role="presentation" aria-hidden="true"></div> 8933 <div id="pfFilterDrawer" class="pf-drawer" role="dialog" aria-modal="true" aria-labelledby="pfDrawerTitle" aria-describedby="pfDrawerDescription" aria-hidden="true"> 8934 <div class="pf-drawer-header"> 8935 <h3 id="pfDrawerTitle">${data.i18n?.filterBy || 'Filtrera efter'}</h3> 8936 <div id="pfDrawerDescription" class="sr-only">Välj filteralternativ för att begränsa produktlistan. Använd tab för att navigera mellan alternativen.</div> 8937 <button id="pfCloseFilterDrawerBtn" class="pf-drawer-close-btn" aria-label="Stäng filterdialog" type="button"> 8938 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 8939 <path d="M6 18L18 6M6 6l12 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 8940 </svg> 8941 </button> 8942 </div> 8943 <div id="pfDrawerContent" class="pf-drawer-content" role="region" aria-label="Filteralternativ" aria-live="polite"> 8944 <!-- Filter options will be populated here --> 8945 </div> 8946 <div class="pf-drawer-footer"> 8947 <button id="pfClearFiltersBtn" class="${this.config.styling.classes.clearFiltersButton}" type="button" aria-describedby="pfClearDescription">${data.i18n?.clearAllFilters || 'Rensa alla filter'}</button> 8948 <div id="pfClearDescription" class="sr-only">Tar bort alla valda filter och återställer till ursprungsläge.</div> 8949 <button id="pfApplyFiltersBtn" class="pf-drawer-apply-btn" type="button" aria-describedby="pfApplyDescription">${data.i18n?.showProducts || 'Visa produkter'}</button> 8950 <div id="pfApplyDescription" class="sr-only">Tillämpar valda filter och visar matchande produkter.</div> 8951 </div> 8952 </div> 8953 ` 8954 8955 // Create a temporary div to parse the HTML 8956 const tempDiv = document.createElement('div') 8957 tempDiv.innerHTML = drawerHTML 8958 8959 // Append both backdrop and drawer directly to body for highest stacking context 8960 const backdrop = tempDiv.querySelector('#pfDrawerBackdrop') 8961 const drawer = tempDiv.querySelector('#pfFilterDrawer') 8962 8963 document.body.appendChild(backdrop) 8964 document.body.appendChild(drawer) 8965 }, 8966 8967 populateDrawerContent: function (filters) { 8968 const drawerContent = document.getElementById('pfDrawerContent') 8969 if (!drawerContent) return 8970 8971 drawerContent.innerHTML = '' 8972 8973 // Count total filter options across all categories 8974 const totalFilterOptions = filters.reduce((total, filter) => total + filter.values.length, 0) 8975 const shouldExpandAll = totalFilterOptions <= 10 8976 // Auto-expand if there's only one filter (drawer mode only)
8977 const hasOnlyOneFilter = filters.length === 1 8978 8979 filters.forEach(filter => { 8980 const groupDiv = document.createElement('div') 8981 groupDiv.className = this.config.styling.classes.filterGroup 8982 8983 // Check if this filter group has any active selections 8984 const hasActiveSelections = this.currentSelections[filter.id] && this.currentSelections[filter.id].length > 0 8985 const shouldStartExpanded = shouldExpandAll || hasActiveSelections || hasOnlyOneFilter 8986 8987 const titleElement = document.createElement('button') 8988 titleElement.className = 'pf-filter-group-title' 8989 titleElement.textContent = filter.title 8990 titleElement.setAttribute('aria-expanded', shouldStartExpanded ? 'true' : 'false') 8991 titleElement.setAttribute('aria-controls', `pf-filter-group-${filter.id}`) 8992 titleElement.setAttribute('type', 'button') 8993 8994 const iconSpan = document.createElement('span') 8995 iconSpan.className = 'pf-accordion-icon' 8996 iconSpan.innerHTML = shouldStartExpanded ? ` 8997 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 8998 <path d="M5 12h14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 8999 </svg> 9000 ` : ` 9001 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 9002 <path d="M12 4.5v15m7.5-7.5h-15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 9003 </svg> 9004 ` 9005 titleElement.appendChild(iconSpan) 9006 9007 groupDiv.appendChild(titleElement) 9008 9009 const optionsContainer = document.createElement('div') 9010 optionsContainer.className = 'pf-filter-options-container' 9011 optionsContainer.id = `pf-filter-group-${filter.id}` 9012 optionsContainer.style.display = shouldStartExpanded ? 'block' : 'none' 9013 9014 filter.values.forEach(value => { 9015 const optionDiv = document.createElement('div') 9016 optionDiv.className = this.config.styling.classes.filterOption 9017 9018 const checkbox = document.createElement('input') 9019 checkbox.type = 'checkbox' 9020 checkbox.className = this.config.styling.classes.filterOptionCheckbox 9021 checkbox.id = `pf-filter-${filter.id}-${value.id}` 9022 checkbox.value = value.id 9023 checkbox.dataset.filterId = filter.id 9024 checkbox.dataset.filterValueTitle = value.title 9025 checkbox.setAttribute('aria-describedby', `pf-filter-${filter.id}-${value.id}-description`) 9026 9027 const label = document.createElement('label') 9028 label.className = this.config.styling.classes.filterOptionLabel 9029 label.htmlFor = checkbox.id 9030 label.textContent = value.title 9031 label.setAttribute('aria-hidden', 'true') // We'll use the checkbox's accessible name 9032 9033 const description = document.createElement('div') 9034 description.id = `pf-filter-${filter.id}-${value.id}-description` 9035 description.className = 'sr-only' 9036 description.textContent = `Välj ${value.title} för att filtrera produkter i kategorin ${filter.title}` 9037 9038 optionDiv.appendChild(checkbox) 9039 optionDiv.appendChild(label) 9040 optionDiv.appendChild(description) 9041 optionsContainer.appendChild(optionDiv) 9042 }) 9043 9044 groupDiv.appendChild(optionsContainer) 9045 drawerContent.appendChild(groupDiv) 9046 }) 9047 9048 // Add accordion functionality 9049 drawerContent.querySelectorAll('.pf-filter-group-title').forEach(title => { 9050 title.addEventListener('click', function () { 9051 const optionsContainer = this.nextElementSibling 9052 const icon = this.querySelector('.pf-accordion-icon') 9053 const isExpanded = this.getAttribute('aria-expanded') === 'true' 9054 9055 if (optionsContainer && optionsContainer.classList.contains('pf-filter-options-container')) { 9056 if (isExpanded) { 9057 // Collapse 9058 optionsContainer.style.display = 'none' 9059 this.setAttribute('aria-expanded', 'false') 9060 // Show plus icon (collapsed) 9061 if (icon) { 9062 icon.innerHTML = `
9063 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 9064 <path d="M12 4.5v15m7.5-7.5h-15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 9065 </svg> 9066 ` 9067 } 9068 } else { 9069 // Expand 9070 optionsContainer.style.display = 'block' 9071 this.setAttribute('aria-expanded', 'true') 9072 // Show minus icon (expanded) 9073 if (icon) { 9074 icon.innerHTML = ` 9075 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 9076 <path d="M5 12h14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 9077 </svg> 9078 ` 9079 } 9080 } 9081 } 9082 }) 9083 }) 9084 }, 9085 9086 injectDrawerFilters: function (drawerContainer) { 9087 let container = document.querySelector(this.config.filterElementTargetSelector) 9088 9089 if (container) { 9090 container.appendChild(drawerContainer) 9091 return 9092 } 9093 9094 // Try common filter container selectors 9095 const fallbackSelectors = [ 9096 '.filter-container', 9097 '.filters', 9098 '#filters', 9099 '.product-filters', 9100 '.category-filters', 9101 '[data-filter-container]', 9102 '.products-header', 9103 '.category-header' 9104 ] 9105 9106 for (const selector of fallbackSelectors) { 9107 container = document.querySelector(selector) 9108 if (container) { 9109 container.appendChild(drawerContainer) 9110 return 9111 } 9112 } 9113 9114 // Try to find a good spot before the product list 9115 const productList = document.querySelector('#productList, .product-list, .products, [data-qb-selector="product-list"]') 9116 if (productList) { 9117 productList.parentNode.insertBefore(drawerContainer, productList) 9118 return 9119 } 9120 9121 // Last resort - inject at top of main content or body 9122 const main = document.querySelector('main, .main, .content, .page-content') 9123 if (main) { 9124 main.insertBefore(drawerContainer, main.firstChild) 9125 } else { 9126 document.body.insertBefore(drawerContainer, document.body.firstChild) 9127 } 9128 }, 9129 9130 setupDrawerFilterHandlers: function (data) { 9131 // Initialize state variables 9132 this.currentSelections = this.parseUrlFilters() 9133 this.pendingSelections = { ...this.currentSelections } 9134 9135 // Populate drawer content 9136 this.populateDrawerContent(data.filters) 9137 9138 // Set up event handlers 9139 this.setupDrawerToggles() 9140 this.setupDrawerCheckboxHandlers() 9141 this.setupDrawerActionButtons() 9142 9143 9144 // Apply initial selections from URL 9145 this.applyInitialSelections() 9146 this.updateActiveFiltersDisplay() 9147 9148 // Apply filtering if we have selections and should use client-side filtering 9149 if (this.shouldUseClientSideFiltering() && Object.keys(this.currentSelections).length > 0) { 9150 this.applyClientSideFiltering() 9151 } 9152 }, 9153 9154 parseUrlFilters: function () { 9155 const urlParams = new URLSearchParams(window.location.search) 9156 const filters = {} 9157 9158 for (const [key, value] of urlParams.entries()) { 9159 if (key.startsWith('filter_')) { 9160 const filterId = key.replace('filter_', '') 9161 filters[filterId] = value.split(',') 9162 } 9163 } 9164 9165 return filters 9166 }, 9167 9168 9169 setupDrawerToggles: function () { 9170 const openDrawerBtn = document.getElementById('pfFilterButton') 9171 const closeDrawerBtn = document.getElementById('pfCloseFilterDrawerBtn') 9172 const drawer = document.getElementById('pfFilterDrawer') 9173 const backdrop = document.getElementById('pfDrawerBackdrop') 9174 9175 if (openDrawerBtn && drawer && backdrop) { 9176 openDrawerBtn.addEventListener('click', (event) => { 9177 event.stopPropagation() 9178 drawer.classList.add('pf-open') 9179 backdrop.classList.add('pf-open') 9180 document.body.classList.add('pf-drawer-open') 9181 9182 // Update ARIA states 9183 openDrawerBtn.setAttribute('aria-expanded', 'true') 9184 drawer.setAttribute('aria-hidden', 'false') 9185 backdrop.setAttribute('aria-hidden', 'false') 9186 9187 // Focus management - focus first interactive element in drawer 9188 setTimeout(() => { 9189 const firstFocusable = drawer.querySelector('button, input, [tabindex]:not
9189([tabindex="-1"])') 9190 if (firstFocusable) { 9191 firstFocusable.focus() 9192 } 9193 }, 100) 9194 }) 9195 } 9196 9197 if (closeDrawerBtn && drawer && backdrop) { 9198 closeDrawerBtn.addEventListener('click', (event) => { 9199 event.stopPropagation() 9200 this.closeDrawer() 9201 }) 9202 } 9203 9204 // Close drawer when clicking on backdrop 9205 if (backdrop) { 9206 backdrop.addEventListener('click', (event) => { 9207 event.stopPropagation() 9208 this.closeDrawer() 9209 }) 9210 } 9211 9212 // Close drawer when clicking outside 9213 this.handleDocumentClick = (event) => { 9214 if (drawer && drawer.classList.contains('pf-open') && 9215 !drawer.contains(event.target) && 9216 !openDrawerBtn?.contains(event.target)) { 9217 this.closeDrawer() 9218 } 9219 } 9220 document.addEventListener('click', this.handleDocumentClick) 9221 9222 // Prevent drawer clicks from closing it 9223 if (drawer) { 9224 drawer.addEventListener('click', (event) => { 9225 event.stopPropagation() 9226 }) 9227 } 9228 9229 // Add keyboard navigation support 9230 this.handleDocumentKeydown = (event) => { 9231 if (drawer && drawer.classList.contains('pf-open')) { 9232 // Close drawer with Escape key 9233 if (event.key === 'Escape') { 9234 this.closeDrawer() 9235 } 9236 9237 // Trap focus within drawer when open 9238 if (event.key === 'Tab') { 9239 const focusableElements = drawer.querySelectorAll( 9240 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])' 9241 ) 9242 const firstFocusable = focusableElements[0] 9243 const lastFocusable = focusableElements[focusableElements.length - 1] 9244 9245 if (event.shiftKey) { 9246 // Shift + Tab 9247 if (document.activeElement === firstFocusable) { 9248 event.preventDefault() 9249 lastFocusable.focus() 9250 } 9251 } else { 9252 // Tab 9253 if (document.activeElement === lastFocusable) { 9254 event.preventDefault() 9255 firstFocusable.focus() 9256 } 9257 } 9258 } 9259 } 9260 } 9261 document.addEventListener('keydown', this.handleDocumentKeydown) 9262 }, 9263 9264 setupDrawerCheckboxHandlers: function () { 9265 this.handleCheckboxChange = (event) => { 9266 if (event.target.matches('input[type="checkbox"]') && event.target.closest('.pf-filter-option')) { 9267 const checkbox = event.target 9268 const filterId = checkbox.getAttribute('data-filter-id') 9269 const value = checkbox.value 9270 9271 if (!this.pendingSelections[filterId]) { 9272 this.pendingSelections[filterId] = [] 9273 } 9274 9275 if (checkbox.checked) { 9276 if (!this.pendingSelections[filterId].includes(value)) { 9277 this.pendingSelections[filterId].push(value) 9278 } 9279 } else { 9280 this.pendingSelections[filterId] = this.pendingSelections[filterId].filter(v => v !== value) 9281 if (this.pendingSelections[filterId].length === 0) { 9282 delete this.pendingSelections[filterId] 9283 } 9284 } 9285 9286 // Check if we're in sidebar mode (immediate filtering) or drawer mode (pending selections) 9287 const isSidebar = event.target.closest('#pfSidebarContent') 9288 9289 if (isSidebar || this.shouldUseClientSideFiltering()) { 9290 // Sidebar or client-side filtering: apply immediately 9291 this.currentSelections = { ...this.pendingSelections } 9292 this.updateActiveFiltersDisplay() 9293 this.updateUrlAndFilter() 9294 } 9295 // Otherwise, for drawer design with pagination (multiple pages), only update pending selections 9296 // and wait for "Visa produkter" button 9297 } 9298 } 9299 document.addEventListener('change', this.handleCheckboxChange) 9300 }, 9301 9302 setupDrawerActionButtons: function () { 9303 // Use a small delay to ensure DOM elements are ready 9304 setTimeout(() => { 9305 const applyBtn = document.getElementById('pfApplyFiltersBtn') 9306 const resetBtn = document.getElementById('pfResetAllFilters') 9307 const clearBtn = document.getElementById('pfClearFiltersBtn') 9308
9309 if (applyBtn) { 9310 applyBtn.addEventListener('click', (event) => { 9311 event.preventDefault() 9312 event.stopPropagation() 9313 this.applyDrawerFilters() 9314 }) 9315 } else { 9316 console.warn('Apply filters button not found') 9317 } 9318 9319 if (resetBtn) { 9320 resetBtn.addEventListener('click', (event) => { 9321 event.preventDefault() 9322 event.stopPropagation() 9323 this.resetDrawerFilters() 9324 }) 9325 } else { 9326 console.warn('Reset filters button not found') 9327 } 9328 9329 if (clearBtn) { 9330 clearBtn.addEventListener('click', (event) => { 9331 event.preventDefault() 9332 event.stopPropagation() 9333 this.clearDrawerFilters() 9334 }) 9335 } else { 9336 console.warn('Clear filters button not found') 9337 } 9338 }, 100) 9339 9340 // Keep the delegated event handler for filter badge removal 9341 document.addEventListener('click', (event) => { 9342 // Remove individual filter badge 9343 if (event.target.matches('.pf-filter-badge-remove') || event.target.closest('.pf-filter-badge-remove')) { 9344 const removeBtn = event.target.matches('.pf-filter-badge-remove') ? event.target : event.target.closest('.pf-filter-badge-remove') 9345 const filterId = removeBtn.getAttribute('data-filter-id') 9346 const value = removeBtn.getAttribute('data-filter-value') 9347 this.removeFilterValue(filterId, value) 9348 return 9349 } 9350 }) 9351 }, 9352 9353 9354 9355 resetDrawerFilters: function () { 9356 this.currentSelections = {} 9357 this.pendingSelections = {} 9358 9359 // Uncheck all checkboxes 9360 const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked') 9361 checkboxes.forEach(checkbox => { 9362 checkbox.checked = false 9363 }) 9364 9365 this.updateActiveFiltersDisplay() 9366 this.updateUrlAndFilterForClear() 9367 this.closeDrawer() 9368 }, 9369 9370 clearDrawerFilters: function () { 9371 // Clear pending selections (what's currently selected in the drawer) 9372 this.pendingSelections = {} 9373 9374 // Uncheck all checkboxes in the drawer 9375 const checkboxes = document.querySelectorAll('#pfFilterDrawer input[type="checkbox"]:checked') 9376 checkboxes.forEach(checkbox => { 9377 checkbox.checked = false 9378 }) 9379 9380 // Apply the cleared filters immediately 9381 this.currentSelections = {} 9382 this.updateActiveFiltersDisplay() 9383 this.updateUrlAndFilterForClear() 9384 this.closeDrawer() 9385 }, 9386 9387 applyDrawerFilters: function () { 9388 // Apply all pending selections 9389 this.currentSelections = { ...this.pendingSelections } 9390 this.updateActiveFiltersDisplay() 9391 this.updateUrlAndFilter() 9392 9393 // Apply client-side filtering if we should use it 9394 if (this.shouldUseClientSideFiltering()) { 9395 this.applyClientSideFiltering() 9396 } 9397 9398 this.closeDrawer() 9399 }, 9400 9401 closeDrawer: function () { 9402 const drawer = document.getElementById('pfFilterDrawer') 9403 const backdrop = document.getElementById('pfDrawerBackdrop') 9404 const openDrawerBtn = document.getElementById('pfFilterButton') 9405 9406 if (drawer) { 9407 drawer.classList.remove('pf-open') 9408 } 9409 9410 if (backdrop) { 9411 backdrop.classList.remove('pf-open') 9412 } 9413 9414 document.body.classList.remove('pf-drawer-open') 9415 9416 // Update ARIA states 9417 if (openDrawerBtn) { 9418 openDrawerBtn.setAttribute('aria-expanded', 'false') 9419 } 9420 if (drawer) { 9421 drawer.setAttribute('aria-hidden', 'true') 9422 } 9423 if (backdrop) { 9424 backdrop.setAttribute('aria-hidden', 'true') 9425 } 9426 9427 // Return focus to the filter button 9428 if (openDrawerBtn) { 9429 openDrawerBtn.focus() 9430 } 9431 }, 9432 9433 9434 updateUrlAndFilter: function () { 9435 const url = new URL(location.href) 9436 9437 // Clear existing filter params 9438 const paramKeys = [] 9439 for (const key of url.searchParams.keys()) { 9440 if (key.startsWith('filter_')) { 9441 paramKeys.push(key) 9442 } 9443 } 9444 paramKeys.forEach(key => url.searchParams.delete(key)) 9445 9446 // Add current filter params 9447 Object.keys(this.currentSelections).forEach(filterId => { 9448 const values = this.currentSelections[filterId] 9449 if (values && values.length > 0) { 9450 url.searchParams.set('filter_' + filterId, values.join(',')) 9451 } 9452 }) 9453 9454 url.searchParams.delete('page') 9455 9456 // Handle pagination vs client-side filtering 9457 if (this.shouldUseClientSideFiltering()) { 9458 history.pushState({}, '', url.toString()) 9459 this.applyClientSideFiltering() 9460 } else { 9461 window.location.href = url.href 9462 } 9463 }, 9464 9465 updateUrlAndFilterForClear: function () { 9466 const url = new URL(location.href) 9467 9468 // Clear all filter params 9469 const paramKeys = [] 9470 for (const key of url.searchParams.keys()) { 9471 if (key.startsWith('filter_')) { 9472 paramKeys.push(key) 9473 } 9474 } 9475 paramKeys.forEach(key => url.searchParams.delete(key)) 9476 9477 url.searchParams.delete('page') 9478 9479 // When clearing filters, always reload if pagination is activated 9480 // because clearing filters might change the total number of products and pages 9481 const pagination = qs_store_apps_data?.pagination || {} 9482 if (pagination.pagination_activated) { 9483 // Always reload page when pagination is activated and filters are cleared 9484 window.location.href = url.href 9485 } else { 9486 // Use client-side filtering when pagination is not activated 9487 history.pushState({}, '', url.toString()) 9488 this.applyClientSideFiltering() 9489 } 9490 }, 9491 9492 applyClientSideFiltering: function () { 9493 // Since products are directly in the page, find them directly 9494 const productElements = document.querySelectorAll('[data-qb-selector="product-item"]') 9495 9496 if (productElements.length === 0) { 9497 return 9498 } 9499
9500 productElements.forEach((productElement) => { 9501 let shouldShow = true 9502 9503 // Check each filter category 9504 Object.keys(this.currentSelections).forEach(filterId => { 9505 const selectedValues = this.currentSelections[filterId] 9506 if (!selectedValues || selectedValues.length === 0) { 9507 return // Skip empty filters 9508 } 9509 9510 // Check if product has any of the selected values for this filter 9511 let hasMatchingValue = false 9512 selectedValues.forEach(value => { 9513 const dataAttribute = `data-filter-${filterId}-${value}` 9514 if (productElement.hasAttribute(dataAttribute)) { 9515 hasMatchingValue = true 9516 } 9517 }) 9518 9519 // If no matching value found for this required filter, hide the product 9520 if (!hasMatchingValue) { 9521 shouldShow = false 9522 } 9523 }) 9524 9525 // Show/hide the product 9526 if (shouldShow) { 9527 productElement.classList.remove('hidden') 9528 productElement.style.display = '' // Also clear any inline style 9529 } else { 9530 productElement.classList.add('hidden') 9531 productElement.style.display = 'none' // Force hide with inline style 9532 } 9533 }) 9534 9535 // Check if any products are visible 9536 const visibleProducts = document.querySelectorAll('[data-qb-selector="product-item"]:not(.hidden)') 9537 if (visibleProducts.length === 0) { 9538 this.showNoResultsMessage() 9539 } else { 9540 // Hide any existing "no results" message 9541 const existingNoResults = document.querySelector(`.${this.config.styling.classes.noResults}`) 9542 if (existingNoResults) { 9543 existingNoResults.remove() 9544 } 9545 } 9546 }, 9547 9548 showNoResultsMessage: function () { 9549 // Remove any existing "no results" message 9550 const existingNoResults = document.querySelector(`.${this.config.styling.classes.noResults}`) 9551 if (existingNoResults) { 9552 existingNoResults.remove() 9553 } 9554 9555 // Find the product container 9556 const productContainer = document.querySelector('#productList, .product-list, .products, [data-qb-selector="product-list"]') 9557 if (!productContainer) return 9558 9559 // Get translation data 9560 const data = window.qs_store_apps?.productfilters || {} 9561 9562 // Create "no results" message 9563 const noResultsDiv = document.createElement('div') 9564 noResultsDiv.className = this.config.styling.classes.noResults 9565 noResultsDiv.innerHTML = ` 9566 <div class="${this.config.styling.classes.noResultsContainer}"> 9567 <h3>${data.i18n?.noResultsHeading || 'No products found'}</h3> 9568 <p>${data.i18n?.noResultsDescription || 'Try adjusting your filters to see more products.'}</p> 9569 <button onclick="window.apps.QuickbutikProductFilter.resetDrawerFilters()" class="pf-clear-filters-btn">${data.i18n?.clearAllFilters || 'Clear all filters'}</button> 9570 </div> 9571 ` 9572 9573 // Insert the message after the product container 9574 productContainer.appendChild(noResultsDiv) 9575 }, 9576 9577 updateActiveFiltersDisplay: function () { 9578 const container = document.getElementById('pfActiveFilters') 9579 if (!container) return 9580 9581 // Clear all badges 9582 const badges = container.querySelectorAll('.pf-filter-badge') 9583 badges.forEach(badge => badge.remove()) 9584 9585 const data = window.qs_store_apps.productfilters 9586 let totalActiveFilters = 0 9587 9588 Object.keys(this.currentSelections).forEach(filterId => { 9589 const values = this.currentSelections[filterId] 9590 if (!values || values.length === 0) return 9591 9592 const filterData = data.filters.find(f => f.id === filterId) 9593 if (!filterData) return 9594 9595 values.forEach(value => { 9596 const valueData = filterData.values.find(v => v.id === value) 9597 if (!valueData) return 9598 9599 totalActiveFilters++ 9600 9601 const badge = document.createElement('div') 9602 badge.className = this.config.styling.classes.filterBadge 9603 badge.setAttribute('role', 'button') 9604 badge.setAttribute('tabindex', '0') 9605 // XSS Protection: Sanitize user content 9606 const sanitizedTitle = this.sanitizeHTML(valueData.title) 9607 badge.setAttribute('aria-label', `Ta bort filter: ${sanitizedTitle}`) 9608 badge.innerHTML = `
9609 <span>${sanitizedTitle}</span> 9610 <span class="${this.config.styling.classes.filterBadgeRemove}" data-filter-id="${filterId}" data-filter-value="${value}" role="button" tabindex="0" aria-label="Ta bort ${sanitizedTitle} filter"> 9611 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 9612 <path d="M6 18L18 6M6 6l12 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> 9613 </svg> 9614 </span> 9615 ` 9616 container.appendChild(badge) 9617 }) 9618 }) 9619 9620 // Update filter button badge 9621 this.updateFilterButtonBadge(totalActiveFilters) 9622 9623 // Show/hide reset button 9624 const resetBtn = document.getElementById('pfResetAllFilters') 9625 9626 if (resetBtn) { 9627 const badgeCount = container.querySelectorAll('.pf-filter-badge').length 9628 const shouldShow = badgeCount > 0 9629 resetBtn.style.display = shouldShow ? 'block' : 'none' 9630 } 9631 }, 9632 9633 updateFilterButtonBadge: function (count) { 9634 const badge = document.getElementById('pfFilterButtonBadge') 9635 if (!badge) return 9636 9637 if (count > 0) { 9638 badge.textContent = count 9639 badge.style.display = 'flex' 9640 badge.setAttribute('aria-label', `${count} aktiva filter`) 9641 } else { 9642 badge.style.display = 'none' 9643 } 9644 }, 9645 9646 applyInitialSelections: function () { 9647 Object.keys(this.currentSelections).forEach(filterId => { 9648 const values = this.currentSelections[filterId] 9649 9650 values.forEach(value => { 9651 const checkboxId = `pf-filter-${filterId}-${value}` 9652 const checkbox = document.getElementById(checkboxId) 9653 if (checkbox) { 9654 checkbox.checked = true 9655 } else { 9656 console.warn(`Checkbox not found: ${checkboxId}`) 9657 } 9658 }) 9659 }) 9660 9661 // Sync pending selections with current selections 9662 this.pendingSelections = { ...this.currentSelections } 9663 }, 9664 9665 9666 removeFilterValue: function (filterId, value) { 9667 if (!this.currentSelections[filterId]) return 9668 9669 this.currentSelections[filterId] = this.currentSelections[filterId].filter(v => v !== value) 9670 if (this.currentSelections[filterId].length === 0) { 9671 delete this.currentSelections[filterId] 9672 } 9673 9674 // Update pending selections and UI 9675 this.pendingSelections = { ...this.currentSelections } 9676 this.updateActiveFiltersDisplay() 9677 this.updateUrlAndFilter() 9678 9679 // Update checkbox 9680 const checkbox = document.getElementById(`pf-filter-${filterId}-${value}`) 9681 if (checkbox) { 9682 checkbox.checked = false 9683 } 9684 }, 9685 9686 getDrawerFilterCSS: function () { 9687 return ` 9688 /* Color variables for consistency */ 9689 :root { 9690 --pf-primary-color: var(--color-primary, #000000); 9691 --pf-primary-dark: var(--color-primary, #000000); 9692 --pf-light-gray: var(--input-bg, var(--bg-header, #f8f8f8)); 9693 --pf-border-color: var(--color-text, #e2e2e2); 9694 --pf-badge-bg: var(--button-bg, var(--color-primary, #000000)); 9695 --pf-badge-border-color: var(--color-primary, #e2e2e2); 9696 --pf-text-dark: var(--color-text, var(--color-foreground, #333333)); 9697 --pf-text-light: var(--button-text-primary, #ffffff); 9698 --pf-button-rounding: var(--button-rounding, 0.5rem); 9699 --pf-button-py: calc(var(--button-py, 1rem) * 0.75); 9700 --pf-button-px: calc(var(--button-px, 2rem) * 0.75); 9701 --pf-input-rounding: var(--input-rounding, 0.5rem); 9702 --pf-gap: var(--gap, 1rem); 9703 --pf-drawer-bg: var(--bg-card, var(--bg-body, #ffffff)); 9704 } 9705 9706 /* Basic Reset & Body Style */ 9707 body.pf-drawer-open { 9708 /* Allow scrolling when drawer is open */ 9709 } 9710 9711 .pf-filter-button-container { 9712 display: flex; 9713 flex-wrap: wrap; 9714 align-items: center; 9715 gap: var(--pf-gap); 9716 } 9717 9718 .pf-filter-button { 9719 padding: var(--pf-button-py) var(--pf-button-px); 9720 cursor: pointer; 9721 background-color: var(--pf-light-gray); 9722 border-color: var(--pf-border-color); 9723 color: var(--pf-text-dark); 9724 border-radius: var(--pf-button-rounding); 9725 transition: all 0.2s ease; 9726 display: inline-flex; 9727 align-items: center; 9728 gap: 8px; 9729 } 9730 9731 .pf-filter-icon { 9732 flex-shrink: 0; 9733 opacity: 0.8; 9734 } 9735 9736 .pf-filter-button:hover { 9737 background-color: var(--pf-primary-color); 9738 color: var(--pf-text-light); 9739 border-color: var(--pf-primary-color); 9740 } 9741 9742 .pf-filter-button-badge { 9743 background-color: var(--pf-primary-color); 9744 } 9745
9746 /* Active filters display container */ 9747 .pf-active-filters { 9748 display: flex; 9749 gap: var(--pf-gap); 9750 align-items: center; 9751 } 9752 9753 .pf-filter-badge { 9754 display: inline-flex; 9755 align-items: center; 9756 padding: var(--pf-button-py) var(--pf-button-px); 9757 border: 1px solid var(--pf-badge-border-color); 9758 background-color: var(--pf-badge-bg); 9759 color: white; 9760 border-radius: var(--pf-button-rounding); 9761 font-size: 1rem; 9762 font-weight: 500; 9763 transition: all 0.2s ease; 9764 } 9765 9766 .pf-filter-badge-remove { 9767 margin-left: 10px; 9768 width: 20px; 9769 height: 20px; 9770 display: inline-flex; 9771 align-items: center; 9772 justify-content: center; 9773 cursor: pointer; 9774 font-weight: bold; 9775 border-radius: 50%; 9776 transition: background-color 0.2s; 9777 background-color: rgba(255, 255, 255, 0.2); 9778 font-size: 1rem; 9779 } 9780 9781 .pf-filter-badge-remove:hover { 9782 background-color: rgba(255, 255, 255, 0.3); 9783 } 9784 9785 .pf-reset-all { 9786 cursor: pointer; 9787 color: var(--pf-text-dark); 9788 text-decoration: underline; 9789 font-size: 1rem; 9790 padding: 8px 14px; 9791 display: inline-flex; 9792 align-items: center; 9793 margin: 0; 9794 font-weight: 500; 9795 transition: color 0.2s; 9796 white-space: nowrap; 9797 } 9798 9799 .pf-reset-all:hover { 9800 color: var(--pf-primary-color); 9801 } 9802 9803 /* Hide when no active filters */ 9804 .pf-active-filters:empty+.pf-reset-all { 9805 display: none; 9806 } 9807 9808 /* Drawer backdrop */ 9809 .pf-drawer-backdrop { 9810 position: fixed; 9811 top: 0; 9812 left: 0; 9813 width: 100%; 9814 height: 100%; 9815 z-index: 9998; 9816 opacity: 0; 9817 visibility: hidden; 9818 transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out; 9819 } 9820 9821 .pf-drawer-backdrop.pf-open { 9822 opacity: 1; 9823 visibility: visible; 9824 } 9825 9826 /* Drawer styling */ 9827 .pf-drawer { 9828 position: fixed; 9829 top: 0; 9830 right: -350px; 9831 width: 350px; 9832 height: 100%; 9833 background-color: var(--pf-drawer-bg); 9834 box-shadow: -2px 0 10px rgba(0, 0, 0, 0.15); 9835 transition: right 0.3s ease-in-out; 9836 z-index: 9999; 9837 display: flex; 9838 flex-direction: column; 9839 overflow-x: hidden; 9840 } 9841 9842 .pf-drawer.pf-open { 9843 right: 0; 9844 } 9845 9846 .pf-drawer-header { 9847 padding: 16px; 9848 border-bottom: 1px solid var(--pf-border-color); 9849 display: flex; 9850 justify-content: space-between; 9851 align-items: center; 9852 } 9853 9854 .pf-drawer-header h3 { 9855 margin: 0; 9856 font-size: 1.2rem; 9857 font-weight: 600; 9858 } 9859 9860 .pf-drawer-close-btn { 9861 background: none; 9862 border: none; 9863 font-size: 1.5rem; 9864 cursor: pointer; 9865 width: 40px; 9866 height: 40px; 9867 display: flex; 9868 align-items: center; 9869 justify-content: center; 9870 border-radius: 50%; 9871 transition: background-color 0.2s; 9872 color: var(--pf-text-dark); 9873 font-weight: bold; 9874 line-height: 1; 9875 } 9876 9877 .pf-drawer-close-btn:hover { 9878 background-color: var(--pf-light-gray); 9879 color: var(--pf-primary-color); 9880 } 9881 9882 .pf-drawer-content { 9883 padding: 16px; 9884 overflow-y: auto; 9885 overflow-x: hidden; 9886 flex-grow: 1; 9887 max-width: 100%; 9888 } 9889 9890 .pf-drawer-footer { 9891 padding: 16px; 9892 border-top: 1px solid var(--pf-border-color); 9893 text-align: center; 9894 display: flex; 9895 flex-direction: column; 9896 gap: 12px; 9897 } 9898 9899 .pf-drawer-apply-btn { 9900 padding: 12px 20px; 9901 font-size: 1rem; 9902 cursor: pointer; 9903 background-color: var(--pf-primary-color); 9904 color: white; 9905 border: none; 9906 border-radius: 25px; 9907 width: 100%; 9908 font-weight: 500; 9909 transition: background-color 0.2s; 9910 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 9911 } 9912 9913 .pf-drawer-apply-btn:hover { 9914 background-color: var(--pf-primary-dark); 9915 } 9916 9917 .pf-clear-filters-btn { 9918 background: none; 9919 border: none; 9920 color: var(--pf-text-dark); 9921 opacity: 0.7; 9922 transition: opacity 0.2s ease; 9923 } 9924 9925 .pf-clear-filters-btn:hover { 9926 opacity: 1; 9927 color: var(--pf-primary-color); 9928 } 9929 9930 /* Styling for filter groups and options in drawer */ 9931 .pf-filter-group { 9932 border-bottom: 1px solid var(--pf-border-color); 9933 } 9934 9935 .pf-filter-group:last-child { 9936 border-bottom: none; 9937 margin-bottom: 0; 9938 padding-bottom: 0; 9939 } 9940 9941 .pf-filter-group-title { 9942 margin-top: 0; 9943 font-size: 1.1rem; 9944 padding: 10px 0; 9945 cursor: pointer; 9946 display: flex; 9947 justify-content: space-between; 9948 align-items: center; 9949 font-weight: 600; 9950 color: var(--pf-text-dark); 9951 border: none; 9952 background: none; 9953 width: 100%; 9954 text-align: left; 9955 } 9956 9957 .pf-accordion-icon { 9958 margin-left: 10px; 9959 width: 24px; 9960 height: 24px; 9961 display: flex; 9962 align-items: center; 9963 justify-content: center; 9964 border-radius: 50%; 9965 background-color: var(--pf-light-gray); 9966 color: var(--pf-text-dark); 9967 transition: background-color 0.2s ease; 9968 } 9969 9970 .pf-accordion-icon:hover { 9971 background-color: var(--pf-primary-color); 9972 color: white; 9973 } 9974 9975 .pf-accordion-icon svg { 9976 width: 16px; 9977 height: 16px; 9978 } 9979 9980 .pf-filter-options-container { 9981 padding-top: 0.5rem; 9982 } 9983 9984 /* Custom Styling for Filter Options (Checkboxes) */ 9985 .pf-filter-option { 9986 display: block; 9987 position: relative; 9988 padding: 0.25rem 1rem; 9989 transition: background-color 0.15s ease-in-out; 9990 } 9991 9992 .pf-filter-option:hover { 9993 background-color: var(--pf-light-gray); 9994 } 9995 9996 .pf-filter-option input[type="checkbox"] { 9997 opacity: 0; 9998 position: absolute; 9999 width: 0; 10000 height: 0; 10001 cursor: pointer; 10002 } 10003 10004 .pf-filter-option label { 10005 display: flex; 10006 align-items: center; 10007 cursor: pointer; 10008 position: relative; 10009 padding-left: 32px; 10010 transition: font-weight 0.15s ease-in-out, color 0.15s ease-in-out; 10011 } 10012 10013 /* Custom Checkbox Box */ 10014 .pf-filter-option label::before { 10015 content: ''; 10016 position: absolute; 10017 left: 0; 10018 top: 50%; 10019 transform: translateY(-50%);
10020 width: 20px; 10021 height: 20px; 10022 border: 2px solid var(--pf-border-color); 10023 border-radius: 4px; 10024 background-color: var(--pf-input-bg, transparent); 10025 transition: border-color 0.15s ease-in-out, background-color 0.15s ease-in-out; 10026 } 10027 10028 /* Custom Checkmark */ 10029 .pf-filter-option input[type="checkbox"]:checked+label::after { 10030 content: ''; 10031 position: absolute; 10032 left: 7px; 10033 top: 50%; 10034 width: 7px; 10035 height: 14px; 10036 border-style: solid; 10037 border-color: #fff; 10038 border-width: 0 3px 3px 0; 10039 transform: translateY(-65%) rotate(45deg); 10040 } 10041 10042 /* When checkbox is checked: style the custom box and label text */ 10043 .pf-filter-option input[type="checkbox"]:checked+label::before { 10044 background-color: var(--pf-primary-color); 10045 border-color: var(--pf-primary-color); 10046 } 10047 10048 .pf-filter-option input[type="checkbox"]:checked+label { 10049 font-weight: 600; 10050 } 10051 10052 /* Focus style for accessibility */ 10053 .pf-filter-option input[type="checkbox"]:focus-visible+label::before { 10054 box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 10055 } 10056 10057 /* Screen reader only class */ 10058 .sr-only { 10059 position: absolute; 10060 width: 1px; 10061 height: 1px; 10062 padding: 0; 10063 margin: -1px; 10064 overflow: hidden; 10065 clip: rect(0, 0, 0, 0); 10066 white-space: nowrap; 10067 border: 0; 10068 } 10069 10070 /* Responsive adjustments for mobile */ 10071 @media (max-width: 767px) { 10072 .pf-filter-button-container { 10073 flex-direction: column; 10074 } 10075 10076 .pf-filter-button { 10077 text-align: center; 10078 } 10079 10080 .pf-active-filters { 10081 display: none; 10082 } 10083 10084 .pf-reset-all { 10085 text-align: center; 10086 font-weight: 500; 10087 padding: 10px; 10088 margin-top: 5px; 10089 border-top: 1px solid var(--pf-border-color); 10090 } 10091 10092 .pf-drawer { 10093 width: 100%; 10094 right: -100%; 10095 } 10096 10097 .pf-drawer-backdrop { 10098 z-index: 9998; 10099 } 10100 } 10101 10102 /* No results message - break out of grid and center full width */ 10103 .pf-no-results { 10104 grid-column: 1 / -1; /* Span all grid columns */ 10105 width: 100%; 10106 display: flex; 10107 justify-content: center; 10108 align-items: center; 10109 min-height: 200px; 10110 } 10111 10112 .pf-no-results-container { 10113 text-align: center; 10114 padding: 40px 20px; 10115 max-width: 500px; 10116 width: 100%; 10117 } 10118 10119 .pf-no-results-container h3 { 10120 margin: 0 0 10px 0; 10121 font-weight: 700; 10122 } 10123 10124 .pf-no-results-container p { 10125 margin: 0 0 20px 0; 10126 line-height: 1.5; 10127 } 10128 10129 .pf-no-results-container .pf-clear-filters-btn { 10130 background: var(--color-primary, #000000); 10131 color: white; 10132 border: none; 10133 padding: 10px 20px; 10134 border-radius: 4px; 10135 cursor: pointer; 10136 transition: background-color 0.2s ease; 10137 } 10138 10139 .pf-no-results-container .pf-clear-filters-btn:hover { 10140 background: var(--color-primary, #000000); 10141 } 10142 ` 10143 }, 10144 10145 // Cleanup method to remove event listeners 10146 cleanup: function() { 10147 if (this.handleDocumentClick) { 10148 document.removeEventListener('click', this.handleDocumentClick) 10149 this.handleDocumentClick = null 10150 } 10151 if (this.handleDocumentKeydown) { 10152 document.removeEventListener('keydown', this.handleDocumentKeydown) 10153 this.handleDocumentKeydown = null 10154 } 10155 if (this.handlePageShow) { 10156 window.removeEventListener('pageshow', this.handlePageShow) 10157 this.handlePageShow = null 10158 } 10159 if (this.handleCheckboxChange) { 10160 document.removeEventListener('change', this.handleCheckboxChange) 10161 this.handleCheckboxChange = null 10162 } 10163 if (this.handleBeforeUnload) { 10164 window.removeEventListener('beforeunload', this.handleBeforeUnload) 10165 this.handleBeforeUnload = null 10166 } 10167 }, 10168 10169}) 10170 10171// Global cleanup function that can be called from anywhere 10172window.cleanupProductFilter = function() { 10173 if (window.QuickbutikProductFilter && window.QuickbutikProductFilter.cleanup) { 10174 window.QuickbutikProductFilter.cleanup() 10175 } 10176}</script>
10176<script type="text/javascript">window.registerApp({ 10177 name: 'ProductProperties', 10178 identifier: 'QuickbutikProductProperties', 10179 description: 'Show product properties', 10180 version: '1.1.0', 10181 pages: ['product'], 10182 data: {}, 10183 config: { 10184 insertTarget: { 10185 selector: '#product-properties', 10186 position: 'afterbegin', // beforebegin | afterbegin | beforeend | afterend 10187 }, 10188 fieldsAttributes: { 10189 fileInputAccept: 'image/*,application/pdf', 10190 }, 10191 styling: { 10192 formClasses: 'flex gap-4 flex-col pb-4', 10193 inputContainerClasses: 'flex flex-col gap-2', 10194 labelClasses: 'text-sm font-bold text-gray-700', 10195 labelErrorClasses: 'ml-1 text-red-500', 10196 checkboxInputClasses: 'form-checkbox h-5 w-5 text-gray-600', 10197 shortTextInputClasses: 10198 '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', 10199 longTextInputClasses: 10200 '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', 10201 fileContainerClasses: 'flex w-full input-bg input-border input-px input-py input-rounding w-full', 10202 fileInputClasses: 'input-bg input-border input-rounding input-px input-py', 10203 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', 10204 fileInputFilenameClasses: 'truncate button-py', 10205 fileUploadProgressContainerClasses: 'block relative w-full mt-2 rounded',
10206 fileUploadProgressBarBackgroundClasses: 'block absolute w-full h-full bg-black opacity-10 text-white rounded', 10207 fileUploadProgressBarClasses: 'block relative bg-primary py-1 px-4 text-xs font-bold text-white rounded', 10208 addonPriceClasses: 'text-sm text-gray-600 ml-0', 10209 }, 10210 }, 10211 onMount: function ({ appData, page }) { 10212 console.debug(`${this.identifier}@onMount`) 10213 10214 const productID = document 10215 .querySelector('[data-product-id]') 10216 .getAttribute('data-product-id') 10217 10218 if (productID) { 10219 fetch(`${qs_store_url}/apps/productproperties/view?product_id=${productID}`) 10220 .then((response) => response.json()) 10221 .then((data) => { 10222 this.data = data 10223 this.renderProductProperties() 10224 }) 10225 .catch((error) => { 10226 console.error('Error:', error); 10227 }) 10228 } 10229 10230 }, 10231 generageInputId: function (id) { 10232 return `ProductProperties-${id}` 10233 }, 10234 generateInputName: function (id) { 10235 return `product_properties_${id}` 10236 }, 10237 renderProductProperties: function () { 10238 const productPropertiesContainer = document.querySelector(this.data.identifier) 10239 10240 const insertPosition = { 10241 before: 'beforebegin', 10242 after: 'afterend', 10243 in: 'afterbegin', 10244 }[this.data.apply] 10245 10246 if (productPropertiesContainer && insertPosition && this.data.properties) { 10247 const form = document.createElement('form') 10248 form.classList.add(...this.config.styling.formClasses.split(' ')) 10249 form.setAttribute('id', 'ProductPropertiesForm') 10250 10251 this.data.properties.forEach((property) => { 10252 const [key] = 10253 Object.entries(property) 10254 .filter(([key]) => 10255 ['isCheckbox', 'isFile', 'isLongtext', 'isShorttext', 'isAddonProduct'].includes( 10256 key 10257 ) 10258 ) 10259 .find(([_, value]) => value) || [] 10260 10261 let input = null 10262 10263 switch (key) { 10264 case 'isCheckbox': 10265 input = this.renderCheckbox(property) 10266 break 10267 case 'isFile': 10268 input = this.renderFile(property) 10269 break 10270 case 'isLongtext': 10271 input = this.renderLongtext(property) 10272 break 10273 case 'isShorttext': 10274 input = this.renderShorttext(property) 10275 break 10276 case 'isAddonProduct': 10277 input = this.renderAddonProduct(property) 10278 break 10279 default: 10280 console.log('No valid type found') 10281 } 10282 10283 if (input) { 10284 const inputContainer = document.createElement('div') 10285 inputContainer.classList.add( 10286 ...this.config.styling.inputContainerClasses.split(' ') 10287 ) 10288 inputContainer.classList.add('product-property-container') // Add a class for easier selection 10289 form.appendChild(inputContainer) 10290 10291 // Only add label for non-addon products and checkboxes 10292 if (!['isAddonProduct', 'isCheckbox'].includes(key)) { 10293 10294 // Label 10295 const label = document.createElement('label') 10296 label.classList.add(...this.config.styling.labelClasses.split(' ')) 10297 10298 // File element 10299 if (key == 'isFile') { 10300 label.setAttribute('for', 'file-' + property.id) 10301 } else { 10302 label.setAttribute('for', this.generageInputId(property.id)) 10303 } 10304 label.innerText = property.title 10305 10306 // Add addon price display next to the property title 10307 if (property.addonPriceRaw && parseFloat(property.addonPriceRaw) > 0) { 10308 const addonPriceSpan = document.createElement('span') 10309 addonPriceSpan.classList.add(...this.config.styling.addonPriceClasses.split(' ')) 10310 addonPriceSpan.classList.add('addon-price') // Add a class for easier selection 10311 addonPriceSpan.innerText = ` (+${property.addonPrice})` 10312 addonPriceSpan.setAttribute('data-addon-price', property.addonPriceRaw) 10313 label.appendChild(addonPriceSpan) 10314 } 10315 10316 if (property.required === '1') { 10317 const requiredStar = document.createElement('span') 10318 requiredStar.classList.add(...this.config.styling.labelErrorClasses.split(' ')) 10319 requiredStar.innerText = '*' 10320 label.appendChild(requiredStar) 10321 } 10322 inputContainer.appendChild(label) 10323 } 10324 10325 // All elements except files and addon products, which need specific structure and attributes 10326 if (key !== 'isFile' && key !== 'isAddonProduct') { 10327 input.setAttribute('id', this.generageInputId(property.id)) 10328 input.setAttribute('name', this.generateInputName(property.id)) 10329 input.setAttribute('data-qb-property', property.id) 10330 10331 if (property.required === '1') { 10332 input.setAttribute('required', true) 10333 } 10334 } 10335 10336 inputContainer.appendChild(input) 10337 10338 form.appendChild(inputContainer) 10339 } 10340 }) 10341 10342 productPropertiesContainer.insertAdjacentElement(insertPosition, form) 10343 10344 // Only render ReCaptcha if we have a File property 10345 if (this.data.hasFileProperties) { 10346 this.renderReCaptcha() 10347 } 10348 } 10349 }, 10350 renderCheckbox: function (property) { 10351 const checkboxWrapper = document.createElement('div') 10352 checkboxWrapper.classList.add('flex', 'items-center') 10353 10354 const checkbox = document.createElement('input') 10355 checkbox.classList.add( 10356 ...this.config.styling.checkboxInputClasses.split(' ') 10357 ) 10358 checkbox.setAttribute('type', 'checkbox') 10359 checkbox.setAttribute('name', this.generateInputName(property.id)) 10360 checkbox.setAttribute('value', '1') 10361 checkbox.setAttribute('data-qb-property', property.id) 10362 10363 // Label 10364 const label = document.createElement('label') 10365 label.classList.add(...this.config.styling.labelClasses.split(' ')) 10366 label.classList.add('ml-4') 10367 label.setAttribute('for', this.generageInputId(property.id)) 10368 label.innerText = property.title 10369 10370 if (property.required === '1') { 10371 const requiredStar = document.createElement('span') 10372 requiredStar.classList.add(...this.config.styling.labelErrorClasses.split(' ')) 10373 requiredStar.innerText = '*' 10374 label.appendChild(requiredStar) 10375 10376 checkbox.setAttribute('required', true) 10377 } 10378 10379 checkboxWrapper.appendChild(checkbox) 10380 checkboxWrapper.appendChild(label) 10381 10382 return checkboxWrapper 10383 }, 10384 renderAddonProduct: function (property) { 10385 const addonContainer = document.createElement('div') 10386 addonContainer.classList.add('addon-product-container', 'flex', 'items-center', 'p-4', 'border', 'rounded-md') 10387 10388 // Checkbox 10389 const checkbox = document.createElement('input') 10390 checkbox.classList.add( 10391 ...this.config.styling.checkboxInputClasses.split(' '), 10392 'mr-4' 10393 ) 10394 checkbox.setAttribute('type', 'checkbox') 10395 checkbox.setAttribute('name', this.generateInputName(property.id)) 10396 checkbox.setAttribute('value', '1') 10397 checkbox.setAttribute('data-qb-property', property.id) 10398 10399 if (property.required === '1') { 10400 checkbox.setAttribute('required', true) 10401 } 10402 10403 // Add checkbox to the container first 10404 addonContainer.appendChild(checkbox) 10405 10406 // Image 10407 if (property.addonProductImage) { 10408 const imageContainer = document.createElement('div') 10409 imageContainer.classList.add('w-16', 'h-16', 'mr-4') 10410 10411 const image = document.createElement('img') 10412 image.src = property.addonProductImage 10413 image.alt = property.addonProductTitle || property.title 10414 image.classList.add('w-full', 'h-full', 'object-cover', 'rounded') 10415 10416 imageContainer.appendChild(image) 10417 addonContainer.appendChild(imageContainer) 10418 } 10419 10420 // Content (Title, Price, Link) 10421 const contentContainer = document.createElement('div') 10422 contentContainer.classList.add('flex', 'flex-col', 'flex-grow') 10423 10424 const titleElement = document.createElement('span') 10425 titleElement.classList.add('font-bold', 'mb-1') 10426 titleElement.textContent = property.addonProductTitle || property.title 10427 10428 const priceElement = document.createElement('span') 10429 priceElement.classList.add('text-sm', 'text-gray-600') 10430 priceElement.textContent = '+' + (property.addonProductPrice || property.addonPrice) 10431 10432 contentContainer.appendChild(titleElement) 10433 contentContainer.appendChild(priceElement) 10434 10435 if (property.addonProductLink) { 10436 const link = document.createElement('a') 10437 link.href = property.addonProductLink 10438 link.target = '_blank' 10439 link.classList.add('text-blue-600', 'hover:underline', 'text-sm', 'mt-1') 10440 link.textContent = property.addonProductLinkText || 'Read More' 10441 contentContainer.appendChild(link) 10442 } 10443 10444 // Add content container last 10445 addonContainer.appendChild(contentContainer) 10446 10447 return addonContainer 10448 }, 10449 renderFile: function (property) { 10450 10451 // Wrapper 10452 const fileContainer = document.createElement('div') 10453 fileContainer.classList.add(...this.config.styling.fileContainerClasses.split(' ')) 10454
10455 // Input 10456 const file = document.createElement('input') 10457 file.classList.add(...this.config.styling.fileInputClasses.split(' ')) 10458 file.classList.add('qs-cart-product-file') 10459 file.setAttribute('id', 'file-' + property.id) 10460 file.setAttribute('type', 'file') 10461 file.setAttribute('name', 'product_fields_files[' + property.id + ']') 10462 file.setAttribute('accept', this.config.fieldsAttributes.fileInputAccept) 10463 if (property.required === '1') { 10464 file.setAttribute('required', true) 10465 file.setAttribute('data-is-required', true) 10466 } 10467 file.style.display = 'none' 10468 10469 file.setAttribute('data-qb-property', property.id) 10470 fileContainer.append(file) 10471 10472 const button = document.createElement('label') 10473 button.setAttribute('for', 'file-' + property.id) 10474 button.classList.add(...this.config.styling.fileInputButtonClasses.split(' ')) 10475 button.innerText = this.data.texts.chooseFile 10476 file.addEventListener('change', function () { 10477 filename.innerText = file.files[0].name 10478 }) 10479 10480 fileContainer.append(button) 10481 10482 const filename = document.createElement('span') 10483 filename.innerText = this.data.texts.noFileChosen 10484 filename.classList.add(...this.config.styling.fileInputFilenameClasses.split(' ')) 10485 fileContainer.append(filename) 10486 10487 return fileContainer 10488 }, 10489 renderLongtext: function (property) { 10490 const longtext = document.createElement('textarea') 10491 longtext.classList.add( 10492 ...this.config.styling.longTextInputClasses.split(' ') 10493 ) 10494 longtext.setAttribute('name', property.name) 10495 longtext.setAttribute('maxlength', 250) 10496 10497 return longtext 10498 }, 10499 renderShorttext: function (property) { 10500 const shorttext = document.createElement('input') 10501 shorttext.classList.add( 10502 ...this.config.styling.shortTextInputClasses.split(' ') 10503 ) 10504 shorttext.setAttribute('type', 'text') 10505 shorttext.setAttribute('name', property.name) 10506 shorttext.setAttribute('maxlength', 250) 10507 10508 return shorttext 10509 }, 10510 renderReCaptcha: function () { 10511 10512 /* PRODUCT FIELD FILE UPLOAD START */ 10513 if (document.readyState === "complete" || document.readyState === "interactive") { 10514 10515 const propertiesForm = document.querySelector('#ProductPropertiesForm') 10516 10517 if (!propertiesForm) { 10518 console.error('Could not find #ProductPropertiesForm') 10519 return false 10520 } 10521 10522 propertiesForm.dataset.prevent = '1' 10523 10524 var recaptchaDiv = document.createElement('div') 10525 recaptchaDiv.setAttribute('class', 'g-recaptcha') 10526 recaptchaDiv.setAttribute('data-sitekey', recaptchaSiteKey) 10527 recaptchaDiv.setAttribute('data-callback', 'recaptchaSubmit') 10528 recaptchaDiv.setAttribute('data-size', 'invisible') 10529 propertiesForm.prepend(recaptchaDiv) 10530 10531 var url = "https://www.google.com/recaptcha/api.js" 10532 var script = document.createElement('script') 10533 script.src = url 10534 script.type = 'text/javascript' 10535 document.getElementsByTagName('head')[0].appendChild(script) 10536 } 10537 }, 10538 uploadFiles: function () { 10539 return new Promise((resolve, reject) => { 10540 const form = document.querySelector('#ProductPropertiesForm') 10541 10542 if (form && form.querySelectorAll('input[type="file"]').length > 0) { 10543 var filesToUpload = new FormData() 10544 Array.from(form.querySelectorAll('input[type="file"]')).forEach(function (tag) { 10545 Array.from(tag.files).forEach(function (file) { 10546 filesToUpload.append(tag.name, file) 10547 }) 10548 }) 10549 10550 var buy_btn_text = null, wait_btn_text = null 10551 var buybtn_selector = document.querySelector('.qs-cart-submit') 10552 10553 var xhr = new XMLHttpRequest() 10554 xhr.open('POST', '/cart/misc', true) 10555 xhr.setRequestHeader('cache-control', 'no-cache') 10556 xhr.responseType = 'json' 10557 10558 var app = this 10559 10560 xhr.onload = function () { 10561 if (xhr.status >= 200 && xhr.status < 300) { 10562 var data = xhr.response 10563 10564 // Reset button 10565 if (buy_btn_text && buy_btn_text.length !== 0) { 10566 buybtn_selector.value = buy_btn_text 10567 buybtn_selector.innerHTML = buy_btn_text 10568 } 10569 buybtn_selector.removeAttribute('disabled') 10570 10571 // Remove progress bar 10572 if (form.querySelector('.productproperties-file-progress')) { 10573 form.querySelector('.productproperties-file-progress').remove() 10574 } 10575 10576 if (data?.properties) { 10577 if (data.properties.errors) { 10578 alert(data.properties.errors) 10579 10580 return false 10581 } else {
10582 data.properties.forEach(function (property, key) { 10583 // document.querySelector('.product-fields-file:eq('+property.property_id+')').value = property.value 10584 }) 10585 10586 document.querySelector('.qs-cart').dataset.prevent = 0 10587 document.querySelector('.qs-cart').dataset.fileuploadstatus = 1 10588 // document.querySelector('.qs-cart').submit() 10589 10590 resolve(true) 10591 } 10592 } else { 10593 10594 // Handle error 10595 console.error('No properties returned from file upload.') 10596 reject(false) 10597 } 10598 } else { 10599 10600 // Handle error 10601 console.error(xhr.statusText) 10602 10603 reject(false) 10604 } 10605 } 10606 10607 xhr.upload.onloadstart = function (e) { 10608 10609 // Progress bar 10610 if (form.querySelectorAll('.productproperties-file-progress').length == 0) { 10611 10612 // Container 10613 const progressContainer = document.createElement('div') 10614 if (app.config?.styling?.fileUploadProgressContainerClasses) { 10615 progressContainer.classList.add(...app.config.styling.fileUploadProgressContainerClasses.split(' ')) 10616 } 10617 progressContainer.classList.add('progress', 'productproperties-file-progress') 10618 10619 // Background
10620 const progressBarBackground = document.createElement('div') 10621 if (app.config?.styling?.fileUploadProgressBarBackgroundClasses) { 10622 progressBarBackground.classList.add(...app.config.styling.fileUploadProgressBarBackgroundClasses.split(' ')) 10623 } 10624 progressBarBackground.style.zIndex = 1 10625 progressContainer.appendChild(progressBarBackground) 10626 10627 // Bar 10628 const progressBar = document.createElement('div') 10629 if (app.config?.styling?.fileUploadProgressBarClasses) { 10630 progressBar.classList.add(...app.config.styling.fileUploadProgressBarClasses.split(' ')) 10631 progressBar.innerText = " " 10632 } 10633 progressBar.style.zIndex = 2 10634 progressBar.classList.add('progress-bar') 10635 progressContainer.appendChild(progressBar) 10636 10637 form.querySelectorAll('input[type="file"]')[form.querySelectorAll('input[type="file"]').length - 1].parentNode.parentNode.insertAdjacentElement('beforeEnd', progressContainer) 10638 } 10639 } 10640 10641 xhr.upload.onloadend = function (e) { } 10642 10643 xhr.upload.onprogress = function (e) { 10644 var percentComplete = Math.ceil((e.loaded / e.total) * 100) 10645 } 10646 10647 xhr.upload.onprogress = function (evt) { 10648 10649 let progressBar 10650 if (form.querySelectorAll('.productproperties-file-progress').length == 0) { 10651 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>') 10652 progressBar = form.querySelectorAll('.productproperties-file-progress') 10653 } 10654 10655 if (evt.lengthComputable) { 10656 var percentComplete = evt.loaded / evt.total 10657 percentComplete = Math.round(percentComplete * 100) 10658 10659 10660 let progressBarBar = form.querySelectorAll('.productproperties-file-progress')[0].querySelector('.progress-bar') 10661 progressBarBar.style.width = percentComplete + '%' 10662 progressBarBar.innerHTML = percentComplete + '%' 10663 } 10664 } 10665 10666 10667 xhr.onerror = function () { 10668 console.error(xhr.statusText) 10669 } 10670 10671 xhr.send(filesToUpload) 10672 10673 // output = false // set as false when this function is executed as sync. 10674 } else { 10675 return true 10676 } 10677 10678 // console.log('Returns: ' + output) 10679 // return output 10680 }) 10681 }, 10682 10683}) 10684 10685function recaptchaSubmit(token) { 10686 fetch('/apps/productproperties/recaptcha', { 10687 method: 'POST', 10688 body: 'recaptchatoken=' + token, 10689 headers: { 10690 'Content-Type': 'application/x-www-form-urlencoded' 10691 } 10692 }) 10693 .then((response) => response.json()) 10694 .then((data) => { 10695 if (data.success) { 10696 document.querySelector('#ProductPropertiesForm').dataset.prevent = 0 10697 10698 // Verification successful. Upload the file and add the item to the cart 10699 window.apps.QuickbutikProductProperties.uploadFiles().then(async (uploadSuccess) => { 10700 Alpine.store('cart').addItemToCart(window.bodyFormData) 10701 }) 10702 10703 try { 10704 // Reset after submit 10705 grecaptcha.reset() 10706 } catch (e) { 10707 console.log(e) 10708 } 10709 10710 document.querySelector('#ProductPropertiesForm').dataset.prevent = 1 10711 10712 return true 10713 } else { 10714 alert('ReCaptcha error: Please try again.') 10715 } 10716 }) 10717} 10718 10719 10720 10721</script>
10721<script type="text/javascript">window.registerApp({ 10722 name: 'Ribbons', 10723 identifier: 'QuickbutikRibbons', 10724 description: 'Display ribbons on the top of product items', 10725 version: '2.0.0', 10726 pages: ['list', 'start', 'product'], 10727 data: {}, 10728 config: { 10729 productItems: { 10730 productItemSelector: '[data-qb-selector="product-item"]', 10731 appsContainer: '[data-qb-selector="product-item"]', 10732 }, 10733 productPage: { 10734 productSelector: '[data-qb-selector="product"]', 10735 appsContainer: '[data-qb-selector="product"]', 10736 }, 10737 style: { 10738 // NOTE: legacy client-side fallbacks, superseded by the server-side 10739 // $ribbon_style_defaults in shop/apps.php (the source of truth). These are 10740 // only used as `font_size || fontSize` / `font_weight || fontWeight`, and 10741 // the server now always sends non-empty values, so they're unreachable. 10742 // Don't "fix" the stale `modern.fontWeight: 600` here in isolation — the 10743 // canonical default is bold; change it in apps.php if you change it at all. 10744 ribbons: { 10745 classic: { 10746 offset: 8, 10747 ribbonHeight: 30, 10748 squareSize: 120, 10749 fontSize: 12, 10750 fontWeight: 700, 10751 textClasses: 'text-white', 10752 }, 10753 rectangle: { 10754 borderRadius: 4, 10755 offsetX: -8, 10756 offsetY: 10, 10757 paddingX: 8, 10758 paddingY: 4, 10759 fontSize: 14, 10760 fontWeight: 700, 10761 textClasses: 'text-white', 10762 }, 10763 circle: { 10764 offsetX: -8, 10765 offsetY: 10, 10766 paddingX: 20, 10767 paddingY: 10, 10768 fontSize: 14, 10769 fontWeight: 700, 10770 textClasses: 'text-white', 10771 }, 10772 modern: { 10773 paddingX: 12, 10774 paddingY: 6, 10775 fontSize: 12, 10776 fontWeight: 600, 10777 textClasses: 'text-white', 10778 }, 10779 vertical: { 10780 paddingX: 4, 10781 paddingY: 16, 10782 fontSize: 12, 10783 fontWeight: 700, 10784 textClasses: 'text-white', 10785 }, 10786 }, 10787 }, 10788 }, 10789 onMount: function ({ appData, page }) { 10790 console.debug(`${this.identifier}@onMount`) 10791 10792 const bodyFormData = new FormData() 10793 let itemSelector = '' 10794 10795 if (['list', 'start'].includes(page)) { 10796 itemSelector = this.config.productItems.productItemSelector 10797 } 10798 if (page == 'product') { 10799 itemSelector = this.config.productPage.productSelector 10800 } 10801 10802 bodyFormData.set( 10803 'products', 10804 [...document.querySelectorAll(itemSelector)].map( 10805 (product) => product.dataset.productId 10806 ) 10807 ) 10808 bodyFormData.set('currentPage', document.body.dataset?.qbPage) 10809 10810 fetch(qs_store_url + '/apps/productribbons/fetch', { 10811 method: 'POST', 10812 body: bodyFormData, 10813 }) 10814 .then((response) => response.json()) 10815 .then((data) => { 10816 if (data?.products) { 10817 const stackingDirection = data.stacking_direction || 'horizontal' 10818 10819 data.products.forEach((productData) => { 10820 let ribbonQuerySelector = '' 10821 if (['list', 'start'].includes(page)) { 10822 ribbonQuerySelector = `${this.config.productItems.appsContainer}[data-product-id="${productData.id}"]` 10823 } else if (page == 'product') { 10824 ribbonQuerySelector = `${this.config.productPage.appsContainer}[data-product-id="${productData.id}"], ${this.config.productItems.appsContainer}[data-product-id="${productData.id}"]` 10825 } 10826 10827 document 10828 .querySelectorAll(ribbonQuerySelector) 10829 .forEach((product) => { 10830 // Separate vertical ribbons from others (they position independently) 10831 const sides = { left: [], right: [] } 10832 const standalone = [] 10833 productData.ribbons.forEach((ribbon) => { 10834 if (ribbon.type === 'vertical') { 10835 standalone.push(ribbon) 10836 } else { 10837 const side = ribbon.position === 'left' ? 'left' : 'right' 10838 sides[side].push(ribbon) 10839 } 10840 }) 10841 10842 // Create a stacking container for each side that has ribbons
10843 Object.entries(sides).forEach(([side, ribbons]) => { 10844 if (ribbons.length === 0) return 10845 10846 const stack = this._createRibbonStack(side, ribbons, stackingDirection) 10847 if (stack) { 10848 product.insertAdjacentElement('beforeend', stack) 10849 } 10850 }) 10851 10852 // Render vertical ribbons with their own absolute positioning 10853 standalone.forEach((ribbon) => { 10854 const el = this.renderRibbon(ribbon) 10855 if (el) { 10856 el.style.position = 'absolute' 10857 el.style.top = '0' 10858 el.style[ribbon.position === 'left' ? 'left' : 'right'] = '0' 10859 el.style.pointerEvents = 'none' 10860 product.insertAdjacentElement('beforeend', el) 10861 } 10862 }) 10863 }) 10864 }) 10865 } 10866 }) 10867 .catch((error) => { 10868 console.error('Error fetching product ribbons:', error) 10869 }) 10870 }, 10871 _createRibbonStack: function (side, ribbons, stackingDirection) { 10872 const flexDirection = stackingDirection === 'horizontal' ? 'row' : 'column' 10873 const stack = document.createElement('div') 10874 stack.setAttribute('data-qb-ribbon-stack', side) 10875 stack.style.position = 'absolute' 10876 stack.style.top = '0' 10877 stack.style[side] = '0' 10878 stack.style.display = 'flex' 10879 stack.style.flexDirection = flexDirection 10880 stack.style.alignItems = side === 'left' ? 'flex-start' : 'flex-end' 10881 stack.style.gap = '6px' 10882 stack.style.pointerEvents = 'none' 10883 10884 ribbons.forEach((ribbon, index) => { 10885 const el = this.renderRibbon({ 10886 text: ribbon.text, 10887 bg_start_color: ribbon.bg_start_color, 10888 bg_end_color: ribbon.bg_end_color, 10889 text_color: ribbon.text_color, 10890 position: ribbon.position, 10891 type: ribbon.type, 10892 font_size: ribbon.font_size, 10893 font_weight: ribbon.font_weight, 10894 custom_css: ribbon.custom_css, 10895 }) 10896 10897 if (el) { 10898 // Convert from absolute to relative for flex stacking 10899 el.classList.remove('absolute') 10900 el.style.position = 'relative' 10901 10902 // First ribbon: preserve original offsets as margin for backward compatibility 10903 if (index === 0) { 10904 if (el.style.top && el.style.top !== '0px') { 10905 el.style.marginTop = el.style.top 10906 } 10907 const sideValue = side === 'left' ? el.style.left : el.style.right 10908 if (sideValue && sideValue !== '0px' && sideValue !== '0') { 10909 el.style[side === 'left' ? 'marginLeft' : 'marginRight'] = sideValue 10910 } 10911 } 10912 10913 el.style.top = '' 10914 el.style.left = '' 10915 el.style.right = '' 10916 10917 stack.appendChild(el) 10918 } else { 10919 console.warn(`Failed to render ribbon with type: ${ribbon.type}`) 10920 } 10921 }) 10922 10923 return stack 10924 }, 10925 renderRibbon: function ({ 10926 bg_end_color, 10927 bg_start_color, 10928 text_color, 10929 position, 10930 text, 10931 type, 10932 font_size, 10933 font_weight, 10934 custom_css, 10935 }) { 10936 10937 if (type === 'classic') { 10938 const { offset, squareSize, ribbonHeight, fontSize, fontWeight, textClasses } = 10939 this.config.style.ribbons.classic 10940 10941 const container = document.createElement('div') 10942 container.setAttribute('data-qb-ribbon', '') 10943 container.classList.add('absolute', 'overflow-hidden') 10944 container.style.pointerEvents = 'none' 10945 container.style.display = 'block' 10946 container.style.top = `-${offset}px` 10947 container.style[ 10948 `${position === 'left' ? 'left' : 'right'}` 10949 ] = `-${offset}px` 10950 container.style.height = `${squareSize}px` 10951 container.style.width = `${squareSize}px` 10952 if (position === 'left') { 10953 container.style.transform = 'rotate(270deg)' 10954 } 10955 10956 const tinySquare1 = document.createElement('div') 10957 tinySquare1.classList.add('absolute', 'top-0', 'left-0') 10958 tinySquare1.style.backgroundColor = bg_start_color 10959 tinySquare1.style.display = 'block' 10960 tinySquare1.style.height = `${offset}px` 10961 tinySquare1.style.width = `${offset}px` 10962 10963 const tinySquare2 = document.createElement('div') 10964 tinySquare2.classList.add('absolute', 'bottom-0', 'right-0') 10965 tinySquare2.style.pointerEvents = 'none' 10966 tinySquare2.style.backgroundColor = bg_end_color 10967 tinySquare2.style.display = 'block' 10968 tinySquare2.style.height = `${offset}px` 10969 tinySquare2.style.width = `${offset}px` 10970 10971 const ribbon = document.createElement('div') 10972 ribbon.classList.add('absolute', 'flex', 'items-center', 'justify-center') 10973 10974 if (textClasses) { 10975 textClasses.split(' ').forEach((cls) => { 10976 ribbon.classList.add(cls) 10977 }) 10978 } 10979 10980 ribbon.style.pointerEvents = 'none' 10981 ribbon.style.height = `${ribbonHeight}px` 10982 ribbon.style.width = `${squareSize}px` 10983 ribbon.style.transformOrigin = 'bottom left' 10984 ribbon.style.top = `-${ribbonHeight}px` 10985 ribbon.style.left = '0' 10986 ribbon.style.transform = 'rotate(45deg)' 10987 ribbon.style.width = `${Math.ceil(squareSize * Math.sqrt(2))}px` 10988 ribbon.style.background = `linear-gradient(${bg_start_color} 0%, ${bg_end_color} 100%)` 10989 ribbon.style.boxShadow = '0 3px 10px -5px rgba(0, 0, 0, 1)' 10990 ribbon.style.color = text_color 10991 ribbon.style.fontSize = `${font_size || fontSize}px` 10992 ribbon.style.fontWeight = font_weight || fontWeight 10993 10994 const textNode = document.createTextNode(text) 10995 ribbon.appendChild(textNode) 10996 10997 container.appendChild(tinySquare1) 10998 container.appendChild(tinySquare2) 10999 container.appendChild(ribbon) 11000 11001 if (custom_css) { 11002 custom_css.split(' ').forEach((cls) => { 11003 if (cls) container.classList.add(cls) 11004 }) 11005 } 11006 11007 return container 11008 } 11009 11010 if (type === 'rectangle') { 11011 const { 11012 borderRadius, 11013 offsetX, 11014 offsetY, 11015 paddingX, 11016 paddingY, 11017 fontSize, 11018 fontWeight, 11019 textClasses, 11020 } = this.config.style.ribbons.rectangle 11021 11022 const container = document.createElement('div') 11023 container.setAttribute('data-qb-ribbon', '') 11024 container.classList.add( 11025 'absolute', 11026 'flex', 11027 'justify-center', 11028 'items-center' 11029 ) 11030 container.style.pointerEvents = 'none' 11031 container.style.top = `${offsetY}px` 11032 container.style[position === 'left' ? 'left' : 'right'] = `${offsetX}px` 11033 container.style.padding = `${paddingY}px ${paddingX}px` 11034 container.style.background = `linear-gradient(${bg_start_color} 0%, ${bg_end_color} 100%)` 11035 container.style.borderRadius = `${borderRadius}px` 11036 11037 const textNode = document.createTextNode(text) 11038 11039 const span = document.createElement('span') 11040 span.appendChild(textNode) 11041 11042 if (textClasses) { 11043 textClasses.split(' ').forEach((cls) => { 11044 span.classList.add(cls) 11045 }) 11046 } 11047 11048 span.style.color = text_color 11049 span.style.fontSize = `${font_size || fontSize}px` 11050 span.style.fontWeight = font_weight || fontWeight 11051 container.appendChild(span) 11052 11053 if (custom_css) { 11054 custom_css.split(' ').forEach((cls) => { 11055 if (cls) container.classList.add(cls) 11056 }) 11057 } 11058 11059 return container 11060 } 11061 11062 if (type === 'circle') { 11063 const { offsetX, offsetY, paddingX, paddingY, fontSize, fontWeight, textClasses } = 11064 this.config.style.ribbons.circle 11065 11066 const container = document.createElement('div') 11067 container.setAttribute('data-qb-ribbon', '') 11068 container.classList.add('absolute') 11069 container.style.pointerEvents = 'none' 11070 container.style.display = 'block' 11071 container.style.top = `${offsetY}px` 11072 container.style[position === 'left' ? 'left' : 'right'] = `${offsetX}px` 11073 container.style.padding = `${paddingY}px ${paddingX}px` 11074 container.style.borderRadius = '50%' 11075 container.style.background = `linear-gradient(${bg_start_color} 0%, ${bg_end_color} 100%)` 11076 11077 const textNode = document.createTextNode(text) 11078 11079 const span = document.createElement('span') 11080 span.appendChild(textNode) 11081 11082 if (textClasses) { 11083 textClasses.split(' ').forEach((cls) => { 11084 span.classList.add(cls) 11085 }) 11086 } 11087 11088 span.style.color = text_color 11089 span.style.fontSize = `${font_size || fontSize}px` 11090 span.style.fontWeight = font_weight || fontWeight 11091 container.appendChild(span) 11092 11093 if (custom_css) { 11094 custom_css.split(' ').forEach((cls) => { 11095 if (cls) container.classList.add(cls) 11096 }) 11097 } 11098 11099 return container 11100 } 11101 11102 if (type === 'modern') { 11103 const { 11104 paddingX, 11105 paddingY, 11106 fontSize, 11107 fontWeight, 11108 textClasses, 11109 } = this.config.style.ribbons.modern 11110 11111 const container = document.createElement('div') 11112 container.setAttribute('data-qb-ribbon', '') 11113 container.classList.add( 11114 'absolute', 11115 'flex', 11116 'justify-center', 11117 'items-center' 11118 ) 11119 container.style.pointerEvents = 'none' 11120 container.style.top = '0px' 11121 container.style[position === 'left' ? 'left' : 'right'] = '0px' 11122 container.style.padding = `${paddingY}px ${paddingX}px` 11123 container.style.backgroundColor = bg_start_color 11124 container.style.backdropFilter = 'blur(4px)' 11125 11126 const textNode = document.createTextNode(text) 11127 11128 const span = document.createElement('span') 11129 span.appendChild(textNode) 11130 11131 if (textClasses) { 11132 textClasses.split(' ').forEach((cls) => { 11133 span.classList.add(cls) 11134 }) 11135 } 11136 11137 span.style.color = text_color 11138 span.style.fontSize = `${font_size || fontSize}px` 11139 span.style.fontWeight = font_weight || fontWeight 11140 span.style.letterSpacing = '0.025em' 11141 container.appendChild(span) 11142 11143 if (custom_css) { 11144 custom_css.split(' ').forEach((cls) => { 11145 if (cls) container.classList.add(cls) 11146 }) 11147 } 11148 11149 return container 11150 } 11151 11152 if (type === 'vertical') { 11153 const { 11154 paddingX, 11155 paddingY, 11156 fontSize, 11157 fontWeight, 11158 textClasses, 11159 } = this.config.style.ribbons.vertical 11160 11161 const container = document.createElement('div') 11162 container.setAttribute('data-qb-ribbon', '') 11163 container.classList.add( 11164 'absolute', 11165 'flex', 11166 'justify-center', 11167 'items-center' 11168 ) 11169 container.style.pointerEvents = 'none' 11170 container.style.top = '0px' 11171 container.style[position === 'left' ? 'left' : 'right'] = '0px' 11172 container.style.padding = `${paddingY}px ${paddingX}px` 11173 container.style.minHeight = '80px' 11174 container.style.backgroundColor = bg_start_color 11175 container.style.writingMode = 'vertical-lr' 11176 container.style.textOrientation = 'mixed' 11177 11178 const textNode = document.createTextNode(text) 11179 11180 const span = document.createElement('span') 11181 span.appendChild(textNode) 11182 11183 if (textClasses) { 11184 textClasses.split(' ').forEach((cls) => { 11185 span.classList.add(cls) 11186 }) 11187 } 11188 11189 span.style.color = text_color 11190 span.style.fontSize = `${font_size || fontSize}px` 11191 span.style.fontWeight = font_weight || fontWeight 11192 container.appendChild(span) 11193 11194 if (custom_css) { 11195 custom_css.split(' ').forEach((cls) => { 11196 if (cls) container.classList.add(cls) 11197 }) 11198 } 11199 11200 return container 11201 } 11202 11203 // Fallback for unknown ribbon types 11204 console.warn(`Unknown ribbon type: ${type}. Expected: classic, rectangle, circle, modern, or vertical`) 11205 return null 11206 }, 11207}) 11208</script>
11208<script type="text/javascript">(function () { 11209 window.registerApp({ 11210 name: "LanguageModal", 11211 identifier: "QuickbutikLanguageModal", 11212 description: "Language Modal", 11213 version: "1.0.0", 11214 pages: ["list", "start", "product"], 11215 data: {}, 11216 config: {}, 11217 onMount: function ({ appData, page }) { 11218 this.loadDependencies().then(function () { 11219 new LanguageModal(appData).check(); 11220 }); 11221 }, 11222 loadDependencies: function () { 11223 return Promise.all([ 11224 loadScript("/assets/shopassets/modal/modal.js"), 11225 loadStylesheet("/assets/shopassets/modal/modal.css"), 11226 11227 loadScript("/assets/shopassets/language-modal/language-modal-v4.js"), 11228 ]) 11229 } 11230 }); 11231 11232 function loadScript(src) { 11233 if (document.querySelector(`script[src="${src}"]`)) { 11234 return Promise.resolve(); 11235 } 11236 11237 return new Promise(function (resolve, reject) { 11238 const script = document.createElement("script"); 11239 script.src = src; 11240 script.async = true; 11241 script.onload = resolve; 11242 script.onerror = reject; 11243 document.head.append(script); 11244 }); 11245 } 11246 11247 function loadStylesheet(href) { 11248 if (document.querySelector(`link[href="${href}"]`)) { 11249 return Promise.resolve(); 11250 } 11251 11252 return new Promise(function (resolve, reject) { 11253 const link = document.createElement("link"); 11254 link.href = href; 11255 link.rel = "stylesheet"; 11256 link.onload = resolve; 11257 link.onerror = reject; 11258 document.head.append(link); 11259 }); 11260 } 11261})(); 11262 11263</script>
11263<script type="text/javascript">window.registerApp({ 11264 name: "ProductReviews", 11265 identifier: "QuickbutikProductReviews", 11266 description: "Show product reviews", 11267 version: "1.3.0", 11268 pages: ["product", "list", "start"], 11269 data: {}, 11270 config: { 11271 containerSelector: "#productReviews", 11272 listItemContainerSelector: 11273 '[data-qb-selector="product-apps-reviews-item-reviews"]', 11274 themeBasedReviews: true, 11275 appContainerSelector: '[data-qb-selector="product-apps-reviews"]', 11276 appContainerFormSelector: '[data-qb-selector="product-apps-reviews-form"]', 11277 appContainerStarsSelector: 11278 '[data-qb-selector="product-apps-reviews-total"]', 11279 appContainerButtonSelector: 11280 '[data-qb-selector="product-apps-reviews-write-review"]', 11281 appContainerReviewsSelector: 11282 '[data-qb-selector="product-apps-reviews-items"]', 11283 star: { 11284 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", 11285 color: { 11286 active: "#F59E0B", 11287 inactive: "#E5E7EB", 11288 }, 11289 }, 11290 stars: { 11291 size: "w-5 h-5", 11292 productPageSize: "w-5 h-5", 11293 }, 11294 styling: { 11295 articleWidth: 85, 11296 classes: { 11297 headerContainer: "flex items-center justify-between py-6 border-b", 11298 addButton: 11299 "inline-flex items-center justify-center w-12 h-12 border hover:border-gray-500", 11300 headerRatingContainer: "flex items-center gap-3", 11301 headerText: "text-xl font-bold font-heading", 11302 ratingCount: "ml-2", 11303 reviewsContainer: "mt-4", 11304 reviewsArticleContainer: "flex gap-4 overflow-hidden", 11305 article: "pb-12 border-b", 11306 articleNameContainer: "flex items-center mb-4 space-x-4", 11307 articleName: "space-y-2 font-medium", 11308 articleRatingContainer: "flex items-center mb-2", 11309 articleContent: "mb-2 text-gray-500 dark:text-gray-400", 11310 articleShowMoreButton: 11311 "text-xs text-xs text-gray-500 ml-2 cursor-pointer", 11312 replyContainer: "border mt-3 ml-3 p-3", 11313 replyText: "text-xs text-gray-500 dark:text-gray-400", 11314 starsContainer: "flex flex-col gap-4", 11315 form: "my-4 flex flex-col gap-6", 11316 formRatingDiv: "flex flex-col gap-4", 11317 formRatingLabel: "text-sm font-bold text-gray-700", 11318 formGroup: "flex flex-col gap-4", 11319 textInput: 11320 "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", 11321 formSubmitButton: 11322 "block w-full cursor-pointer bg-primary text-center text-white font-bold py-5 px-8 uppercase transition duration-200 hover:opacity-70", 11323 formErrorMessageContainer: "text-red-500 text-sm font-bold", 11324 formThankYouMessageContainer: "flex flex-col gap-4 my-12", 11325 formThankYouMessageLabel: "text-sm font-bold text-gray-700", 11326 }, 11327 gradientOverlay: `linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1) 80%)`, 11328 articleFade: { 11329 background: 11330 "linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1))", 11331 classes: "h-12", 11332 }, 11333 articleMaxHeight: "100px", 11334 }, 11335 }, 11336 11337 onMount: function ({ appData, page }) { 11338 console.debug(`${this.identifier}@onMount`); 11339 11340 const bodyFormData = new FormData(); 11341 let itemSelector = ""; 11342 11343 // Respect backend flag: show_in_lists 11344 const normalizeShowInLists = (val) => { 11345 if (typeof val === "boolean") return val; 11346 if (val === 1 || val === "1" || val === "true") return true; 11347 if (val === 0 || val === "0" || val === "false") return false; 11348 return true; // default to true if missing 11349 }; 11350 this.data.showInLists = normalizeShowInLists(appData?.productreviews?.show_in_lists); 11351 11352 // Store review settings 11353 this.data.reviewSectionDescription = appData?.productreviews?.review_section_description || null; 11354 this.data.reviewTextPlaceholder = appData?.productreviews?.review_text_placeholder || null; 11355 11356 // Set star color from settings (backend resolves "primary" to actual color) 11357 if (appData?.productreviews?.star_color) { 11358 this.config.star.color.active = appData.productreviews.star_color; 11359 this.injectStarColorStyles(appData.productreviews.star_color); 11360 } 11361 11362 if (["list", "start"].includes(page)) { 11363 itemSelector = '[data-qb-selector="product-item"]'; 11364 } else if (page === "product") { 11365 // Main product + related product cards 11366 itemSelector = '[data-qb-selector="product"], [data-qb-selector="product-item"]'; 11367 } 11368 // If needed, we could scope to only main product: 11369 // if (page === "product") { itemSelector = '[data-qb-selector="product"]'; } 11370 11371 // On product page, render main product UI from top-level meta 11372 if (page === "product") { 11373 const mainProductEl = document.querySelector('[data-qb-selector="product"]'); 11374 const mainMeta = appData?.productreviews || {}; 11375 const mainProductId = mainProductEl?.getAttribute("data-product-id"); 11376 if (mainProductEl && mainProductId) { 11377 const mainProductData = { 11378 id: mainProductId, 11379 rating: mainMeta.rating,
11380 rating_decimal: mainMeta.rating_decimal, 11381 amountTotal: mainMeta.amountTotal, 11382 reviews: mainMeta.reviews, 11383 has_rated: mainMeta.has_rated, 11384 }; 11385 this.renderProductReviews(mainProductData, page, mainProductEl); 11386 } 11387 } 11388 11389 if ((page === "list" || page === "start") && !this.data.showInLists) { 11390 return; 11391 } 11392 const existingProducts = appData?.productreviews?.products; 11393 if (Array.isArray(existingProducts) && existingProducts.length > 0) { 11394 if (page === "product" && !this.data.showInLists) { 11395 // Do not render related items when disabled 11396 return; 11397 } 11398 existingProducts.forEach((productData) => { 11399 let productQuerySelector = ""; 11400 if (["list", "start"].includes(page)) { 11401 productQuerySelector = `[data-qb-selector="product-item"][data-product-id="${productData.id}"]`; 11402 } else if (page === "product") { 11403 // Only related product cards; main product handled from top-level meta above 11404 productQuerySelector = `[data-qb-selector="product-item"][data-product-id="${productData.id}"]`; 11405 } 11406 11407 document 11408 .querySelectorAll(productQuerySelector) 11409 .forEach((product) => { 11410 this.renderProductReviews(productData, page, product); 11411 }); 11412 }); 11413 return; 11414 } 11415 11416 // Get all product IDs on the page (same as productribbons) 11417 const productElements = document.querySelectorAll(itemSelector); 11418 if (productElements.length === 0) { 11419 return; 11420 } 11421 11422 let productIds = [...productElements] 11423 .map((product) => product.dataset.productId) 11424 .filter((id) => id && id !== "0"); 11425 11426 // On product page, exclude main product ID (we render it from top-level meta) 11427 if (page === "product") { 11428 const mainProductId = document.querySelector('[data-qb-selector="product"]')?.getAttribute('data-product-id'); 11429 if (mainProductId) { 11430 productIds = productIds.filter((id) => id !== mainProductId); 11431 } 11432 } 11433 11434 if (page === 'product' && !this.data.showInLists) { 11435 return; 11436 } 11437 11438 if (productIds.length === 0) { 11439 return; 11440 } 11441 11442 bodyFormData.set("products", productIds.join(",")); 11443 bodyFormData.set("currentPage", document.body.dataset?.qbPage); 11444 11445 // Fetch reviews data from endpoint (same pattern as productribbons) 11446 fetch(qs_store_url + "/apps/productreviews/fetch", { 11447 method: "POST", 11448 body: bodyFormData, 11449 }) 11450 .then((response) => response.json()) 11451 .then((data) => { 11452 console.debug( 11453 `${this.identifier}: Received data for ${ 11454 data?.products?.length || 0 11455 } products` 11456 ); 11457 11458 // Store settings if fetched 11459 if (data?.review_section_description !== undefined) { 11460 this.data.reviewSectionDescription = data.review_section_description; 11461 } 11462 if (data?.review_text_placeholder !== undefined) { 11463 this.data.reviewTextPlaceholder = data.review_text_placeholder; 11464 } 11465 11466 // Set star color from fetched settings (backend resolves "primary" to actual color) 11467 if (data?.star_color) { 11468 this.config.star.color.active = data.star_color; 11469 this.injectStarColorStyles(data.star_color); 11470 } 11471 11472 if (data?.products) { 11473 data?.products.forEach((productData) => { 11474 let productQuerySelector = ""; 11475 if (["list", "start", ].includes(page)) { 11476 productQuerySelector = `[data-qb-selector="product-item"][data-product-id="${productData.id}"]`; 11477 } else if (page === "product") { 11478 // Only related product cards; main product handled from top-level meta 11479 productQuerySelector = `[data-qb-selector="product-item"][data-product-id="${productData.id}"]`; 11480 } 11481 11482 document 11483 .querySelectorAll(productQuerySelector) 11484 .forEach((product) => { 11485 this.renderProductReviews(productData, page, product); 11486 }); 11487 }); 11488 } 11489 }) 11490 .catch((error) => { 11491 console.error( 11492 `${this.identifier}: Error fetching product reviews:`, 11493 error 11494 ); 11495 }); 11496 }, 11497 11498 injectStarColorStyles: function (starColor) { 11499 // Check if style element already exists 11500 const existingStyle = document.getElementById('qb-review-star-styles'); 11501 if (existingStyle) { 11502 // Update existing style 11503 existingStyle.textContent = `.qb-review-star-active { color: ${starColor} !important; }`; 11504 } else { 11505 // Create new style element 11506 const styleElement = document.createElement('style'); 11507 styleElement.id = 'qb-review-star-styles'; 11508 styleElement.textContent = `.qb-review-star-active { color: ${starColor} !important; }`; 11509 document.head.appendChild(styleElement); 11510 } 11511 }, 11512 11513 renderProductReviews: function (productData, page, product) { 11514 const { rating } = productData; 11515 11516 if (this.config.themeBasedReviews) { 11517 // Only render full UI for the main product container on product pages 11518 if (page === 'product' && product?.matches?.('[data-qb-selector="product"]')) { 11519 this.renderThemeBasedProductReviews(productData, page); 11520 } 11521 } else { 11522 // Traditional mode: only render full UI for the main product container 11523 if (page === 'product' && product?.matches?.('[data-qb-selector="product"]')) { 11524 this.renderTraditionalProductReviews(productData, page); 11525 } 11526 } 11527 11528 // Always render product item ratings for list/start pages 11529 // On product page, render ratings for related product cards as well 11530 if ((["list", "start"].includes(page) || page === 'product') && rating > 0 && product?.matches?.('[data-qb-selector="product-item"]')) { 11531 this.renderProductItemRating(productData, product, page); 11532 } 11533 }, 11534 11535 renderThemeBasedProductReviews: function (productData, page) { 11536 const { rating, rating_decimal, reviews, amountTotal } = productData; 11537 11538 // For product page, render full review interface 11539 if (page === "product") { 11540 // Total review score/stars (if any, in multiple places) 11541 if (rating > 0) { 11542 const containers = document.querySelectorAll( 11543 this.config.appContainerStarsSelector 11544 );
11545 if (containers) { 11546 containers.forEach((container) => { 11547 // Only add if not already populated 11548 if (container.children.length === 0) { 11549 container.appendChild( 11550 this.renderRating(parseFloat(rating_decimal ?? rating), page) 11551 ); 11552 const reviewsCount = document.createElement("div"); 11553 reviewsCount.classList.add( 11554 ...this.config.styling.classes.ratingCount.split(" ") 11555 ); 11556 reviewsCount.textContent = "(" + (amountTotal || 0) + ")"; 11557 container.appendChild(reviewsCount); 11558 } 11559 }); 11560 } 11561 } 11562 11563 // Write review form 11564 const container = document.querySelector( 11565 this.config.appContainerFormSelector 11566 ); 11567 if (container && container.children.length === 0) { 11568 // Extract language tags 11569 const language = container.getAttribute("data-language") 11570 ? JSON.parse(container.getAttribute("data-language")) 11571 : {}; 11572 11573 container.appendChild(this.renderForm(language)); 11574 } 11575 11576 // Reviews 11577 if (reviews && reviews.length > 0) { 11578 // Reviews container 11579 const reviewsContainer = document.querySelector( 11580 this.config.appContainerReviewsSelector 11581 ); 11582 if (reviewsContainer) { 11583 // Add reviews to x-data 11584 reviewsContainer.setAttribute( 11585 "x-init", 11586 "reviews = " + JSON.stringify(reviews) 11587 ); 11588 } 11589 } 11590 } 11591 }, 11592 11593 renderTraditionalProductReviews: function (productData, page) { 11594 // Only render for product page in traditional mode 11595 if (page === "product") { 11596 const selector = this.config.containerSelector; 11597 const container = document.querySelector(selector); 11598 11599 if (container && container.children.length === 0) { 11600 container.appendChild(this.renderProductReviewsUI(productData, page)); 11601 } 11602 } 11603 }, 11604 11605 renderProductReviewsUI: function (productData, page) { 11606 const { rating, rating_decimal, has_rated, reviews } = productData; 11607 11608 const mainContainer = document.createElement("div"); 11609 mainContainer.setAttribute("x-data", JSON.stringify({ showForm: false })); 11610 11611 const headerContainer = document.createElement("div"); 11612 headerContainer.classList.add( 11613 ...this.config.styling.classes.headerContainer.split(" ") 11614 ); 11615 11616 const headerRatingContainer = document.createElement("div"); 11617 headerRatingContainer.classList.add( 11618 ...this.config.styling.classes.headerRatingContainer.split(" ") 11619 ); 11620 11621 const h4 = document.createElement("h4"); 11622 h4.textContent = "Reviews"; 11623 h4.classList.add(...this.config.styling.classes.headerText.split(" ")); 11624 11625 headerRatingContainer.appendChild(h4); 11626 11627 if (rating > 0) { 11628 headerRatingContainer.appendChild( 11629 this.renderRating(parseFloat(rating_decimal ?? rating), page) 11630 ); 11631 } 11632 11633 const showRatingFormButton = document.createElement("a"); 11634 showRatingFormButton.classList.add( 11635 ...this.config.styling.classes.addButton.split(" ") 11636 ); 11637 showRatingFormButton.setAttribute("x-on:click", "showForm = !showForm"); 11638 showRatingFormButton.id = "productReviewShowFormButton"; 11639 11640 const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 11641 svg.setAttribute("width", "12"); 11642 svg.setAttribute("height", "12"); 11643 svg.setAttribute("viewBox", "0 0 12 12"); 11644 svg.setAttribute("fill", "none"); 11645 11646 const rect1 = document.createElementNS( 11647 "http://www.w3.org/2000/svg", 11648 "rect" 11649 ); 11650 rect1.setAttribute("x", "5"); 11651 rect1.setAttribute("width", "2"); 11652 rect1.setAttribute("height", "12"); 11653 rect1.setAttribute("fill", "#161616"); 11654 svg.appendChild(rect1); 11655 11656 const rect2 = document.createElementNS( 11657 "http://www.w3.org/2000/svg", 11658 "rect" 11659 ); 11660 rect2.setAttribute("x", "12"); 11661 rect2.setAttribute("y", "5"); 11662 rect2.setAttribute("width", "2"); 11663 rect2.setAttribute("height", "12"); 11664 rect2.setAttribute("transform", "rotate(90 12 5)"); 11665 rect2.setAttribute("fill", "#161616"); 11666 svg.appendChild(rect2); 11667 11668 showRatingFormButton.appendChild(svg); 11669
11670 headerContainer.appendChild(headerRatingContainer); 11671 11672 if (!has_rated) { 11673 headerContainer.appendChild(showRatingFormButton); 11674 } 11675 11676 mainContainer.appendChild(headerContainer); 11677 mainContainer.appendChild(this.renderForm()); 11678 11679 if (reviews && reviews.length > 0) { 11680 const reviewsContainer = document.createElement("div"); 11681 reviewsContainer.className = "relative"; 11682 reviewsContainer.classList.add( 11683 ...this.config.styling.classes.reviewsContainer.split(" ") 11684 ); 11685 11686 const reviewsArticleContainer = document.createElement("div"); 11687 reviewsArticleContainer.className = 11688 this.config.styling.classes.reviewsArticleContainer; 11689 reviewsArticleContainer.style.overflowX = "auto"; 11690 reviewsArticleContainer.style.scrollSnapType = "x mandatory"; 11691 11692 reviews.forEach((review) => { 11693 reviewsArticleContainer.appendChild(this.renderReview(review)); 11694 }); 11695 11696 reviewsContainer.appendChild(reviewsArticleContainer); 11697 11698 if (reviews.length > 1) { 11699 reviewsArticleContainer.lastElementChild.style.marginRight = `${ 11700 100 - this.config.styling.articleWidth 11701 }%`; 11702 11703 const gradientOverlay = document.createElement("div"); 11704 gradientOverlay.style.position = "absolute"; 11705 gradientOverlay.style.top = "0"; 11706 gradientOverlay.style.bottom = "0"; 11707 gradientOverlay.style.right = "0"; 11708 gradientOverlay.style.width = `${ 11709 100 - this.config.styling.articleWidth 11710 }%`; 11711 gradientOverlay.style.pointerEvents = "none"; 11712 gradientOverlay.style.backgroundImage = 11713 this.config.styling.gradientOverlay; 11714 11715 reviewsContainer.appendChild(gradientOverlay); 11716 } 11717 11718 mainContainer.appendChild(reviewsContainer); 11719 } 11720 11721 return mainContainer; 11722 }, 11723 11724 renderReview: function ({ content, name, rating, reply }) { 11725 const article = document.createElement("article"); 11726 article.classList.add(...this.config.styling.classes.article.split(" ")); 11727 article.style.scrollSnapAlign = "start"; 11728 article.style.flex = "0 0 auto"; 11729 article.style.width = `${ 11730 this.data.reviews?.length > 1 ? this.config.styling.articleWidth : 100 11731 }%`; 11732 11733 const articleNameContainer = document.createElement("div"); 11734 articleNameContainer.classList.add( 11735 ...this.config.styling.classes.articleNameContainer.split(" ") 11736 ); 11737 11738 const articleName = document.createElement("div"); 11739 articleName.classList.add( 11740 ...this.config.styling.classes.articleName.split(" ") 11741 ); 11742 const p1 = document.createElement("p"); 11743 p1.textContent = name; 11744 articleName.appendChild(p1); 11745 articleNameContainer.appendChild(articleName); 11746 article.appendChild(articleNameContainer); 11747 11748 const div3 = document.createElement("div"); 11749 div3.classList.add( 11750 ...this.config.styling.classes.articleRatingContainer.split(" ") 11751 ); 11752 div3.appendChild(this.renderRating(rating, "product")); 11753 11754 article.appendChild(div3); 11755 11756 const contentContainer = document.createElement("div"); 11757 contentContainer.setAttribute( 11758 "x-data", 11759 `{ 11760 showMore: false, 11761 isOverflowing(element) { 11762 return element.scrollHeight > element.clientHeight; 11763 } 11764}` 11765 ); 11766 11767 const p2 = document.createElement("p"); 11768 p2.className = "relative"; 11769 p2.classList.add(...this.config.styling.classes.articleContent.split(" ")); 11770 11771 p2.setAttribute("x-bind:class", '{ "overflow-hidden": !showMore }'); 11772 p2.setAttribute( 11773 "x-bind:style", 11774 `{ maxHeight: showMore ? "none" : "${this.config.styling.articleMaxHeight}" }` 11775 ); 11776 11777 p2.innerHTML = content; 11778 11779 const showMoreLink = document.createElement("a"); 11780 showMoreLink.classList.add( 11781 ...this.config.styling.classes.articleShowMoreButton.split(" ") 11782 ); 11783 11784 showMoreLink.setAttribute("x-text", 'showMore ? "Show less" : "Show more"'); 11785 showMoreLink.setAttribute("x-show", "isOverflowing($refs.content)"); 11786 showMoreLink.setAttribute("x-on:click", "showMore = !showMore"); 11787 showMoreLink.setAttribute("x-ref", "showMoreLink"); 11788 11789 const fadeOverlay = document.createElement("div"); 11790 fadeOverlay.className = 11791 "absolute bottom-0 left-0 right-0 pointer-events-none"; 11792 fadeOverlay.classList.add( 11793 ...this.config.styling.articleFade.classes.split(" ") 11794 ); 11795 fadeOverlay.style.backgroundImage = 11796 this.config.styling.articleFade.background; 11797 11798 fadeOverlay.setAttribute( 11799 "x-show", 11800 "!showMore && isOverflowing($refs.content)" 11801 ); 11802 11803 p2.appendChild(fadeOverlay); 11804 11805 contentContainer.appendChild(p2); 11806 p2.setAttribute("x-ref", "content"); 11807 contentContainer.appendChild(showMoreLink); 11808 article.appendChild(contentContainer); 11809 11810 if (reply) { 11811 const aside = document.createElement("aside"); 11812 aside.classList.add( 11813 ...this.config.styling.classes.replyContainer.split(" ") 11814 ); 11815 11816 const p3 = document.createElement("p"); 11817 11818 p3.classList.add(...this.config.styling.classes.replyText.split(" ")); 11819 p3.textContent = reply; 11820 aside.appendChild(p3); 11821 11822 article.appendChild(aside); 11823 } 11824 11825 return article; 11826 }, 11827 renderProductItemRating: function (productData, productItem, page) { 11828 const { id, rating, rating_decimal, amountTotal } = productData; 11829 11830 try { 11831 if (rating && parseFloat(rating) > 0) { 11832 const productItemRatingContainer = document.createElement("div"); 11833 11834 const containerClass = 11835 this.config.styling.classes.productItemRatingContainer; 11836 if (containerClass && containerClass.trim() !== "") { 11837 productItemRatingContainer.classList.add( 11838 ...containerClass.split(" ") 11839 ); 11840 } 11841 11842 // Create rating with review count 11843 const ratingElement = this.renderRating( 11844 parseFloat(rating_decimal ?? rating), 11845 page 11846 ); 11847 11848 // Add review count in parentheses if available 11849 if (amountTotal && parseInt(amountTotal) > 0) { 11850 const reviewCountSpan = document.createElement("span"); 11851 reviewCountSpan.classList.add( 11852 "review-count", 11853 "text-sm", 11854 "text-gray-600", 11855 "ml-2" 11856 ); 11857 reviewCountSpan.textContent = `(${amountTotal})`; 11858 11859 const ratingWrapper = document.createElement("div"); 11860 ratingWrapper.classList.add("flex", "items-center"); 11861 ratingWrapper.appendChild(ratingElement); 11862 ratingWrapper.appendChild(reviewCountSpan); 11863 11864 productItemRatingContainer.appendChild(ratingWrapper); 11865 } else { 11866 productItemRatingContainer.appendChild(ratingElement); 11867 } 11868 11869 // Check if listItemContainerSelector is defined and not blank 11870 let targetContainer = productItem; 11871 if ( 11872 this.config.listItemContainerSelector && 11873 this.config.listItemContainerSelector.trim() !== "" 11874 ) { 11875 const listItemContainer = productItem.querySelector( 11876 this.config.listItemContainerSelector 11877 ); 11878 if (listItemContainer) { 11879 targetContainer = listItemContainer; 11880 } 11881 } 11882 11883 // Only add if not already present (avoid duplicates) 11884 if ( 11885 !targetContainer.querySelector(".product-reviews-rating") && 11886 !targetContainer.querySelector(".review-count") 11887 ) { 11888 productItemRatingContainer.classList.add("product-reviews-rating"); 11889 targetContainer.appendChild(productItemRatingContainer); 11890 } 11891 } 11892 } catch (error) { 11893 console.error( 11894 `${this.identifier}: Error processing product item ${id}:`, 11895 error 11896 ); 11897 } 11898 }, 11899 renderRating: function (rating, page = null) { 11900 const uniqueId = Math.floor(Math.random() * 100000); 11901 11902 const div = document.createElement("div"); 11903 div.classList.add(...this.config.styling.classes.starsContainer.split(" ")); 11904 11905 const ratingDiv = document.createElement("div"); 11906 ratingDiv.className = "flex items-center"; 11907 11908 // Use productPageSize if on product page, otherwise use default size 11909 const starSize = (page === "product") ? this.config.stars.productPageSize : this.config.stars.size; 11910 11911 for (let starIndex = 1; starIndex <= 5; starIndex++) { 11912 const gradientId = `rating-gradient-${uniqueId}-${starIndex}`; 11913 11914 const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 11915 svg.setAttribute("aria-hidden", "true"); 11916 svg.setAttribute("class", starSize); 11917 svg.setAttribute("viewBox", "0 0 20 20"); 11918 svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); 11919 11920 const title = document.createElementNS( 11921 "http://www.w3.org/2000/svg", 11922 "title" 11923 ); 11924 title.textContent = `Star ${starIndex}`; 11925 svg.appendChild(title); 11926 11927 const defs = document.createElementNS( 11928 "http://www.w3.org/2000/svg", 11929 "defs" 11930 ); 11931 const linearGradient = document.createElementNS( 11932 "http://www.w3.org/2000/svg", 11933 "linearGradient" 11934 ); 11935 linearGradient.setAttribute("id", gradientId); 11936 11937 const remainder = rating - starIndex + 1; 11938 const fillPercentage = 11939 remainder > 0 && remainder < 1 11940 ? remainder * 100 11941 : starIndex <= Math.floor(rating) 11942 ? 100 11943 : 0; 11944 11945 const stop1 = document.createElementNS( 11946 "http://www.w3.org/2000/svg", 11947 "stop" 11948 ); 11949 stop1.setAttribute("offset", "0%"); 11950 stop1.setAttribute("stop-color", this.config.star.color.active); 11951 linearGradient.appendChild(stop1); 11952 11953 const stop2 = document.createElementNS( 11954 "http://www.w3.org/2000/svg", 11955 "stop" 11956 ); 11957 stop2.setAttribute("offset", fillPercentage + "%"); 11958 stop2.setAttribute("stop-color", this.config.star.color.active); 11959 linearGradient.appendChild(stop2); 11960 11961 const stop3 = document.createElementNS( 11962 "http://www.w3.org/2000/svg", 11963 "stop" 11964 ); 11965 stop3.setAttribute("offset", fillPercentage + "%"); 11966 stop3.setAttribute("stop-color", this.config.star.color.inactive); 11967 linearGradient.appendChild(stop3); 11968 11969 const stop4 = document.createElementNS( 11970 "http://www.w3.org/2000/svg", 11971 "stop" 11972 ); 11973 stop4.setAttribute("offset", "100%"); 11974 stop4.setAttribute("stop-color", this.config.star.color.inactive); 11975 linearGradient.appendChild(stop4); 11976 11977 defs.appendChild(linearGradient); 11978 svg.appendChild(defs); 11979 11980 const path = document.createElementNS( 11981 "http://www.w3.org/2000/svg", 11982 "path" 11983 ); 11984 path.setAttribute("fill", `url(#${gradientId})`); 11985 path.setAttribute("d", this.config.star.path); 11986 svg.appendChild(path); 11987 11988 ratingDiv.appendChild(svg); 11989 } 11990 11991 div.appendChild(ratingDiv); 11992 11993 return div; 11994 }, 11995 renderForm: function (language) { 11996 const form = document.createElement("form"); 11997 11998 form.id = "productReviewForm"; 11999 form.style.position = "relative"; 12000 12001 form.setAttribute( 12002 "x-on:submit.prevent", 12003 `window.apps.${this.identifier}.onSubmitReview()` 12004 ); 12005 12006 form.classList.add(...this.config.styling.classes.form.split(" ")); 12007 form.setAttribute("x-show", "showForm"); 12008 form.setAttribute("x-transition", ""); 12009 form.setAttribute("x-cloak", ""); 12010 12011 // Add description if available 12012 if (this.data.reviewSectionDescription) { 12013 const descriptionP = document.createElement("p"); 12014 descriptionP.className = "review-section-description text-sm text-gray-700 mb-4"; 12015 descriptionP.textContent = this.data.reviewSectionDescription; 12016 form.appendChild(descriptionP); 12017 } 12018 12019 const ratingDiv = document.createElement("div"); 12020 ratingDiv.classList.add( 12021 ...this.config.styling.classes.formRatingDiv.split(" ") 12022 ); 12023 12024 const ratingLabel = document.createElement("label"); 12025 ratingLabel.classList.add( 12026 ...this.config.styling.classes.formRatingLabel.split(" ") 12027 ); 12028
12029 ratingLabel.textContent = language?.yourRating ?? "Your rating"; 12030 ratingDiv.appendChild(ratingLabel); 12031 12032 ratingDiv.appendChild(this.renderRatingInput()); 12033 12034 form.appendChild(ratingDiv); 12035 12036 const nameDiv = document.createElement("div"); 12037 nameDiv.classList.add(...this.config.styling.classes.formGroup.split(" ")); 12038 12039 const nameLabel = document.createElement("label"); 12040 nameLabel.setAttribute("for", "reviewName"); 12041 nameLabel.className = "text-sm font-bold text-gray-700"; 12042 nameLabel.textContent = language?.name ?? "Name"; 12043 nameDiv.appendChild(nameLabel); 12044 12045 const nameInput = document.createElement("input"); 12046 nameInput.required = true; 12047 nameInput.type = "text"; 12048 nameInput.id = "reviewName"; 12049 nameInput.classList.add( 12050 ...this.config.styling.classes.textInput.split(" ") 12051 ); 12052 nameDiv.appendChild(nameInput); 12053 12054 form.appendChild(nameDiv); 12055 12056 const emailDiv = document.createElement("div"); 12057 emailDiv.classList.add(...this.config.styling.classes.formGroup.split(" ")); 12058 12059 const emailLabel = document.createElement("label"); 12060 emailLabel.setAttribute("for", "reviewEmail"); 12061 emailLabel.className = "text-sm font-bold text-gray-700"; 12062 emailLabel.textContent = language?.email ?? "Email"; 12063 emailDiv.appendChild(emailLabel); 12064 12065 const emailInput = document.createElement("input"); 12066 emailInput.required = true; 12067 emailInput.type = "email"; 12068 emailInput.id = "reviewEmail"; 12069 emailInput.classList.add( 12070 ...this.config.styling.classes.textInput.split(" ") 12071 ); 12072 emailDiv.appendChild(emailInput); 12073 12074 form.appendChild(emailDiv); 12075 12076 const messageDiv = document.createElement("div"); 12077 messageDiv.classList.add( 12078 ...this.config.styling.classes.formGroup.split(" ") 12079 ); 12080 12081 const messageLabel = document.createElement("label"); 12082 messageLabel.setAttribute("for", "reviewMessage"); 12083 messageLabel.className = "text-sm font-bold text-gray-700"; 12084 messageLabel.textContent = language?.message ?? "Message"; 12085 messageDiv.appendChild(messageLabel); 12086 12087 const messageTextarea = document.createElement("textarea"); 12088 messageTextarea.required = true; 12089 messageTextarea.id = "reviewMessage"; 12090 messageTextarea.rows = 10; 12091 messageTextarea.classList.add( 12092 ...this.config.styling.classes.textInput.split(" ") 12093 ); 12094 if (this.data.reviewTextPlaceholder) { 12095 messageTextarea.placeholder = this.data.reviewTextPlaceholder; 12096 } 12097 messageDiv.appendChild(messageTextarea); 12098 12099 form.appendChild(messageDiv); 12100 12101 const submitButton = document.createElement("button"); 12102 submitButton.classList.add( 12103 ...this.config.styling.classes.formSubmitButton.split(" ") 12104 ); 12105 submitButton.textContent = language?.submitReview ?? "Submit review"; 12106 form.appendChild(submitButton); 12107 12108 const errorMessageContainer = document.createElement("div"); 12109 errorMessageContainer.classList.add( 12110 ...this.config.styling.classes.formErrorMessageContainer.split(" ") 12111 ); 12112 errorMessageContainer.id = "productReviewErrorMessage"; 12113 12114 form.appendChild(errorMessageContainer); 12115 12116 return form; 12117 }, 12118 renderRatingInput: function () { 12119 const ratingDiv = document.createElement("div"); 12120 ratingDiv.className = "flex items-center mb-2"; 12121 ratingDiv.setAttribute( 12122 "x-data", 12123 "{ currentRating: null, hoverRating: null }" 12124 ); 12125 12126 // Use productPageSize since this is only used on product page forms 12127 const starSize = this.config.stars.productPageSize; 12128 12129 ratingDiv.innerHTML = ` 12130<template x-for="starIndex in 5"> 12131 <div x-id="['reviewStar']"> 12132 <input required class="opacity-0 absolute -z-50" type="radio" :id="$id('reviewStar')" name="rating" :value="starIndex" x-model="currentRating" /> 12133 12134 <label 12135 :for="$id('reviewStar')" 12136 @mouseover="hoverRating = starIndex" 12137 @mouseleave="hoverRating = null" 12138 > 12139 <svg aria-hidden="true" 12140 :style="{color: !hoverRating && currentRating >= starIndex || hoverRating >= starIndex ? '${this.config.star.color.active}' : '${this.config.star.color.inactive}'}" 12141 class="${starSize}" 12142 fill="currentColor"
12143 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> 12144 </svg> 12145 12146 </label> 12147 </div> 12148</template> 12149 `; 12150 12151 return ratingDiv; 12152 }, 12153 onSubmitReview: function () { 12154 const errorMessageContainer = document.getElementById( 12155 "productReviewErrorMessage" 12156 ); 12157 12158 try { 12159 this.submitReview(); 12160 errorMessageContainer.textContent = ""; 12161 } catch (error) { 12162 this.handleError(error); 12163 } 12164 }, 12165 handleError: function (error) { 12166 console.error("QuickbutikProductReviews@handleError", error); 12167 12168 const errorMessageContainer = document.getElementById( 12169 "productReviewErrorMessage" 12170 ); 12171 12172 const errorMessage = `An error occurred${ 12173 error ? `: ${JSON.stringify(error, null, 2)}` : "" 12174 }.`; 12175 12176 errorMessageContainer.textContent = errorMessage; 12177 }, 12178 submitReview: function () { 12179 const data = [ 12180 { 12181 key: "product_id", 12182 value: document 12183 .querySelector("[data-product-id]") 12184 .getAttribute("data-product-id"), 12185 }, 12186 { 12187 key: "name", 12188 value: document.getElementById("reviewName").value, 12189 }, 12190 { 12191 key: "email", 12192 value: document.getElementById("reviewEmail").value, 12193 }, 12194 { 12195 key: "rating", 12196 value: document.querySelector('input[name="rating"]:checked').value, 12197 }, 12198 { 12199 key: "message", 12200 value: document.getElementById("reviewMessage").value, 12201 }, 12202 ]; 12203 12204 const formData = new FormData(); 12205 12206 data.forEach(({ key, value }) => { 12207 if (!value) { 12208 throw new Error(`Missing ${key}`); 12209 } 12210 12211 formData.append(key, value); 12212 }); 12213 12214 fetch("/apps/productreviews/add", { 12215 method: "POST", 12216 body: formData, 12217 }) 12218 .then((response) => { 12219 if (!response.ok) { 12220 this.handleError(response.statusText); 12221 } 12222 return response.json(); 12223 }) 12224 .then((data) => { 12225 if (data.success === 1) { 12226 this.renderThankYouMessage(data); 12227 } else { 12228 this.handleError(data?.error); 12229 } 12230 }) 12231 .catch((error) => { 12232 this.handleError(error); 12233 }); 12234 }, 12235 renderThankYouMessage: function (data) { 12236 const { message_lbl, message_txt } = data; 12237 12238 const thankYouDiv = document.createElement("div"); 12239 thankYouDiv.classList.add( 12240 ...this.config.styling.classes.formThankYouMessageContainer.split(" ") 12241 ); 12242 12243 const thankYouLabel = document.createElement("label"); 12244 thankYouLabel.classList.add( 12245 ...this.config.styling.classes.formThankYouMessageLabel.split(" ") 12246 ); 12247 thankYouLabel.textContent = message_lbl; 12248 thankYouDiv.appendChild(thankYouLabel); 12249 12250 const thankYouMessage = document.createElement("p"); 12251 thankYouMessage.classList.add( 12252 ...this.config.styling.classes.formThankYouMessageLabel.split(" ") 12253 ); 12254 thankYouMessage.textContent = message_txt; 12255 thankYouDiv.appendChild(thankYouMessage); 12256 12257 const form = document.getElementById("productReviewForm"); 12258 12259 form.parentNode.replaceChild(thankYouDiv, form); 12260 12261 setTimeout(() => { 12262 thankYouDiv.parentNode.removeChild(thankYouDiv); 12263 12264 const productReviewButton = document.getElementById( 12265 "productReviewShowFormButton" 12266 ); 12267 12268 if (productReviewButton) { 12269 productReviewButton.parentNode.removeChild(productReviewButton); 12270 } 12271 }, 5000); 12272 }, 12273 renderErrorMessage: function (error) { 12274 const errorDiv = document.querySelector("#productReviewErrorMessage"); 12275 errorDiv.textContent = error || "Something went wrong"; // TODO: Translate 12276 }, 12277}); 12278</script>
vendor: 1 bytes, line 12278
12278
12279<script> 12280 qb_cookieconsent.run({ 12281 current_lang: 'en', 12282 autoclear_cookies: true, 12283 cookie_expiration: 90, 12284 autoload_css: true, 12285 theme_css: '/assets/shopassets/cookieconsent/cookieconsent.css?v=231', 12286 languages: { 12287 en: { 12288 consent_modal: { 12289 title: "Vi använder cookies", 12290 description: 'Vecanis använder cookies - inte från ugnen, utan de digitala. Genom att fortsätta på sidan godkänner du användandet av cookies.', 12291 primary_btn: { 12292 text: 'Godkänn alla', 12293 role: 'accept_all' 12294 }, 12295 secondary_btn: { 12296 text: 'Inställningar', 12297 role: 'settings' 12298 } 12299 }, 12300 settings_modal: { 12301 title: 'Cookie inställningar', 12302 save_settings_btn: "Spara", 12303 accept_all_btn: "Godkänn alla", 12304 cookie_table_headers: [ 12305 { col1: "Name" }, 12306 { col2: "Domain" }, 12307 { col3: "Expiration" }, 12308 { col4: "Description" } 12309 ], 12310 blocks: qb_cookieconsent_blocks 12311 } 12312 } 12313 }, 12314 onAccept: function (cookie) { 12315 var event = new CustomEvent("QB_CookieConsentAccepted", { 12316 detail: { 12317 cookie: cookie 12318 } 12319 }); 12320 12321 document.dispatchEvent(event); 12322 }, 12323 onChange: function (cookie) { 12324 var event = new CustomEvent("QB_CookieConsentAccepted", { 12325 detail: { 12326 cookie: cookie 12327 } 12328 }); 12329 12330 document.dispatchEvent(event); 12331 }, 12332 }); 12333 12334</script>
12334 12335
12336<script type="text/javascript">// Autocomplete Search App loaded 12337</script>
12337<script type="text/javascript">window.registerApp({ 12338 name: 'Recaptcha', 12339 identifier: 'QuickbutikRecaptcha', 12340 description: 'Recaptcha', 12341 version: '1.1.0', 12342 pages: ['contact'], 12343 data: {}, 12344 config: {}, 12345 onMount: function ({ appData, page }) { 12346 console.debug(`${this.identifier}@onMount`) 12347 12348 let form = document.querySelector('form[role=form]') 12349 if (!form) { 12350 console.error('Aborting recaptcha, form not found.') 12351 return 12352 } 12353 12354 const createCaptchaDiv = () => { 12355 let captchaDiv = document.createElement('div'); 12356 captchaDiv.className = 'g-recaptcha' 12357 captchaDiv.dataset.sitekey = recaptchaSiteKey 12358 captchaDiv.style.marginBottom = '15px' 12359 captchaDiv.style.overflow = 'hidden' 12360 return captchaDiv 12361 } 12362 12363 let button = form.querySelector('button'); 12364 let inputSubmit = form.querySelector('input[type=submit]'); 12365 if (button) { 12366 button.insertAdjacentElement('beforebegin', createCaptchaDiv()) 12367 } else if (inputSubmit) { 12368 inputSubmit.insertAdjacentElement('beforebegin', createCaptchaDiv()) 12369 } 12370 12371 // Honeypot for contact page 12372 let contactForm = document.querySelector('form[role=form]#contact-form') 12373 if (contactForm) { 12374 let pottyDiv = document.createElement('div') 12375 pottyDiv.style.position = 'absolute' 12376 pottyDiv.style.left = '-5000px' 12377 pottyDiv.setAttribute('aria-hidden', true) 12378 12379 let codeLabel = document.createElement('label') 12380 codeLabel.innerText = 'Code' 12381 codeLabel.for = 'code' 12382 codeLabel.className = contactForm.querySelector('label').className 12383 codeLabel.setAttribute('aria-hidden', true) 12384 pottyDiv.appendChild(codeLabel) 12385 12386 let codeInput = document.createElement('input') 12387 codeInput.type = 'text' 12388 codeInput.id = 'code' 12389 codeInput.name = 'code' 12390 codeInput.tabIndex = -1 12391 codeInput.autocomplete = 'off' 12392 codeInput.className = contactForm.querySelector('input[name="name"]').className 12393 codeInput.setAttribute('aria-hidden', true) 12394 pottyDiv.appendChild(codeInput) 12395 12396 contactForm.querySelector('textarea').insertAdjacentElement('afterend', pottyDiv) 12397 } 12398 12399 // Inject Recaptcha script 12400 let existingScript = document.querySelector('script[src="https://www.google.com/recaptcha/api.js"]') 12401 if (!existingScript) { 12402 let script = document.createElement('script') 12403 script.src = 'https://www.google.com/recaptcha/api.js' 12404 script.type = 'text/javascript' 12405 script.async = true 12406 document.querySelector('head').appendChild(script) 12407 } 12408 12409 }, 12410}) 12411</script>
12411
12411<script src="/assets/shopassets/misc/magicbean.js?v=3" data-shop-id="2666" data-url="https://magic-bean.services.quickbutik.com/v1/sow" data-site-section="1" data-key="f35e6c2bb99ee24531c52131069484366fdf8dfe417219e9b2a429657c71446c" data-timestamp="1791438379" defer></script>
12411<script> 12412 document.addEventListener('QB_CookieConsentAccepted', function(event) { 12413 const cookie = qbGetEvent(event).cookie; 12414 12415 const getConsentLevel = (level) => (cookie && cookie.level && cookie.level.includes(level)) ? 'granted' : 'denied'; 12416 12417 gtag('consent', 'update', { 12418 'analytics_storage': getConsentLevel('analytics'), 12419 'ad_storage': getConsentLevel('marketing'), 12420 'ad_user_data': getConsentLevel('marketing'), 12421 'ad_personalization': getConsentLevel('marketing'), 12422 }); 12423 }); 12424 12425 document.addEventListener('QB_CartAdded', function(e) { 12426 const itemData = qbGetEvent(e).itemData; 12427 12428 // item.id is the CART-LINE key — an md5 of the line (see 12429 // Model_Shop_Basketnew), unique per product+variant+options and 12430 // meaningless outside this basket. item.raw_id is the product id, 12431 // which is what view_item sends, what <g:id> carries in the Google 12432 // Shopping feed, and what GA4 needs to join the item funnel. 12433 let eventData = { 12434 currency: 'SEK', 12435 value: parseFloat(itemData.item.price_raw), 12436 items: [{ 12437 id: itemData.item.raw_id || itemData.item.id, 12438 item_id: itemData.item.raw_id || itemData.item.id, 12439 name: itemData.item.title.replaceAll('"', ''), 12440 item_name: itemData.item.title.replaceAll('"', ''), 12441 affiliation: `Vecanis`, 12442 ...(itemData.variant_name && { 12443 name: itemData.item.title.replaceAll('"', '') + ' ' + itemData.variant_name.replaceAll('"', ''), 12444 item_name: itemData.item.title.replaceAll('"', '') + ' ' + itemData.variant_name.replaceAll('"', ''), 12445 variant: itemData.variant_name.replaceAll('"', ''), 12446 item_variant: itemData.variant_name.replaceAll('"', ''), 12447 }), 12448 ...(itemData.item.item_category && { 12449 item_category: itemData.item.item_category.replaceAll('"', ''), 12450 }), 12451 price: parseFloat(itemData.item.price_raw), 12452 quantity: itemData.qty 12453 }], 12454 }; 12455 'undefined' !== typeof gtag && gtag('event', 'add_to_cart', eventData); 12456 }); 12457</script>
12457<script type="text/javascript"> 12458 if(qb_cookieconsent.allowedCategory('analytics')) 12459 { 12460 !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]); 12461 posthog.init('phc_mPzWNLM3JN9i34tiiQ6LlKfr6uoMArp0NOwHdMLOg63',{api_host:'https://eu.i.posthog.com'}) 12462 } 12463 </script>
12463<script id="mcjs">if (typeof qb_cookieconsent == 'undefined' || qb_cookieconsent.allowedCategory('marketing')) { !function(c,h,i,m,p){m=c.createElement(h),p=c.getElementsByTagName(h)[0],m.async=1,m.src=i,p.parentNode.insertBefore(m,p)}(document,"script","https://chimpstatic.com/mcjs-connected/js/users/172f2db97a5ff2a4bd7008db8/c92311638d4901fb3786b162f.js"); } </script>
12463<script>;(function () { 12464 if (window.parent === window) return; 12465 12466 const urlParams = new URLSearchParams(window.location.search); 12467 if (urlParams.has('_qbdev')) { 12468 try { sessionStorage.setItem('_qbdev', '1'); } catch(e) {} 12469 } 12470 try { if (!sessionStorage.getItem('_qbdev')) return; } catch(e) { return; } 12471 12472 const report = () => { 12473 try { 12474 const params = new URLSearchParams(window.location.search); 12475 params.delete('_qbdev'); 12476 params.delete('_qbtid'); 12477 const qs = params.toString(); 12478 window.parent.postMessage( 12479 { type: 'navigation', path: window.location.pathname + (qs ? '?' + qs : '') }, 12480 '*' 12481 ); 12482 } catch (e) {} 12483 }; 12484 12485 report(); 12486 12487 const { pushState, replaceState } = history; 12488 history.pushState = function (...args) { 12489 const result = pushState.apply(this, args); 12490 report(); 12491 return result; 12492 }; 12493 history.replaceState = function (...args) { 12494 const result = replaceState.apply(this, args); 12495 report(); 12496 return result; 12497 }; 12498 window.addEventListener('popstate', report); 12499})()</script>
vendor: 1 bytes, line 12499
12499
12500<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>
12500 12501</body> 12502 12503</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.