Skip to content

Instantly share code, notes, and snippets.

@derblub
Created March 5, 2026 08:38
Show Gist options
  • Select an option

  • Save derblub/f70b7e2fa18f5e90188971f279598da5 to your computer and use it in GitHub Desktop.

Select an option

Save derblub/f70b7e2fa18f5e90188971f279598da5 to your computer and use it in GitHub Desktop.

🌿 Medical Cannabis Strain Analyzer & Rx Manager

A powerful, client-side JavaScript bookmarklet that transforms standard Shopify-based medical cannabis dispensary pages into a fully-featured, personalized analytics dashboard and pharmacy management system.

Use at https://can-doc.de/collections/medizinische-cannabisblueten

✨ Features

  • πŸ“Š Smart Sorting & Heatmaps: Automatically extracts THC, CBD, and Price to calculate a "Value Score" (THC per €). Uses dynamic HSL color heatmaps for quick visual scanning of potency and price.
  • πŸ›’ Rx Stash Builder & 1-Click Cart: Build your monthly prescription with + / - buttons (5g increments). Automatically tracks your budget and total grams, and pushes your entire stash directly into the Shopify cart with a single click.
  • β˜€οΈπŸŒ™ Day / Night Tracking: Set separate monthly gram limits for Sativas (Day) and Indicas (Night). The script automatically categorizes strains and warns you if you exceed your prescription limits.
  • πŸ€– "Magic Match" Engine: Click the πŸͺ„ wand icon on any strain to turn the table into a recommendation engine. It calculates a 0-100% Similarity Score for every other strain based on genetics, THC/CBD proximity, and shared aroma profiles.
  • 🧠 Persistent Memory: Uses localStorage to remember your Dark/Light mode preference, pinned strains, current stash, and budget across sessions.
  • πŸ“‰ Price History Tracker: Remembers the prices of strains from your last visit and flags any price drops (↓) or hikes (↑).
  • πŸ–¨οΈ Doctor Printout & Export: Instantly generate a clean, white-background PDF/Printout of your selected stash for your doctor, or copy a formatted text list to your clipboard.
  • πŸ” Hover Magnifier: Hover over any tiny thumbnail to instantly see a high-res preview of the flower.
  • πŸ”— External Review Integration: Auto-generates clean search links to Flowzz (πŸ‡©πŸ‡ͺ) and Leafly (🍁) for quick patient reviews.

πŸ› οΈ How to Install (Bookmarklet)

  1. Show your browser's Bookmarks Bar (Ctrl+Shift+B or Cmd+Shift+B).
  2. Right-click the Bookmarks Bar and select Add Page (or Add Bookmark).
  3. Name it something recognizable: 🌿 Strain Analyzer
  4. In the URL (or Location) field, paste the minified code provided below.
  5. Click Save.

πŸš€ How to Use

  1. Navigate to the dispensary page containing the product list.
  2. Click the 🌿 Strain Analyzer bookmark in your Bookmarks Bar.
  3. The custom dashboard will instantly overlay the page.

πŸ’» The Code

Option 1: Minified Bookmarklet (Ready to Use)

Copy this entire block and paste it into the URL field of your new bookmark.

javascript:(function(){function safeParse(k,d){try{return JSON.parse(localStorage.getItem(k))||d;}catch(e){return d;}}let isDark=localStorage.getItem('cpto_isDark')==='false'?false:true;let pinnedIds=new Set(safeParse('cpto_pinned',[]));let stash=safeParse('cpto_stash',{});let rxDay=parseFloat(localStorage.getItem('cpto_rx_day')||'10');let rxNight=parseFloat(localStorage.getItem('cpto_rx_night')||'10');let budgetTarget=parseFloat(localStorage.getItem('cpto_budget')||'0');let priceHistory=safeParse('cpto_prices',{});function saveState(){localStorage.setItem('cpto_isDark',isDark);localStorage.setItem('cpto_pinned',JSON.stringify([...pinnedIds]));localStorage.setItem('cpto_stash',JSON.stringify(stash));localStorage.setItem('cpto_rx_day',rxDay);localStorage.setItem('cpto_rx_night',rxNight);localStorage.setItem('cpto_budget',budgetTarget);localStorage.setItem('cpto_prices',JSON.stringify(priceHistory));}const cards=document.querySelectorAll('product-card');let maxThc=0,minPrice=Infinity,maxValue=0;let allBrands=new Set();const products=Array.from(cards).map((card,index)=>{const titleEl=card.querySelector('.product-card__title a');const name=titleEl?titleEl.innerText.trim():'Unknown';const link=titleEl?titleEl.href:'#';const uniqueId=encodeURIComponent(name.replace(/[^a-zA-Z0-9]/g,''));const idInput=card.querySelector('input[name="id"]');const variantId=idInput?idInput.value:null;let brand=name.split(/(?=\s\d)|:/)[0].trim();if(brand)allBrands.add(brand);const imgEl=card.querySelector('img.product-card__image');const image=imgEl?imgEl.src:'';const effectEl=card.querySelector('.product-card__badge-list .badge');const effect=effectEl?effectEl.innerText.trim():'';const aromaEl=card.querySelector('.product-card__aroma');const aroma=aromaEl?aromaEl.innerText.trim():'';const badges=card.querySelectorAll('.product-card__badges .badge');const genetics=badges[0]?badges[0].innerText.trim():'';const radiation=badges[1]?badges[1].innerText.trim():'';const thcText=badges[2]?badges[2].innerText.replace(/[^0-9.,]/g,'').replace(',','.'):'0';const cbdText=badges[3]?badges[3].innerText.replace(/[^0-9.,]/g,'').replace(',','.'):'0';const thc=parseFloat(thcText)||0;const cbd=parseFloat(cbdText)||0;const priceEl=card.querySelector('sale-price');let priceStr=priceEl?priceEl.innerText:'0';let match=priceStr.match(/(\d+[,.]\d+)/);let price=match?parseFloat(match[1].replace(',','.')):0;let priceTrend='';if(priceHistory[uniqueId]){let oldPrice=priceHistory[uniqueId];if(price<oldPrice)priceTrend=`<span style="color:#2ecc71;font-size:12px;margin-left:4px;">↓ -€${(oldPrice-price).toFixed(2)}</span>`;else if(price>oldPrice)priceTrend=`<span style="color:#e74c3c;font-size:12px;margin-left:4px;">↑ +€${(price-oldPrice).toFixed(2)}</span>`;}priceHistory[uniqueId]=price;const comparePriceEl=card.querySelector('compare-at-price');const isOnSale=comparePriceEl&&!comparePriceEl.hasAttribute('hidden')&&price>0;const saleTag=isOnSale?' 🚨':'';const valueScore=price>0?parseFloat((thc/price).toFixed(2)):0;const searchString=`${name} ${effect} ${aroma} ${genetics} ${radiation} ${brand}`.toLowerCase();let cleanStrainName=name.includes(':')?name.split(':')[1].trim():name;const encodedStrain=encodeURIComponent(cleanStrainName);const flowzzLink=`https://flowzz.com/search?q=${encodedStrain}`;const leaflyLink=`https://www.leafly.com/search?q=${encodedStrain}`;if(thc>maxThc)maxThc=thc;if(price>0&&price<minPrice)minPrice=price;if(valueScore>maxValue)maxValue=valueScore;return{uniqueId,variantId,brand,name:name+saleTag,cleanStrainName,link,image,effect,aroma,genetics,radiation,thc,cbd,price,priceTrend,valueScore,searchString,flowzzLink,leaflyLink,trophies:[],similarity:0};});if(products.length===0)return alert("No products found on this page.");saveState();products.forEach(p=>{if(p.thc===maxThc)p.trophies.push('πŸ₯‡');if(p.price===minPrice)p.trophies.push('πŸ’Έ');if(p.valueScore===maxValue)p.trophies.push('πŸ‘‘');});let currentSort={key:'valueScore',asc:false};let searchTerm='';let selectedBrand='';let magicMatchTarget=null;let activeFilters={cheap:false,sativa:false,unbestrahlt:false,highThc:false};function calculateSimilarity(target,cand){if(target.uniqueId===cand.uniqueId)return 100;let score=0;const tG=target.genetics.toLowerCase(),cG=cand.genetics.toLowerCase();if(tG===cG&&tG!=='')score+=30;else if(tG.includes('sativa')&&cG.includes('sativa'))score+=20;else if(tG.includes('indica')&&cG.includes('indica'))score+=20;else if(tG.includes('hybrid')&&cG.includes('hybrid'))score+=15;const diffThc=Math.abs(target.thc-cand.thc);score+=Math.max(0,30-(diffThc*3));const diffCbd=Math.abs(target.cbd-cand.cbd);score+=Math.max(0,10-(diffCbd*5));const tAro=target.aroma.toLowerCase().split(',').map(s=>s.trim()).filter(s=>s);const cAro=cand.aroma.toLowerCase().split(',').map(s=>s.trim()).filter(s=>s);if(tAro.length===0)score+=15;else{let matches=tAro.filter(a=>cAro.includes(a)).length;score+=(matches/tAro.length)*30;}return Math.round(score);}const themes={light:{bg:'#f9f9f9',text:'#111',panel:'#fff',border:'#ddd',row1:'#fdfdfd',row2:'#f1f1f1',th:'#2c3e50',thText:'#fff',link:'#2980b9',inputBg:'#fff',pinBg:'#fff3cd',pinBorder:'#f39c12',pinText:'#000',foot:'#fff'},dark:{bg:'#121212',text:'#eee',panel:'#1e1e1e',border:'#444',row1:'#1a1a1a',row2:'#242424',th:'#000',thText:'#ddd',link:'#63b3ed',inputBg:'#2c2c2c',pinBg:'#332b00',pinBorder:'#b8860b',pinText:'#ffd700',foot:'#1a1a1a'}};function getTheme(){return isDark?themes.dark:themes.light;}function getThcColor(thc){const h=120-((Math.max(10,Math.min(30,thc))-10)/20)*120;return`hsl(${h}, 80%, ${isDark?'25%':'80%'})`;}function getPriceColor(price){const h=120-(((Math.max(5,Math.min(15,price))-5)/10)*120);return`hsl(${h}, 100%, ${isDark?'65%':'35%'})`;}function getAromaIcons(text){let i='';if(text.includes('süß'))i+='🍬 ';if(text.includes('zitronig'))i+='πŸ‹ ';if(text.includes('erdig'))i+='🌲 ';if(text.includes('moschus'))i+='🦨 ';if(text.includes('wΓΌrzig')||text.includes('pfeffrig'))i+='🌢️ ';return i+text;}function getGeneticsIcon(text){if(text.includes('sativa'))return'β˜€οΈ '+text;if(text.includes('indica'))return'πŸŒ™ '+text;return'βš–οΈ '+text;}const existing=document.getElementById('custom-product-table-overlay');if(existing)existing.remove();const tooltip=document.createElement('img');tooltip.id='cpto-hover-image';tooltip.style.cssText='position:fixed;display:none;max-width:350px;max-height:350px;z-index:9999999;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,0.8);pointer-events:none;object-fit:contain;background:#fff;border:2px solid #555;';document.body.appendChild(tooltip);let brandOptions=`<option value="">🌍 All Brands</option>`;[...allBrands].sort().forEach(b=>brandOptions+=`<option value="${b}">${b}</option>`);const container=document.createElement('div');container.id='custom-product-table-overlay';const topBar=document.createElement('div');topBar.innerHTML=`<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;"><h2 style="margin:0;">🌿 Strain Analyzer <span id="cpto-count" style="font-size:16px;font-weight:normal;opacity:0.8;"></span></h2><div><button id="cpto-print" style="padding:8px 12px; background:#7f8c8d; color:white; border:none; border-radius:4px; cursor:pointer; margin-right:5px; font-weight:bold;">πŸ–¨οΈ Print Rx</button><button id="cpto-copy" style="padding:8px 12px; background:#2980b9; color:white; border:none; border-radius:4px; cursor:pointer; margin-right:15px; font-weight:bold;">πŸ“‹ Copy List</button><button id="cpto-dark" style="padding:8px 12px; cursor:pointer; border-radius:4px; border:none; margin-right:5px;">${isDark?'β˜€οΈ':'πŸŒ™'}</button><button id="cpto-close" style="padding:8px 15px; background:#e74c3c; color:#fff; cursor:pointer; border-radius:4px; border:none; font-weight:bold;">Close</button></div></div><div style="display:flex; flex-wrap:wrap; gap:10px; align-items:center;"><input type="text" id="cpto-search" placeholder="Search..." style="padding:8px; border-radius:4px; outline:none; width:150px;"><select id="cpto-brand" style="padding:8px; border-radius:4px; outline:none; cursor:pointer;">${brandOptions}</select><label>β˜€οΈ Day Rx: <input type="number" id="cpto-rx-day" value="${rxDay}" style="width:50px;padding:6px;border-radius:4px; outline:none;" step="5">g</label><label>πŸŒ™ Night Rx: <input type="number" id="cpto-rx-night" value="${rxNight}" style="width:50px;padding:6px;border-radius:4px; outline:none;" step="5">g</label><label>Budget: €<input type="number" id="cpto-budget" value="${budgetTarget}" style="width:60px;padding:6px;border-radius:4px; outline:none;" step="10"></label><div style="border-left:2px solid #ccc; height:20px; margin:0 5px;"></div><button class="cpto-filter" data-filter="cheap" style="padding:5px 10px; cursor:pointer; border-radius:15px;">πŸ’° < €7/g</button><button class="cpto-filter" data-filter="sativa" style="padding:5px 10px; cursor:pointer; border-radius:15px;">β˜€οΈ Sativas</button><button class="cpto-filter" data-filter="unbestrahlt" style="padding:5px 10px; cursor:pointer; border-radius:15px;">🌱 Unbestrahlt</button><button class="cpto-filter" data-filter="highThc" style="padding:5px 10px; cursor:pointer; border-radius:15px;">πŸ”₯ 25%+ THC</button><span id="cpto-magic-clear" style="display:none; cursor:pointer; background:#9b59b6; color:white; padding:5px 10px; border-radius:15px;">❌ Clear Match</span></div>`;const tableContainer=document.createElement('div');const footerBar=document.createElement('div');container.appendChild(topBar);container.appendChild(tableContainer);container.appendChild(footerBar);document.body.appendChild(container);document.getElementById('cpto-close').onclick=()=>{container.remove();tooltip.remove();};document.getElementById('cpto-search').addEventListener('input',(e)=>{searchTerm=e.target.value.toLowerCase();renderTable();});document.getElementById('cpto-brand').addEventListener('change',(e)=>{selectedBrand=e.target.value;renderTable();});document.getElementById('cpto-rx-day').addEventListener('input',(e)=>{rxDay=parseFloat(e.target.value)||0;saveState();renderTable();});document.getElementById('cpto-rx-night').addEventListener('input',(e)=>{rxNight=parseFloat(e.target.value)||0;saveState();renderTable();});document.getElementById('cpto-budget').addEventListener('input',(e)=>{budgetTarget=parseFloat(e.target.value)||0;saveState();renderTable();});document.getElementById('cpto-dark').addEventListener('click',()=>{isDark=!isDark;saveState();renderTable();});document.getElementById('cpto-magic-clear').addEventListener('click',()=>{magicMatchTarget=null;renderTable();});document.getElementById('cpto-print').addEventListener('click',()=>{let stashed=products.filter(p=>stash[p.uniqueId]);if(stashed.length===0)return alert('Your Stash is empty! Add items first.');let win=window.open('','','width=800,height=600');let html=`<html lang="en"><head><title>Medical Rx Printout</title><style>body{font-family:Arial,sans-serif;padding:30px;color:#000;} table{width:100%;border-collapse:collapse;margin-top:20px;} th,td{border:1px solid #000;padding:12px;text-align:left;} th{background:#eee;} h1{margin-bottom:5px;}</style></head><body><h1>Requested Prescription</h1><p>Generated: ${new Date().toLocaleDateString()}</p><table><tr><th>Amount</th><th>Strain Name</th><th>Brand</th><th>Genetics</th><th>THC / CBD</th></tr>`;stashed.forEach(p=>{html+=`<tr><td><b>${stash[p.uniqueId]}g</b></td><td>${p.cleanStrainName}</td><td>${p.brand}</td><td>${p.genetics}</td><td>${p.thc}% / ${p.cbd}%</td></tr>`;});html+=`</table></body></html>`;win.document.write(html);win.document.close();win.focus();setTimeout(()=>{win.print();win.close();},500);});document.getElementById('cpto-copy').addEventListener('click',()=>{let txt="🌿 My Rx Stash:\n\n";let totG=0,totP=0;products.forEach(p=>{if(stash[p.uniqueId]){txt+=`${stash[p.uniqueId]}g x ${p.name.replace(' 🚨','')} (${p.thc}% / ${p.cbd}% CBD) - €${(p.price*stash[p.uniqueId]).toFixed(2)}\n`;totG+=stash[p.uniqueId];totP+=p.price*stash[p.uniqueId];}});txt+=`\nTotal: ${totG}g | €${totP.toFixed(2)}`;navigator.clipboard.writeText(txt).then(()=>alert('List copied to clipboard!'));});footerBar.addEventListener('click',async(e)=>{if(e.target.id!=='cpto-checkout-btn')return;const btn=e.target;const items=products.filter(p=>stash[p.uniqueId]&&p.variantId).map(p=>({id:p.variantId,quantity:stash[p.uniqueId]}));if(items.length===0)return alert("Your Stash is empty!");btn.innerText="Adding to Cart ⏳...";btn.style.background="#f39c12";try{await fetch('/cart/add.js',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({items})});btn.innerText="βœ… Successfully Added!";btn.style.background="#27ae60";setTimeout(()=>{window.location.href='/cart';},1000);}catch(err){alert("Error adding to cart. Please try manually.");btn.innerText="πŸ›οΈ Add Stash to Cart";btn.style.background="#9b59b6";}});document.querySelectorAll('.cpto-filter').forEach(btn=>{btn.addEventListener('click',(e)=>{const f=e.target.getAttribute('data-filter');activeFilters[f]=!activeFilters[f];renderTable();});});tableContainer.addEventListener('click',(e)=>{const t=e.target;const id=t.getAttribute('data-id');if(!id)return;if(t.classList.contains('pin-btn')){pinnedIds.has(id)?pinnedIds.delete(id):pinnedIds.add(id);}else if(t.classList.contains('stash-inc')){stash[id]=(stash[id]||0)+5;}else if(t.classList.contains('stash-dec')){if(stash[id]>0){stash[id]-=5;if(stash[id]<=0)delete stash[id];}}else if(t.classList.contains('magic-btn')){magicMatchTarget=(magicMatchTarget===id)?null:id;}saveState();renderTable();});function renderTable(){const t=getTheme();container.style.cssText=`position:fixed;top:0;left:0;width:100vw;height:100vh;background:${t.bg};color:${t.text};z-index:999999;overflow:auto;padding:20px;padding-bottom:100px;font-family:sans-serif;box-sizing:border-box;`;topBar.style.cssText=`margin-bottom:20px;background:${t.panel};padding:15px;border-radius:8px;border: 1px solid ${t.border}; box-shadow:0 2px 10px rgba(0,0,0,0.1);`;['cpto-search','cpto-rx-day','cpto-rx-night','cpto-budget','cpto-brand','cpto-dark'].forEach(id=>{document.getElementById(id).style.cssText+=`background:${t.inputBg};color:${t.text};border:1px solid ${t.border};`;});document.getElementById('cpto-dark').innerText=isDark?'β˜€οΈ Light':'πŸŒ™ Dark';document.querySelectorAll('.cpto-filter').forEach(btn=>{const f=btn.getAttribute('data-filter');btn.style.background=activeFilters[f]?'#3498db':t.inputBg;btn.style.color=activeFilters[f]?'#fff':t.text;btn.style.border=`1px solid ${activeFilters[f]?'#3498db':t.border}`;});const mTarget=magicMatchTarget?products.find(p=>p.uniqueId===magicMatchTarget):null;document.getElementById('cpto-magic-clear').style.display=mTarget?'inline-block':'none';if(mTarget)products.forEach(p=>p.similarity=calculateSimilarity(mTarget,p));let filtered=products.filter(p=>{if(searchTerm&&!p.searchString.includes(searchTerm))return false;if(selectedBrand&&p.brand!==selectedBrand)return false;if(activeFilters.cheap&&p.price>=7)return false;if(activeFilters.sativa&&!p.genetics.toLowerCase().includes('sativa'))return false;if(activeFilters.unbestrahlt&&!p.radiation.toLowerCase().includes('unbestrahlt'))return false;if(activeFilters.highThc&&p.thc<25)return false;return true;});document.getElementById('cpto-count').innerText=`(${filtered.length} showing)`;let dayG=0,nightG=0,otherG=0,totalP=0;products.forEach(p=>{let q=stash[p.uniqueId];if(q){totalP+=(q*p.price);const g=p.genetics.toLowerCase();if(g.includes('sativa'))dayG+=q;else if(g.includes('indica'))nightG+=q;else otherG+=q;}});let dayWarn=(rxDay>0&&dayG>rxDay)?'color:#e74c3c;':'';let nightWarn=(rxNight>0&&nightG>rxNight)?'color:#e74c3c;':'';let budgetWarn=(budgetTarget>0&&totalP>budgetTarget)?`<span style="color:#e74c3c;font-size:14px;margin-left:5px;">(Over Budget!)</span>`:'';let totalColor=(budgetTarget>0&&totalP>budgetTarget)?'#e74c3c':'#2ecc71';footerBar.style.cssText=`position:fixed;bottom:0;left:0;width:100%;background:${t.foot};border-top:2px solid ${t.border};padding:15px 30px;display:flex;justify-content:space-between;align-items:center;font-size:16px;box-shadow:0 -5px 15px rgba(0,0,0,0.2);box-sizing:border-box;`;footerBar.innerHTML=`<div style="display:flex;align-items:center;gap:15px;"><div>πŸ›’ <b style="margin-left:4px;">Stash:</b></div><div style="${dayWarn}">β˜€οΈ Day: <b>${dayG}</b>${rxDay>0?`/${rxDay}g`:''}</div><div style="${nightWarn}">πŸŒ™ Night: <b>${nightG}</b>${rxNight>0?`/${rxNight}g`:''}</div>${otherG>0?`<div>βš–οΈ Other: <b>${otherG}g</b></div>`:''}<div style="border-left:1px solid ${t.border};height:20px;margin:0 10px;"></div><div style="font-size:22px;color:${totalColor};font-weight:bold;display:flex;align-items:center;">Total: €${totalP.toFixed(2)} ${budgetTarget>0?`<span style="font-size:16px;font-weight:normal;color:${t.text};margin-left:5px;margin-right:5px;">/ €${budgetTarget.toFixed(2)}</span>`:''} ${budgetWarn}</div></div><button id="cpto-checkout-btn" style="background:#9b59b6;color:white;border:none;padding:12px 25px;font-size:18px;border-radius:6px;font-weight:bold;cursor:pointer;box-shadow:0 4px 6px rgba(0,0,0,0.3);transition:all 0.2s;">πŸ›οΈ Add Stash to Cart</button>`;filtered.sort((a,b)=>{const aPin=pinnedIds.has(a.uniqueId)||stash[a.uniqueId]?1:0;const bPin=pinnedIds.has(b.uniqueId)||stash[b.uniqueId]?1:0;if(aPin!==bPin)return bPin-aPin;if(mTarget){return b.similarity-a.similarity;}let valA=a[currentSort.key],valB=b[currentSort.key];if(typeof valA==='string')valA=valA.toLowerCase();if(typeof valB==='string')valB=valB.toLowerCase();if(valA<valB)return currentSort.asc?-1:1;if(valA>valB)return currentSort.asc?1:-1;return 0;});const table=document.createElement('table');table.style.cssText=`width:100%;border-collapse:collapse;text-align:left;background:${t.panel};box-shadow:0 0 10px rgba(0,0,0,0.1);`;const thead=document.createElement('thead');const headerRow=document.createElement('tr');const cols=[{label:'πŸ“Œ',key:'pin',sortable:false},{label:'Stash',key:'stash',sortable:false},{label:'Img',key:'image',sortable:false},{label:'Name',key:'name',sortable:true},{label:'Aroma',key:'aroma',sortable:true},{label:'Genetics',key:'genetics',sortable:true},{label:'THC %',key:'thc',sortable:true},{label:'CBD %',key:'cbd',sortable:true},{label:'Price/g',key:'price',sortable:true},{label:'Val πŸ”₯',key:'valueScore',sortable:true},{label:'Out',key:'reviews',sortable:false}];cols.forEach(c=>{const th=document.createElement('th');th.style.cssText=`padding:12px;background:${t.th};color:${t.thText};position:sticky;top:0;border:1px solid ${t.border};white-space:nowrap;z-index:10;`;if(c.sortable){th.style.cursor='pointer';th.innerText=c.label+(currentSort.key===c.key?(currentSort.asc?' β–²':' β–Ό'):'');th.onclick=()=>{if(currentSort.key===c.key)currentSort.asc=!currentSort.asc;else{currentSort.key=c.key;currentSort.asc=['thc','cbd','price','valueScore'].includes(c.key)?false:true;}renderTable();};}else{th.innerText=c.label;}headerRow.appendChild(th);});thead.appendChild(headerRow);table.appendChild(thead);const tbody=document.createElement('tbody');filtered.forEach((p,index)=>{const tr=document.createElement('tr');const qty=stash[p.uniqueId]||0;const isPinned=pinnedIds.has(p.uniqueId)||qty>0;const isMagic=magicMatchTarget===p.uniqueId;tr.style.cssText=isPinned?`background:${t.pinBg};color:${t.pinText};border:2px solid ${t.pinBorder};`:(index%2===0?`background:${t.row1};`:`background:${t.row2};`);const hoverLogic=`onmouseover="const tp=document.getElementById('cpto-hover-image'); tp.src=this.src; tp.style.display='block'; tp.style.left=(event.clientX+20)+'px'; tp.style.top=(event.clientY+20)+'px';" onmousemove="const tp=document.getElementById('cpto-hover-image'); tp.style.left=(event.clientX+20)+'px'; tp.style.top=(event.clientY+20)+'px';" onmouseout="document.getElementById('cpto-hover-image').style.display='none';"`;const imgTag=p.image?`<img src="${p.image}" style="width:40px;height:40px;object-fit:cover;border-radius:4px;cursor:zoom-in;" ${hoverLogic}>`:'';const btnStyle="width:24px;height:24px;border:none;color:#fff;border-radius:4px;cursor:pointer;font-weight:bold;display:flex;align-items:center;justify-content:center;padding:0;line-height:1;";const stashUI=`<div style="display:flex;align-items:center;justify-content:center;gap:8px;"><button class="stash-dec" data-id="${p.uniqueId}" style="${btnStyle}background:#e74c3c;">-</button><span style="font-weight:bold;width:20px;text-align:center;">${qty}</span><button class="stash-inc" data-id="${p.uniqueId}" style="${btnStyle}background:#2ecc71;">+</button></div>`;let magicBadge='';if(mTarget){const badgeColor=p.similarity>80?'#27ae60':(p.similarity>50?'#f39c12':'#7f8c8d');magicBadge=`<div style="margin-top:4px;"><span style="background:${badgeColor};color:#fff;padding:2px 6px;border-radius:12px;font-size:11px;font-weight:bold;">πŸ€– ${p.similarity}% Match</span></div>`;}tr.innerHTML=`<td style="padding:10px;border:1px solid ${t.border};text-align:center;cursor:pointer;font-size:20px;" class="pin-btn" data-id="${p.uniqueId}">${pinnedIds.has(p.uniqueId)?'πŸ“Œ':'πŸ“'}</td><td style="padding:10px;border:1px solid ${t.border};">${stashUI}</td><td style="padding:5px;border:1px solid ${t.border};text-align:center;">${imgTag}</td><td style="padding:10px;border:1px solid ${t.border};"><span class="magic-btn" data-id="${p.uniqueId}" style="cursor:pointer;font-size:18px;margin-right:5px;filter:grayscale(${mTarget?'1':'0'});" title="Magic Match (Find Similar)">πŸͺ„</span><a href="${p.link}" target="_blank" style="color:${isPinned?t.pinText:t.link};font-weight:bold;text-decoration:none;">${p.trophies.join('')} ${p.name}</a><br><small style="opacity:0.7;">${p.brand}</small>${magicBadge}</td><td style="padding:10px;border:1px solid ${t.border};"><div>${p.effect?'🧠 '+p.effect:''}</div><div style="opacity:0.8;">πŸ‘… ${p.aroma}</div></td><td style="padding:10px;border:1px solid ${t.border};white-space:nowrap;"><div>${p.genetics.includes('Sativa')?'β˜€οΈ':p.genetics.includes('Indica')?'πŸŒ™':'βš–οΈ'} ${p.genetics}</div><div style="opacity:0.8;">${p.radiation.includes('unbestrahlt')?'🌱':'☒️'} ${p.radiation}</div></td><td style="padding:10px;border:1px solid ${t.border};font-weight:bold;background-color:${getThcColor(p.thc)};color:${isDark?'#fff':'#000'};text-shadow:0px 1px 2px rgba(0,0,0,0.2);text-align:center;">${p.thc}%</td><td style="padding:10px;border:1px solid ${t.border};font-weight:bold;text-align:center;">${p.cbd}%</td><td style="padding:10px;border:1px solid ${t.border};font-weight:bold;color:${getPriceColor(p.price)};white-space:nowrap;">€${p.price.toFixed(2)}${p.priceTrend}</td><td style="padding:10px;border:1px solid ${t.border};text-align:center;${p.valueScore>4?`font-weight:bold;color:${isDark?'#4ade80':'#27ae60'};`:''}">${p.valueScore}</td><td style="padding:10px;border:1px solid ${t.border};text-align:center;white-space:nowrap;"><a href="${p.flowzzLink}" target="_blank" title="Flowzz" style="text-decoration:none;font-size:18px;margin-right:4px;">πŸ‡©πŸ‡ͺ</a><a href="${p.leaflyLink}" target="_blank" title="Leafly" style="text-decoration:none;font-size:18px;">🍁</a></td>`;tbody.appendChild(tr);});table.appendChild(tbody);tableContainer.innerHTML='';tableContainer.appendChild(table);}renderTable();})();

Option 2: Readable / Developer Version

Use this version if you want to read, audit, or modify the code.

(function() {
// --- 1. MEMORY & HISTORY ---
function safeParse(key, defaultVal) {
try { return JSON.parse(localStorage.getItem(key)) || defaultVal; }
catch (e) { return defaultVal; }
}
let isDark = localStorage.getItem('cpto_isDark') === 'false' ? false : true;
let pinnedIds = new Set(safeParse('cpto_pinned', []));
let stash = safeParse('cpto_stash', {});
let rxDay = parseFloat(localStorage.getItem('cpto_rx_day') || '10');
let rxNight = parseFloat(localStorage.getItem('cpto_rx_night') || '10');
let budgetTarget = parseFloat(localStorage.getItem('cpto_budget') || '0');
let priceHistory = safeParse('cpto_prices', {});
function saveState() {
localStorage.setItem('cpto_isDark', isDark);
localStorage.setItem('cpto_pinned', JSON.stringify([...pinnedIds]));
localStorage.setItem('cpto_stash', JSON.stringify(stash));
localStorage.setItem('cpto_rx_day', rxDay);
localStorage.setItem('cpto_rx_night', rxNight);
localStorage.setItem('cpto_budget', budgetTarget);
localStorage.setItem('cpto_prices', JSON.stringify(priceHistory));
}
// --- 2. DATA EXTRACTION ---
const cards = document.querySelectorAll('product-card');
let maxThc = 0, minPrice = Infinity, maxValue = 0;
let allBrands = new Set();
const products = Array.from(cards).map((card, index) => {
const titleEl = card.querySelector('.product-card__title a');
const name = titleEl ? titleEl.innerText.trim() : 'Unknown';
const link = titleEl ? titleEl.href : '#';
const uniqueId = encodeURIComponent(name.replace(/[^a-zA-Z0-9]/g, ''));
const idInput = card.querySelector('input[name="id"]');
const variantId = idInput ? idInput.value : null;
let brand = name.split(/(?=\s\d)|:/)[0].trim();
if (brand) allBrands.add(brand);
const imgEl = card.querySelector('img.product-card__image');
const image = imgEl ? imgEl.src : '';
const effectEl = card.querySelector('.product-card__badge-list .badge');
const effect = effectEl ? effectEl.innerText.trim() : '';
const aromaEl = card.querySelector('.product-card__aroma');
const aroma = aromaEl ? aromaEl.innerText.trim() : '';
const badges = card.querySelectorAll('.product-card__badges .badge');
const genetics = badges[0] ? badges[0].innerText.trim() : '';
const radiation = badges[1] ? badges[1].innerText.trim() : '';
const thcText = badges[2] ? badges[2].innerText.replace(/[^0-9.,]/g, '').replace(',', '.') : '0';
const cbdText = badges[3] ? badges[3].innerText.replace(/[^0-9.,]/g, '').replace(',', '.') : '0';
const thc = parseFloat(thcText) || 0;
const cbd = parseFloat(cbdText) || 0; // EXTRACTED CBD
const priceEl = card.querySelector('sale-price');
let priceStr = priceEl ? priceEl.innerText : '0';
let match = priceStr.match(/(\d+[,.]\d+)/);
let price = match ? parseFloat(match[1].replace(',', '.')) : 0;
let priceTrend = '';
if (priceHistory[uniqueId]) {
let oldPrice = priceHistory[uniqueId];
if (price < oldPrice) priceTrend = `<span style="color:#2ecc71;font-size:12px;margin-left:4px;">↓ -€${(oldPrice - price).toFixed(2)}</span>`;
else if (price > oldPrice) priceTrend = `<span style="color:#e74c3c;font-size:12px;margin-left:4px;">↑ +€${(price - oldPrice).toFixed(2)}</span>`;
}
priceHistory[uniqueId] = price;
const comparePriceEl = card.querySelector('compare-at-price');
const isOnSale = comparePriceEl && !comparePriceEl.hasAttribute('hidden') && price > 0;
const saleTag = isOnSale ? ' 🚨' : '';
const valueScore = price > 0 ? parseFloat((thc / price).toFixed(2)) : 0;
const searchString = `${name} ${effect} ${aroma} ${genetics} ${radiation} ${brand}`.toLowerCase();
let cleanStrainName = name.includes(':') ? name.split(':')[1].trim() : name;
const encodedStrain = encodeURIComponent(cleanStrainName);
const flowzzLink = `https://flowzz.com/search?q=${encodedStrain}`;
const leaflyLink = `https://www.leafly.com/search?q=${encodedStrain}`;
if (thc > maxThc) maxThc = thc;
if (price > 0 && price < minPrice) minPrice = price;
if (valueScore > maxValue) maxValue = valueScore;
return { uniqueId, variantId, brand, name: name + saleTag, cleanStrainName, link, image, effect, aroma, genetics, radiation, thc, cbd, price, priceTrend, valueScore, searchString, flowzzLink, leaflyLink, trophies: [], similarity: 0 };
});
if (products.length === 0) return alert("No products found on this page.");
saveState();
products.forEach(p => {
if (p.thc === maxThc) p.trophies.push('πŸ₯‡');
if (p.price === minPrice) p.trophies.push('πŸ’Έ');
if (p.valueScore === maxValue) p.trophies.push('πŸ‘‘');
});
let currentSort = { key: 'valueScore', asc: false };
let searchTerm = '';
let selectedBrand = '';
let magicMatchTarget = null;
let activeFilters = { cheap: false, sativa: false, unbestrahlt: false, highThc: false };
// --- SMART MATCH ALGORITHM ---
function calculateSimilarity(target, cand) {
if (target.uniqueId === cand.uniqueId) return 100;
let score = 0;
// 1. Genetics Match (Max 30pts)
const tG = target.genetics.toLowerCase(), cG = cand.genetics.toLowerCase();
if (tG === cG && tG !== '') score += 30;
else if (tG.includes('sativa') && cG.includes('sativa')) score += 20;
else if (tG.includes('indica') && cG.includes('indica')) score += 20;
else if (tG.includes('hybrid') && cG.includes('hybrid')) score += 15;
// 2. THC Proximity (Max 30pts) - Loses points if gap is > 0
const diffThc = Math.abs(target.thc - cand.thc);
score += Math.max(0, 30 - (diffThc * 3)); // 10% diff = 0 pts
// 3. CBD Proximity (Max 10pts)
const diffCbd = Math.abs(target.cbd - cand.cbd);
score += Math.max(0, 10 - (diffCbd * 5)); // 2% diff = 0 pts
// 4. Aroma Overlap (Max 30pts)
const tAro = target.aroma.toLowerCase().split(',').map(s=>s.trim()).filter(s=>s);
const cAro = cand.aroma.toLowerCase().split(',').map(s=>s.trim()).filter(s=>s);
if (tAro.length === 0) score += 15; // default middle if no data
else {
let matches = tAro.filter(a => cAro.includes(a)).length;
score += (matches / tAro.length) * 30;
}
return Math.round(score);
}
// --- 3. UI GENERATION ---
const themes = {
light: { bg: '#f9f9f9', text: '#111', panel: '#fff', border: '#ddd', row1: '#fdfdfd', row2: '#f1f1f1', th: '#2c3e50', thText: '#fff', link: '#2980b9', inputBg: '#fff', pinBg: '#fff3cd', pinBorder: '#f39c12', pinText: '#000', foot: '#fff' },
dark: { bg: '#121212', text: '#eee', panel: '#1e1e1e', border: '#444', row1: '#1a1a1a', row2: '#242424', th: '#000', thText: '#ddd', link: '#63b3ed', inputBg: '#2c2c2c', pinBg: '#332b00', pinBorder: '#b8860b', pinText: '#ffd700', foot: '#1a1a1a' }
};
function getTheme() { return isDark ? themes.dark : themes.light; }
function getThcColor(thc) { const h = 120 - ((Math.max(10, Math.min(30, thc)) - 10) / 20) * 120; return `hsl(${h}, 80%, ${isDark ? '25%' : '80%'})`; }
function getPriceColor(price) { const h = 120 - (((Math.max(5, Math.min(15, price)) - 5) / 10) * 120); return `hsl(${h}, 100%, ${isDark ? '65%' : '35%'})`; }
function getAromaIcons(text) { let i=''; if(text.includes('süß'))i+='🍬 '; if(text.includes('zitronig'))i+='πŸ‹ '; if(text.includes('erdig'))i+='🌲 '; if(text.includes('moschus'))i+='🦨 '; if(text.includes('wΓΌrzig')||text.includes('pfeffrig'))i+='🌢️ '; return i+text; }
function getGeneticsIcon(text) { if(text.includes('sativa'))return 'β˜€οΈ '+text; if(text.includes('indica'))return 'πŸŒ™ '+text; return 'βš–οΈ '+text; }
const existing = document.getElementById('custom-product-table-overlay');
if (existing) existing.remove();
const tooltip = document.createElement('img');
tooltip.id = 'cpto-hover-image';
tooltip.style.cssText = 'position:fixed;display:none;max-width:350px;max-height:350px;z-index:9999999;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,0.8);pointer-events:none;object-fit:contain;background:#fff;border:2px solid #555;';
document.body.appendChild(tooltip);
let brandOptions = `<option value="">🌍 All Brands</option>`;
[...allBrands].sort().forEach(b => brandOptions += `<option value="${b}">${b}</option>`);
const container = document.createElement('div');
container.id = 'custom-product-table-overlay';
const topBar = document.createElement('div');
topBar.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<h2 style="margin:0;">🌿 Strain Analyzer <span id="cpto-count" style="font-size:16px;font-weight:normal;opacity:0.8;"></span></h2>
<div>
<button id="cpto-print" style="padding:8px 12px; background:#7f8c8d; color:white; border:none; border-radius:4px; cursor:pointer; margin-right:5px; font-weight:bold;">πŸ–¨οΈ Print Rx</button>
<button id="cpto-copy" style="padding:8px 12px; background:#2980b9; color:white; border:none; border-radius:4px; cursor:pointer; margin-right:15px; font-weight:bold;">πŸ“‹ Copy List</button>
<button id="cpto-dark" style="padding:8px 12px; cursor:pointer; border-radius:4px; border:none; margin-right:5px;">${isDark?'β˜€οΈ':'πŸŒ™'}</button>
<button id="cpto-close" style="padding:8px 15px; background:#e74c3c; color:#fff; cursor:pointer; border-radius:4px; border:none; font-weight:bold;">Close Window</button>
</div>
</div>
<div style="display:flex; flex-wrap:wrap; gap:10px; align-items:center;">
<input type="text" id="cpto-search" placeholder="Search..." style="padding:8px; border-radius:4px; outline:none; width:150px;">
<select id="cpto-brand" style="padding:8px; border-radius:4px; outline:none; cursor:pointer;">${brandOptions}</select>
<label>β˜€οΈ Day: <input type="number" id="cpto-rx-day" value="${rxDay}" style="width:50px;padding:6px;border-radius:4px; outline:none;" step="5">g</label>
<label>πŸŒ™ Night: <input type="number" id="cpto-rx-night" value="${rxNight}" style="width:50px;padding:6px;border-radius:4px; outline:none;" step="5">g</label>
<label>Budget: €<input type="number" id="cpto-budget" value="${budgetTarget}" style="width:60px;padding:6px;border-radius:4px; outline:none;" step="10"></label>
<div style="border-left:2px solid #ccc; height:20px; margin:0 5px;"></div>
<button class="cpto-filter" data-filter="cheap" style="padding:5px 10px; cursor:pointer; border-radius:15px;">πŸ’° < €7/g</button>
<button class="cpto-filter" data-filter="sativa" style="padding:5px 10px; cursor:pointer; border-radius:15px;">β˜€οΈ Sativas</button>
<button class="cpto-filter" data-filter="unbestrahlt" style="padding:5px 10px; cursor:pointer; border-radius:15px;">🌱 Unbestrahlt</button>
<button class="cpto-filter" data-filter="highThc" style="padding:5px 10px; cursor:pointer; border-radius:15px;">πŸ”₯ 25%+ THC</button>
<span id="cpto-magic-clear" style="display:none; cursor:pointer; background:#9b59b6; color:white; padding:5px 10px; border-radius:15px;">❌ Clear Match</span>
</div>
`;
const tableContainer = document.createElement('div');
const footerBar = document.createElement('div');
container.appendChild(topBar);
container.appendChild(tableContainer);
container.appendChild(footerBar);
document.body.appendChild(container);
// --- 4. EVENT LISTENERS ---
document.getElementById('cpto-close').onclick = () => { container.remove(); tooltip.remove(); };
document.getElementById('cpto-search').addEventListener('input', (e) => { searchTerm = e.target.value.toLowerCase(); renderTable(); });
document.getElementById('cpto-brand').addEventListener('change', (e) => { selectedBrand = e.target.value; renderTable(); });
document.getElementById('cpto-rx-day').addEventListener('input', (e) => { rxDay = parseFloat(e.target.value)||0; saveState(); renderTable(); });
document.getElementById('cpto-rx-night').addEventListener('input', (e) => { rxNight = parseFloat(e.target.value)||0; saveState(); renderTable(); });
document.getElementById('cpto-budget').addEventListener('input', (e) => { budgetTarget = parseFloat(e.target.value)||0; saveState(); renderTable(); });
document.getElementById('cpto-dark').addEventListener('click', () => { isDark = !isDark; saveState(); renderTable(); });
document.getElementById('cpto-magic-clear').addEventListener('click', () => { magicMatchTarget = null; renderTable(); });
document.getElementById('cpto-print').addEventListener('click', () => {
let stashed = products.filter(p => stash[p.uniqueId]);
if(stashed.length === 0) return alert('Your Stash is empty! Add items first.');
let win = window.open('', '', 'width=800,height=600');
let html = `<html lang="en"><head><title>Medical Rx Printout</title><style>body{font-family:Arial,sans-serif;padding:30px;color:#000;} table{width:100%;border-collapse:collapse;margin-top:20px;} th,td{border:1px solid #000;padding:12px;text-align:left;} th{background:#eee;} h1{margin-bottom:5px;}</style></head><body><h1>Requested Prescription</h1><p>Generated: ${new Date().toLocaleDateString()}</p><table><tr><th>Amount</th><th>Strain Name</th><th>Brand</th><th>Genetics</th><th>THC / CBD</th></tr>`;
stashed.forEach(p => { html += `<tr><td><b>${stash[p.uniqueId]}g</b></td><td>${p.cleanStrainName}</td><td>${p.brand}</td><td>${p.genetics}</td><td>${p.thc}% / ${p.cbd}%</td></tr>`; });
html += `</table></body></html>`;
win.document.write(html); win.document.close(); win.focus();
setTimeout(() => { win.print(); win.close(); }, 500);
});
document.getElementById('cpto-copy').addEventListener('click', () => {
let txt = "🌿 My Rx Stash:\n\n"; let totG = 0, totP = 0;
products.forEach(p => { if (stash[p.uniqueId]) { txt += `${stash[p.uniqueId]}g x ${p.name.replace(' 🚨','')} (${p.thc}% / ${p.cbd}% CBD) - €${(p.price * stash[p.uniqueId]).toFixed(2)}\n`; totG += stash[p.uniqueId]; totP += p.price * stash[p.uniqueId]; } });
txt += `\nTotal: ${totG}g | €${totP.toFixed(2)}`;
navigator.clipboard.writeText(txt).then(() => alert('List copied to clipboard!'));
});
footerBar.addEventListener('click', async (e) => {
if(e.target.id !== 'cpto-checkout-btn') return;
const btn = e.target;
const items = products.filter(p => stash[p.uniqueId] && p.variantId).map(p => ({ id: p.variantId, quantity: stash[p.uniqueId] }));
if(items.length === 0) return alert("Your Stash is empty!");
btn.innerText = "Adding to Cart ⏳...";
btn.style.background = "#f39c12";
try {
await fetch('/cart/add.js', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ items }) });
btn.innerText = "βœ… Successfully Added!";
btn.style.background = "#27ae60";
setTimeout(() => { window.location.href = '/cart'; }, 1000);
} catch(err) {
alert("Error adding to cart. Please try manually.");
btn.innerText = "πŸ›οΈ Add Stash to Cart";
btn.style.background = "#9b59b6";
}
});
document.querySelectorAll('.cpto-filter').forEach(btn => {
btn.addEventListener('click', (e) => {
const f = e.target.getAttribute('data-filter');
activeFilters[f] = !activeFilters[f];
renderTable();
});
});
tableContainer.addEventListener('click', (e) => {
const t = e.target;
const id = t.getAttribute('data-id');
if (!id) return;
if (t.classList.contains('pin-btn')) { pinnedIds.has(id) ? pinnedIds.delete(id) : pinnedIds.add(id); }
else if (t.classList.contains('stash-inc')) { stash[id] = (stash[id] || 0) + 5; }
else if (t.classList.contains('stash-dec')) { if (stash[id] > 0) { stash[id] -= 5; if (stash[id] <= 0) delete stash[id]; } }
else if (t.classList.contains('magic-btn')) { magicMatchTarget = (magicMatchTarget === id) ? null : id; }
saveState(); renderTable();
});
// --- 5. RENDER LOOP ---
function renderTable() {
const t = getTheme();
container.style.cssText = `position:fixed;top:0;left:0;width:100vw;height:100vh;background:${t.bg};color:${t.text};z-index:999999;overflow:auto;padding:20px;padding-bottom:100px;font-family:sans-serif;box-sizing:border-box;`;
topBar.style.cssText = `margin-bottom:20px;background:${t.panel};padding:15px;border-radius:8px;border: 1px solid ${t.border}; box-shadow:0 2px 10px rgba(0,0,0,0.1);`;
['cpto-search', 'cpto-rx-day', 'cpto-rx-night', 'cpto-budget', 'cpto-brand', 'cpto-dark'].forEach(id => {
document.getElementById(id).style.cssText += `background:${t.inputBg};color:${t.text};border:1px solid ${t.border};`;
});
document.getElementById('cpto-dark').innerText = isDark ? 'β˜€οΈ Light' : 'πŸŒ™ Dark';
document.querySelectorAll('.cpto-filter').forEach(btn => {
const f = btn.getAttribute('data-filter');
btn.style.background = activeFilters[f] ? '#3498db' : t.inputBg;
btn.style.color = activeFilters[f] ? '#fff' : t.text;
btn.style.border = `1px solid ${activeFilters[f] ? '#3498db' : t.border}`;
});
const mTarget = magicMatchTarget ? products.find(p => p.uniqueId === magicMatchTarget) : null;
document.getElementById('cpto-magic-clear').style.display = mTarget ? 'inline-block' : 'none';
// Calculate similarities if target exists
if (mTarget) products.forEach(p => p.similarity = calculateSimilarity(mTarget, p));
let filtered = products.filter(p => {
if (searchTerm && !p.searchString.includes(searchTerm)) return false;
if (selectedBrand && p.brand !== selectedBrand) return false;
if (activeFilters.cheap && p.price >= 7) return false;
if (activeFilters.sativa && !p.genetics.toLowerCase().includes('sativa')) return false;
if (activeFilters.unbestrahlt && !p.radiation.toLowerCase().includes('unbestrahlt')) return false;
if (activeFilters.highThc && p.thc < 25) return false;
return true;
});
document.getElementById('cpto-count').innerText = `(${filtered.length} showing)`;
// Calculate Split Day/Night Totals
let dayG = 0, nightG = 0, otherG = 0, totalP = 0;
products.forEach(p => {
let q = stash[p.uniqueId];
if (q) {
totalP += (q * p.price);
const g = p.genetics.toLowerCase();
if (g.includes('sativa')) dayG += q;
else if (g.includes('indica')) nightG += q;
else otherG += q;
}
});
let dayWarn = (rxDay > 0 && dayG > rxDay) ? 'color:#e74c3c;' : '';
let nightWarn = (rxNight > 0 && nightG > rxNight) ? 'color:#e74c3c;' : '';
let budgetWarn = (budgetTarget > 0 && totalP > budgetTarget) ? `<span style="color:#e74c3c;font-size:14px;margin-left:5px;">(Over Budget!)</span>` : '';
let totalColor = (budgetTarget > 0 && totalP > budgetTarget) ? '#e74c3c' : '#2ecc71';
footerBar.style.cssText = `position:fixed;bottom:0;left:0;width:100%;background:${t.foot};border-top:2px solid ${t.border};padding:15px 30px;display:flex;justify-content:space-between;align-items:center;font-size:16px;box-shadow:0 -5px 15px rgba(0,0,0,0.2);box-sizing:border-box;`;
footerBar.innerHTML = `
<div style="display:flex;align-items:center;gap:15px;">
<div>πŸ›’ <b style="margin-left:4px;">Stash:</b></div>
<div style="${dayWarn}">β˜€οΈ Day: <b>${dayG}</b>${rxDay>0?`/${rxDay}g`:''}</div>
<div style="${nightWarn}">πŸŒ™ Night: <b>${nightG}</b>${rxNight>0?`/${rxNight}g`:''}</div>
${otherG > 0 ? `<div>βš–οΈ Other: <b>${otherG}g</b></div>` : ''}
<div style="border-left:1px solid ${t.border};height:20px;margin:0 10px;"></div>
<div style="font-size:22px;color:${totalColor};font-weight:bold;display:flex;align-items:center;">Total: €${totalP.toFixed(2)} ${budgetTarget > 0 ? `<span style="font-size:16px;font-weight:normal;color:${t.text};margin-left:5px;margin-right:5px;">/ €${budgetTarget.toFixed(2)}</span>` : ''} ${budgetWarn}</div>
</div>
<button id="cpto-checkout-btn" style="background:#9b59b6;color:white;border:none;padding:12px 25px;font-size:18px;border-radius:6px;font-weight:bold;cursor:pointer;box-shadow:0 4px 6px rgba(0,0,0,0.3);transition:all 0.2s;">πŸ›οΈ Add Stash to Cart</button>
`;
filtered.sort((a, b) => {
const aPin = pinnedIds.has(a.uniqueId) || stash[a.uniqueId] ? 1 : 0;
const bPin = pinnedIds.has(b.uniqueId) || stash[b.uniqueId] ? 1 : 0;
if (aPin !== bPin) return bPin - aPin;
// Magic Match Sorting Override
if (mTarget) {
return b.similarity - a.similarity;
}
let valA = a[currentSort.key], valB = b[currentSort.key];
if (typeof valA === 'string') valA = valA.toLowerCase();
if (typeof valB === 'string') valB = valB.toLowerCase();
if (valA < valB) return currentSort.asc ? -1 : 1;
if (valA > valB) return currentSort.asc ? 1 : -1;
return 0;
});
const table = document.createElement('table');
table.style.cssText = `width:100%;border-collapse:collapse;text-align:left;background:${t.panel};box-shadow:0 0 10px rgba(0,0,0,0.1);`;
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
const cols = [
{ label: 'πŸ“Œ', key: 'pin', sortable: false },
{ label: 'Stash', key: 'stash', sortable: false },
{ label: 'Img', key: 'image', sortable: false },
{ label: 'Name', key: 'name', sortable: true },
{ label: 'Aroma', key: 'aroma', sortable: true },
{ label: 'Genetics', key: 'genetics', sortable: true },
{ label: 'THC %', key: 'thc', sortable: true },
{ label: 'CBD %', key: 'cbd', sortable: true },
{ label: 'Price/g', key: 'price', sortable: true },
{ label: 'Val πŸ”₯', key: 'valueScore', sortable: true },
{ label: 'Out', key: 'reviews', sortable: false }
];
cols.forEach(c => {
const th = document.createElement('th');
th.style.cssText = `padding:12px;background:${t.th};color:${t.thText};position:sticky;top:0;border:1px solid ${t.border};white-space:nowrap;z-index:10;`;
if (c.sortable) {
th.style.cursor = 'pointer';
th.innerText = c.label + (currentSort.key === c.key ? (currentSort.asc ? ' β–²' : ' β–Ό') : '');
th.onclick = () => {
if (currentSort.key === c.key) currentSort.asc = !currentSort.asc;
else { currentSort.key = c.key; currentSort.asc = ['thc', 'cbd', 'price', 'valueScore'].includes(c.key) ? false : true; }
renderTable();
};
} else { th.innerText = c.label; }
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement('tbody');
filtered.forEach((p, index) => {
const tr = document.createElement('tr');
const qty = stash[p.uniqueId] || 0;
const isPinned = pinnedIds.has(p.uniqueId) || qty > 0;
tr.style.cssText = isPinned ? `background:${t.pinBg};color:${t.pinText};border:2px solid ${t.pinBorder};` : (index % 2 === 0 ? `background:${t.row1};` : `background:${t.row2};`);
const hoverLogic = `onmouseover="const tp=document.getElementById('cpto-hover-image'); tp.src=this.src; tp.style.display='block'; tp.style.left=(event.clientX+20)+'px'; tp.style.top=(event.clientY+20)+'px';" onmousemove="const tp=document.getElementById('cpto-hover-image'); tp.style.left=(event.clientX+20)+'px'; tp.style.top=(event.clientY+20)+'px';" onmouseout="document.getElementById('cpto-hover-image').style.display='none';"`;
const imgTag = p.image ? `<img src="${p.image}" style="width:40px;height:40px;object-fit:cover;border-radius:4px;cursor:zoom-in;" ${hoverLogic}>` : '';
const btnStyle = "width:24px;height:24px;border:none;color:#fff;border-radius:4px;cursor:pointer;font-weight:bold;display:flex;align-items:center;justify-content:center;padding:0;line-height:1;";
const stashUI = `
<div style="display:flex;align-items:center;justify-content:center;gap:8px;">
<button class="stash-dec" data-id="${p.uniqueId}" style="${btnStyle}background:#e74c3c;">-</button>
<span style="font-weight:bold;width:20px;text-align:center;">${qty}</span>
<button class="stash-inc" data-id="${p.uniqueId}" style="${btnStyle}background:#2ecc71;">+</button>
</div>
`;
// Show magic score if magic target is active
let magicBadge = '';
if (mTarget) {
const badgeColor = p.similarity > 80 ? '#27ae60' : (p.similarity > 50 ? '#f39c12' : '#7f8c8d');
magicBadge = `<div style="margin-top:4px;"><span style="background:${badgeColor};color:#fff;padding:2px 6px;border-radius:12px;font-size:11px;font-weight:bold;">πŸ€– ${p.similarity}% Match</span></div>`;
}
tr.innerHTML = `
<td style="padding:10px;border:1px solid ${t.border};text-align:center;cursor:pointer;font-size:20px;" class="pin-btn" data-id="${p.uniqueId}">${pinnedIds.has(p.uniqueId) ? 'πŸ“Œ' : 'πŸ“'}</td>
<td style="padding:10px;border:1px solid ${t.border};">${stashUI}</td>
<td style="padding:5px;border:1px solid ${t.border};text-align:center;">${imgTag}</td>
<td style="padding:10px;border:1px solid ${t.border};">
<span class="magic-btn" data-id="${p.uniqueId}" style="cursor:pointer;font-size:18px;margin-right:5px;filter:grayscale(${mTarget?'1':'0'});" title="Magic Match (Find Similar)">πŸͺ„</span>
<a href="${p.link}" target="_blank" style="color:${isPinned ? t.pinText : t.link};font-weight:bold;text-decoration:none;">${p.trophies.join('')} ${p.name}</a><br>
<small style="opacity:0.7;">${p.brand}</small>
${magicBadge}
</td>
<td style="padding:10px;border:1px solid ${t.border};">
<div>${p.effect ? '🧠 ' + p.effect : ''}</div>
<div style="opacity:0.8;">πŸ‘… ${p.aroma}</div>
</td>
<td style="padding:10px;border:1px solid ${t.border};white-space:nowrap;">
<div>${p.genetics.includes('Sativa')?'β˜€οΈ':p.genetics.includes('Indica')?'πŸŒ™':'βš–οΈ'} ${p.genetics}</div>
<div style="opacity:0.8;">${p.radiation.includes('unbestrahlt')?'🌱':'☒️'} ${p.radiation}</div>
</td>
<td style="padding:10px;border:1px solid ${t.border};font-weight:bold;background-color:${getThcColor(p.thc)};color:${isDark?'#fff':'#000'};text-shadow:0px 1px 2px rgba(0,0,0,0.2);text-align:center;">${p.thc}%</td>
<td style="padding:10px;border:1px solid ${t.border};font-weight:bold;text-align:center;">${p.cbd}%</td>
<td style="padding:10px;border:1px solid ${t.border};font-weight:bold;color:${getPriceColor(p.price)};white-space:nowrap;">€${p.price.toFixed(2)}${p.priceTrend}</td>
<td style="padding:10px;border:1px solid ${t.border};text-align:center;${p.valueScore > 4 ? `font-weight:bold;color:${isDark ? '#4ade80' : '#27ae60'};` : ''}">${p.valueScore}</td>
<td style="padding:10px;border:1px solid ${t.border};text-align:center;white-space:nowrap;">
<a href="${p.flowzzLink}" target="_blank" title="Flowzz" style="text-decoration:none;font-size:18px;margin-right:4px;">πŸ‡©πŸ‡ͺ</a>
<a href="${p.leaflyLink}" target="_blank" title="Leafly" style="text-decoration:none;font-size:18px;">🍁</a>
</td>
`;
tbody.appendChild(tr);
});
table.appendChild(tbody);
tableContainer.innerHTML = '';
tableContainer.appendChild(table);
}
renderTable();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment