1<!DOCTYPE html> 2<html lang="he" dir="rtl"> 3<head> 4<meta charset="UTF-8"/> 5<meta name="viewport" content="width=device-width, initial-scale=1.0"/> 6<!-- Google Consent Mode v2 â must run before any Google scripts -->
7<script> 8window.dataLayer = window.dataLayer || []; 9function gtag(){dataLayer.push(arguments);} 10(function(){ 11 var consent = localStorage.getItem('couponai_cookie_consent'); 12 var granted = consent === 'granted'; 13 gtag('consent', 'default', { 14 ad_storage: granted ? 'granted' : 'denied', 15 analytics_storage: granted ? 'granted' : 'denied', 16 ad_user_data: granted ? 'granted' : 'denied', 17 ad_personalization: granted ? 'granted' : 'denied' 18 }); 19})(); 20gtag('js', new Date()); 21gtag('config', 'G-5E92N5ZJGV', { send_page_view: true }); 22</script>
22 23<!-- Google Analytics 4 -->
vendor: 64 bytes, lines 23-24
23 24<script async src="https://www.googletagmanager.com/gtag/js?id=
24G-5E92N5ZJGV
vendor: 12 bytes, line 24
24"></script>
25<!-- Google AdSense -->
26<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8550253923281063" crossorigin="anonymous"></script>
26 27<!-- AI scraping protection --> 28<meta name="robots" content="noai, noimageai"> 29<meta name="googlebot" content="noai"> 30<meta name="CCBot" content="noindex, nofollow"> 31<meta name="GPTBot" content="noindex, nofollow">
32<script> 33// Early mobile-preview detection â runs before CSS is applied 34(function(){ 35 var m=(location.search||'').match(/[?&]pw=(\d+)/); 36 if(!m)return; 37 var pw=+m[1]; 38 // Override viewport width so @media queries fire correctly 39 var vp=document.querySelector('meta[name="viewport"]'); 40 if(vp)vp.content='width='+pw+',initial-scale=1'; 41 // Store for the later IIFE 42 window._previewWidth=pw; 43})(); 44</script>
44 45<title>×§×פ×× AI â ×§××× ×§×פ×× ×¢××× ××× ××× ×××ת ×שר××××ת</title> 46<meta name="description" content="×§×פ×× AI - ×§××× ×§×פ×× ×¢××× ××× ××× ×××ת ×שר××××ת ××תר×× ××× ×××××××. ×× ××ת ×××ת××ת ×××××¨× × ×××××ר×× ××¢×× - ×××× ×. ×ס×× ××§× ××ת ×¢× ×××¤× ×§×פ×× ×× ×©×תע××× ×× ×××××××ת 24/7. ×× ××× ××ת ×××§×× ××× - ×××× ×!"/> 47<link rel="canonical" href="https://couponai.co.il/"/> 48<link rel="icon" type="image/png" href="/favicon.png"/> 49<link rel="apple-touch-icon" href="/favicon.png"/> 50<!-- Open Graph --> 51<meta property="og:type" content="website"/> 52<meta property="og:url" content="https://couponai.co.il/"/> 53<meta property="og:title" content="×§×פ×× AI â ×§××× ×§×פ×× ×¢××× ××× ××× ×××ת ×שר××××ת"/> 54<meta property="og:description" content="×§××× ×§×פ×× ×¢××× ××× ××× ×××ת ×שר××××ת ××תר×× ××× ×××××××. ×× ××ת ×××ת××ת ×××××¨× × ×××××ר×× ××¢×× - ×××× ×. ×××¤× ×§×פ×× ×× ×©×תע××× ×× ×××××××ת 24/7."/> 55<meta property="og:image" content="https://couponai.co.il/og_image.png"/> 56<meta property="og:locale" content="he_IL"/> 57<!-- Twitter Card --> 58<meta name="twitter:card" content="summary_large_image"/> 59<meta name="twitter:title" content="×§×פ×× AI â ×§××× ×§×פ×× ×¢××× ××× ××× ×××ת ×שר××××ת"/> 60<meta name="twitter:description" content="×§××× ×§×פ×× ×¢××× ××× ××× ×××ת ×שר××××ת ××תר×× ××× ×××××××. ×× ××ת ×××ת××ת ×××××¨× × ×××××ר×× ××¢×× - ×××× ×."/> 61<meta name="twitter:image" content="https://couponai.co.il/og_image.png"/> 62<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet"/>
63<script type="application/ld+json"> 64{ 65 "@context": "https://schema.org", 66 "@type": "WebSite", 67 "name": "×§×פ×× AI", 68 "url": "https://couponai.co.il", 69 "description": "×§××× ×§×פ×× ×¢××× ××× ××× ×××ת ×שר××××ת ××תר×× ××× ×××××××. ×× ××ת ×××ת××ת ××××ר×, × ×××××ר×× ××¢×× â ×××× ×.", 70 "inLanguage": "he-IL", 71 "potentialAction": { 72 "@type": "SearchAction", 73 "target": { 74 "@type": "EntryPoint", 75 "urlTemplate": "https://couponai.co.il/?q={search_term_string}" 76 }, 77 "query-input": "required name=search_term_string" 78 } 79} 80</script>
vendor: 1 bytes, line 80
80
81<script type="application/ld+json"> 82{ 83 "@context": "https://schema.org", 84 "@type": "Organization", 85 "name": "×§×פ×× AI", 86 "url": "https://couponai.co.il", 87 "logo": "https://couponai.co.il/og_image.png", 88 "contactPoint": { 89 "@type": "ContactPoint", 90 "contactType": "customer support", 91 "url": "https://couponai.co.il/contact" 92 } 93} 94</script>
vendor: 1 bytes, line 94
94
95<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
95 96<style> 97*{box-sizing:border-box;margin:0;padding:0} 98:root{ 99 --black:#0a0a0a;--white:#fafaf8;--accent:#00e87a;--accent2:#ffe45e; 100 --gray1:#111;--gray2:#1a1a1a;--gray3:#2a2a2a;--gray4:#3d3d3d; 101 --gray5:#888;--gray6:#bbb; 102 --radius:12px;--radiuslg:20px; 103 --nav-bg:rgba(10,10,10,0.92); 104} 105/* ââ Light mode overrides ââ */ 106body.light{ 107 --black:#f5f5f3;--white:#111110;--accent:#00b85e;--accent2:#c49800; 108 --gray1:#ebebea;--gray2:#e2e2e0;--gray3:#d0d0ce;--gray4:#b0b0ae; 109 --gray5:#606060;--gray6:#383838; 110 --nav-bg:rgba(245,245,243,0.92); 111} 112body{font-family:'DM Sans',sans-serif;background:var(--black);color:var(--white);min-height:100vh;overflow-x:hidden;direction:rtl;transition:background 0.25s,color 0.25s} 113 114.nav{display:flex;align-items:center;justify-content:space-between;padding:18px 32px;border-bottom:1px solid var(--gray3);position:sticky;top:0;background:var(--nav-bg);backdrop-filter:blur(12px);z-index:100;transition:background 0.25s} 115.nav-logo{font-family:'Syne',sans-serif;font-size:20px;font-weight:800;letter-spacing:-0.5px} 116.nav-logo span{color:var(--accent)} 117.nav-links{display:flex;gap:8px} 118.nav-link{padding:7px 16px;border-radius:8px;font-size:13px;cursor:pointer;color:var(--gray6);transition:all 0.15s;border:none;background:transparent;font-family:'DM Sans',sans-serif} 119.nav-link:hover{color:var(--white);background:var(--gray3)} 120.nav-link.active{color:var(--white);background:var(--gray3)} 121.theme-toggle{width:36px;height:36px;border-radius:50%;border:1px solid var(--gray3);background:var(--gray2);color:var(--gray6);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.15s;flex-shrink:0} 122.theme-toggle:hover{border-color:var(--accent);color:var(--accent)} 123 124/* ââ Mobile nav ââ */ 125.burger-btn{display:none;width:36px;height:36px;border-radius:8px;border:1px solid var(--gray3);background:var(--gray2);color:var(--gray6);cursor:pointer;align-items:center;justify-content:center;flex-shrink:0} 126.burger-btn:hover{border-color:var(--accent);color:var(--accent)} 127.nav-drawer{position:fixed;top:0;right:0;height:100%;width:240px;background:var(--gray1);border-left:1px solid var(--gray3);z-index:999;transform:translateX(100%);transition:transform 0.25s ease;display:flex;flex-direction:column;padding:20px 0} 128.nav-drawer.open{transform:translateX(0)} 129.nav-drawer-header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px 14px;border-bottom:1px solid var(--gray3);margin-bottom:8px} 130.nav-drawer-header span{font-family:'Syne',sans-serif;font-size:15px;font-weight:700;color:var(--white)} 131.nav-drawer-close{width:36px;height:36px;border-radius:8px;border:1px solid var(--gray3);background:var(--gray2);color:var(--gray6);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0} 132.nav-drawer-close:hover{color:var(--white);border-color:var(--accent);background:var(--gray3)} 133.nav-drawer .nav-link{display:block;width:100%;text-align:right;padding:12px 20px;border-radius:0;border:none;background:transparent;font-size:15px;color:var(--gray6);cursor:pointer;font-family:'DM Sans',sans-serif;transition:background 0.15s,color 0.15s} 134.nav-drawer .nav-link:hover,.nav-drawer .nav-link.active{background:var(--gray3);color:var(--white)} 135.nav-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:998} 136.nav-backdrop.show{display:block} 137@media(max-width:640px){ 138 .nav{padding:12px 16px;display:grid;grid-template-columns:36px 1fr 36px;align-items:center} 139 .nav-links{display:none} 140 .burger-btn{display:flex} 141 .nav-logo{order:2;text-align:center} 142 .nav>.theme-toggle{order:3} 143 .nav>.burger-btn{order:1} 144} 145body.is-mobile .nav{padding:12px 16px;display:grid;grid-template-columns:36px 1fr 36px;align-items:center} 146body.is-mobile .nav-links{display:none} 147body.is-mobile .burger-btn{display:flex} 148body.is-mobile .nav-logo{order:2;text-align:center} 149body.is-mobile .nav>.theme-toggle{order:3} 150body.is-mobile .nav>.burger-btn{order:1} 151 152/* ââ Mobile homepage ââ */ 153@media(max-width:640px){ 154 #pub-store-daily .daily-row{flex-direction:column!important} 155 #pub-store-grid{grid-template-columns:repeat(3,1fr)!important;gap:10px!important} 156 .pub-hp-wrap{padding:0 14px!important} 157} 158body.is-mobile #pub-store-daily .daily-row{flex-direction:column!important} 159body.is-mobile #pub-store-grid{grid-template-columns:repeat(3,1fr)!important;gap:10px!important} 160body.is-mobile .pub-hp-wrap{padding:0 14px!important} 161 162.page{display:none} 163.page.active{display:block} 164 165.hero{padding:32px 32px 40px;text-align:center;position:relative;overflow:hidden} 166.hero-bg{position:absolute;inset:0;background:radial-gradient(ellipse 60% 40% at 50% 0%,rgba(0,232,122,0.08) 0%,transparent 70%);pointer-events:none} 167.hero-badge{display:inline-flex;align-items:center;gap:6px;background:var(--gray2);border:1px solid var(--gray3);border-radius:20px;padding:5px 14px;font-size:12px;color:var(--gray6);margin-bottom:24px} 168.hero-badge span{width:6px;height:6px;background:var(--accent);border-radius:50%;display:inline-block;animation:pulse 2s infinite} 169@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}} 170.hero h1{font-family:'Syne',sans-serif;font-size:52px;font-weight:800;line-height:1.05;letter-spacing:-2px;margin-bottom:16px} 171.hero h1 em{color:var(--accent);font-style:normal} 172.hero p{font-size:17px;color:var(--gray5);max-width:480px;margin:0 auto 0;line-height:1.6;font-weight:300} 173 174.search-box{max-width:860px;margin:0 auto 48px;position:relative} 175.search-box input{width:100%;padding:22px 24px 22px 150px;background:var(--gray2);border:1px solid var(--gray3);border-radius:var(--radiuslg);font-size:18px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;transition:border-color 0.2s;direction:rtl} 176.search-box input::placeholder{color:var(--gray5)} 177.search-box input:focus{border-color:var(--accent)} 178.search-box button{position:absolute;
178left:8px;top:50%;transform:translateY(-50%);background:var(--accent);color:var(--black);border:none;border-radius:12px;padding:14px 22px;font-size:15px;font-weight:600;cursor:pointer;font-family:'DM Sans',sans-serif;display:flex;align-items:center;gap:7px} 179.search-mode-toggle{display:flex;justify-content:center;margin-top:10px} 180.mode-toggle-track{display:flex;background:var(--gray2);border:1px solid var(--gray3);border-radius:24px;padding:3px;gap:2px} 181.mode-toggle-opt{padding:5px 18px;border-radius:20px;font-size:13px;font-weight:500;cursor:pointer;border:none;background:transparent;color:var(--gray5);font-family:'DM Sans',sans-serif;transition:all 0.18s;display:flex;align-items:center;gap:5px;white-space:nowrap} 182.mode-toggle-opt:hover{color:var(--white)} 183.mode-toggle-opt.active{background:var(--accent);color:var(--black);font-weight:600} 184.mode-toggle-opt.active-deep{background:var(--gray3);color:var(--white);font-weight:600} 185.mode-badge{display:inline-flex;align-items:center;background:rgba(255,255,255,0.15);border-radius:8px;padding:1px 6px;font-size:10px;font-weight:700;letter-spacing:0.3px} 186.sort-tabs{display:flex;gap:6px;padding:0 32px;margin-bottom:14px;flex-wrap:wrap} 187.sort-tab{padding:5px 14px;border:1px solid var(--gray3);border-radius:20px;font-size:12px;background:transparent;color:var(--gray5);cursor:pointer;font-family:'DM Sans',sans-serif;transition:all 0.15s} 188.sort-tab.on{background:var(--gray3);color:var(--white);border-color:var(--gray4)} 189 190.stats-row{display:flex;justify-content:center;gap:40px;margin-bottom:60px;flex-wrap:wrap} 191.stat{text-align:center} 192.stat-n{font-family:system-ui,sans-serif;font-size:42px;font-weight:800;color:var(--white);line-height:1} 193.stat-l{font-size:14px;color:var(--gray5);margin-top:6px} 194 195.section-label{font-family:'Syne',sans-serif;font-size:24px;font-weight:700;color:var(--white);margin-bottom:20px;padding:0 32px} 196.coupon-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;padding:0 32px 60px;max-width:1300px;margin-left:auto;margin-right:auto} 197 198.coupon-card{background:var(--gray2);border:1px solid var(--gray3);border-radius:var(--radius);padding:16px;transition:all 0.2s;position:relative;overflow:hidden;text-align:right} 199.coupon-card:hover{border-color:var(--gray4);transform:translateY(-2px)} 200.coupon-card.featured{border-color:rgba(0,232,122,0.3)} 201.coupon-card.featured::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:var(--accent)} 202.cc-store-row{display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-direction:row-reverse;justify-content:flex-end} 203.cc-logo{width:32px;height:32px;border-radius:8px;background:#fff;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:var(--gray6);overflow:hidden} 204.cc-logo img{width:100%;height:100%;object-fit:contain} 205.cc-store{font-size:13px;font-weight:500} 206.cc-discount{font-family:system-ui,sans-serif;font-size:26px;font-weight:800;letter-spacing:-0.5px;margin-bottom:8px;color:var(--white);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} 207.cc-notes{font-size:13px;color:var(--gray5);margin-bottom:10px;line-height:1.5;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical} 208.cc-code-row{display:flex;align-items:center;gap:6px;margin-bottom:12px;flex-direction:row-reverse} 209/* cc-code is a grid so masked+revealed share the same cell â same height always */ 210.cc-code{font-family:monospace;font-size:13px;background:var(--gray3);border:1px solid var(--gray4);border-radius:6px;padding:10px 12px;flex:1;color:var(--gray6);letter-spacing:1.5px;text-align:left;direction:ltr;overflow:hidden;touch-action:manipulation;display:grid;position:relative;transition:border-color 0.2s,box-shadow 0.2s,background 0.2s,transform 0.1s} 211.cc-code:active{transform:scale(0.98)} 212/* Pre-reveal: Solid Fill green CTA button */ 213.cc-code-row:not(.revealed) .cc-code{background:#00e87a;border-color:#00e87a;cursor:pointer} 214.cc-code-row:not(.revealed) .cc-code:hover{filter:brightness(1.08)} 215.cc-code-row:not(.revealed) .cc-code .cc-code-dots{color:rgba(0,0,0,0.4);animation:none} 216.cc-code-row:not(.revealed) .cc-code .cc-code-end{color:#003d1f} 217.cc-code-row:not(.revealed) .cc-code .cc-code-eye{color:rgba(0,0,0,0.45);opacity:1} 218/* Post-reveal: copy-friendly neutral state */ 219.cc-code-row.revealed .cc-code{background:var(--gray3)!important;border-color:rgba(0,232,122,0.35)!important;filter:none!important;cursor:pointer} 220.cc-code-row.revealed .cc-code:hover{background:var(--gray4)!important;border-color:rgba(0,232,122,0.7)!important;box-shadow:0 0 0 3px rgba(0,232,122,0.12)} 221.cc-copy{background:var(--gray3);border:1px dashed var(--gray4);border-radius:6px;color:var(--accent);cursor:pointer;padding:0 12px;display:flex;align-items:center;justify-content:center;align-self:stretch;opacity:0.8;transition:opacity 0.15s,border-color 0.2s,background 0.2s,box-shadow 0.2s,transform 0.1s;touch-action:manipulation} 222.cc-copy:hover{opacity:1;border-color:rgba(0,232,122,0.7);background:rgba(0,232,122,0.15);box-shadow:0 0 0 3px rgba(0,232,122,0.12);transform:scale(1.05)} 223.cc-copy:active{transform:scale(0.96)} 224.cc-code-row.revealed .cc-copy{border-style:solid;border-color:rgba(0,232,122,0.5);background:rgba(0,232,122,0.08);opacity:1} 225.cc-code-row.revealed .cc-copy:hover{border-color:rgba(0,232,122,0.7);background:rgba(0,232,122,0.2);box-shadow:0 0 0 3px rgba(0,232,122,0.12);transform:scale(1.05)}/* Both states in the same grid cell â no height difference */ 226.cc-code-masked,.cc-code-revealed{grid-area:1/1;display:flex;align-items:center} 227/* Masked: dots + last 2 chars on left, eye icon on right */ 228.cc-code-masked{gap:0;cursor:pointer;transition:opacity 0.15s ease;justify-content:space-between} 229.cc-code-masked.hiding{opacity:0;pointer-events:none} 230/* Peek group: dots+last2 stay flush together */ 231.cc-code-peek{display:flex;align-items:center;gap:0}
232/* Eye icon on right of masked state */ 233.cc-code-eye{color:var(--gray5);display:flex;align-items:center;flex-shrink:0;transition:color .2s;opacity:.7} 234.cc-code:hover .cc-code-eye{color:var(--accent);opacity:1} 235/* Breathing dots */ 236@keyframes cc-dots-breathe{0%,100%{opacity:0.35}50%{opacity:0.7}} 237.cc-code-dots{color:rgba(255,255,255,0.45);font-size:16px;font-weight:700;letter-spacing:3px;white-space:nowrap;animation:cc-dots-breathe 2.4s ease-in-out infinite} 238.cc-code-end{color:var(--accent);font-weight:800;font-size:16px;letter-spacing:3px} 239/* Revealed â crossfades over the masked layer */ 240.cc-code-revealed{opacity:0;transform:translateY(3px);pointer-events:none;color:var(--accent);font-weight:800;letter-spacing:2px;font-size:16px;cursor:pointer;width:100%;transition:opacity 0.25s ease,transform 0.25s ease;font-family:monospace} 241.cc-code-revealed.visible{opacity:1;transform:translateY(0);pointer-events:auto} 242/* Full-field pop on reveal â ::after overlay sweeps in then fades */ 243@keyframes cc-highlight-pop{0%{transform:scaleX(0);opacity:1}55%{transform:scaleX(1);opacity:1}100%{transform:scaleX(1);opacity:0}} 244.cc-highlight::after{content:'';position:absolute;inset:0;background:rgba(0,232,122,0.3);border-radius:6px;transform-origin:left;pointer-events:none;animation:cc-highlight-pop 0.55s ease-out forwards} 245/* Respect reduced-motion */ 246@media(prefers-reduced-motion:reduce){ 247 .cc-highlight::after{animation:none;opacity:0} 248 .cc-code-dots{animation:none} 249 .cc-code-revealed{transition:none} 250 .cc-code-masked{transition:none} 251 .coupon-card:hover{transform:none} 252} 253.shimmer-bg{background:linear-gradient(90deg,var(--gray3) 25%,var(--gray4) 50%,var(--gray3) 75%);background-size:200% 100%;animation:shimmer 1.6s infinite} 254@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}} 255.tip-pulse{animation:tip-dot-bounce 1.2s infinite} 256@keyframes tip-dot-bounce{0%,80%,100%{transform:scale(0.5);opacity:0.3}40%{transform:scale(1);opacity:1}} 257.no-code-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:11px 12px;border-radius:8px;border:1px solid rgba(0,232,122,0.3);background:rgba(0,232,122,0.07);color:var(--accent);font-size:14px;font-weight:600;cursor:pointer;font-family:'DM Sans',sans-serif;transition:all 0.15s;margin-bottom:12px;text-align:center;box-sizing:border-box} 258.no-code-btn:hover{background:rgba(0,232,122,0.15);border-color:var(--accent)} 259.cc-footer{display:flex;justify-content:flex-start;align-items:center;gap:6px;flex-direction:row-reverse} 260.vote-btn{display:flex;align-items:center;gap:4px;border:none;border-radius:8px;padding:4px 10px;font-size:11px;font-weight:500;cursor:pointer;font-family:'DM Sans',sans-serif;transition:all 0.15s} 261.vote-up{background:rgba(0,232,122,0.12);color:var(--accent)} 262.vote-up:hover:not(:disabled){background:rgba(0,232,122,0.25)} 263.vote-down{background:rgba(255,80,80,0.1);color:#ff6060} 264.vote-down:hover:not(:disabled){background:rgba(255,80,80,0.22)} 265.vote-btn:disabled{opacity:0.45;cursor:default} 266.vote-btn.voted{opacity:0.55;cursor:default} 267.cc-age{display:none;position:absolute;top:10px;left:12px;font-size:10px;color:var(--gray5);background:var(--gray3);border-radius:6px;padding:2px 7px;direction:ltr} 268.cc-age.old{color:#ff9060;background:rgba(255,100,60,0.12)} 269body.is-staff .cc-age{display:block} 270.cc-newsletter-badge{display:inline-flex;align-items:center;gap:4px;background:rgba(255,228,94,0.12);border:1px solid rgba(255,228,94,0.35);border-radius:6px;padding:3px 9px;font-size:11px;color:var(--accent2);margin-bottom:8px;font-weight:500} 271 272/* ââ Expired card styles (⤠â10 downvotes) ââ */ 273 274/* Style 1 â red diagonal ribbon */ 275.exp-ribbon{overflow:hidden} 276.exp-ribbon::after{content:'×¤× ×ª××§×£';position:absolute;top:20px;left:-26px;width:96px;background:#e53935;color:#fff;font-size:9px;font-weight:700;padding:4px 0;text-align:center;transform:rotate(-45deg);letter-spacing:0.5px;z-index:5;pointer-events:none} 277/* Push age badge to right so ribbon doesn't cover it */ 278.exp-ribbon .cc-age{left:auto;right:12px;z-index:6} 279 280/* Style 2 â gray fade + red badge */ 281.exp-gray{opacity:0.42;filter:grayscale(0.3)} 282.exp-badge-red{display:inline-flex;align-items:center;gap:5px;background:rgba(229,57,53,0.15);border:1px solid rgba(229,57,53,0.3);color:#e57373;font-size:11px;font-weight:500;border-radius:20px;padding:4px 12px;margin-bottom:8px} 283 284/* Style 3 â red top border + warning row */ 285.exp-border{border-top:3px solid #e53935 !important} 286.exp-warning{font-size:11px;color:#e57373;background:rgba(229,57,53,0.08);border-radius:6px;padding:4px 10px;margin-bottom:8px;display:flex;align-items:center;gap:5px} 287 288/* Style 4 â orange badge at bottom (no strikethrough) */ 289.exp-badge-orange{display:inline-flex;align-items:center;gap:4px;background:rgba(255,152,0,0.15);border:1px solid rgba(255,152,0,0.3);color:#ffb74d;font-size:11px;font-weight:500;border-radius:20px;padding:3px 10px} 290 291/* Style 5 â collapsed card */ 292.exp-collapsed{padding:10px 16px !important;cursor:pointer} 293.exp-collapsed .cc-collapsed-content{display:none} 294.exp-collapsed.open .cc-collapsed-content{display:block} 295.exp-collapsed-header{display:flex;justify-content:space-between;align-items:center;gap:8px} 296 297/* Demo section label */ 298.demo-opt-label{margin-top:10px;text-align:center;font-size:11px;font-weight:600;color:var(--accent);border-top:1px solid var(--gray3);padding-top:8px} 299 300.cats{display:flex;gap:8px;padding:0 32px;flex-wrap:wrap;margin-bottom:24px} 301.cat-btn{padding:7px 16px;border:1px solid var(--gray3);border-radius:20px;font-size:12px;background:transparent;color:var(--gray5);cursor:pointer;transition:all 0.15s;font-family:'DM Sans',sans-serif} 302.cat-btn.on{background:var(--accent);color:var(--black);border-color:var(--accent);font-weight:500} 303.cat-btn:hover:not(.on){border-color:var(--gray4);color:var(--white)} 304 305.admin-layout{display:grid;grid-template-columns:1fr 220px;min-height:calc(100vh - 57px)} 306.sidebar{background:var(--gray1);border-left:1px solid var(--gray3);padding:20px 0;order:2} 307/* Mobile-only elements â hidden on desktop */ 308#mob-backdrop{display:none} 309#mob-close-btn{display:none} 310#mob-menu-bar{display:none} 311.sidebar-section{padding:0 12px;margin-bottom:24px} 312.sidebar-label{font-size:10px;font-weight:500;letter-spacing:2px;color:var(--gray5);text-transform:uppercase;padding:0 12px;margin-bottom:8px;display:block} 313.sidebar-item{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:8px;font-size:13px;
313cursor:pointer;color:var(--gray6);transition:all 0.15s;margin-bottom:2px;border:none;background:transparent;width:100%;text-align:right;font-family:'DM Sans',sans-serif;direction:rtl} 314.sidebar-item:hover{background:var(--gray3);color:var(--white)} 315.sidebar-item.on{background:var(--gray3);color:var(--white)} 316.sidebar-icon{width:16px;height:16px;border-radius:4px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:10px} 317.sidebar-count{margin-right:auto;background:var(--gray3);color:var(--gray5);font-size:10px;padding:2px 7px;border-radius:10px} 318.sidebar-item.on .sidebar-count{background:var(--gray4);color:var(--gray6)} 319 320.admin-main{padding:28px;overflow-y:auto;order:1} 321.admin-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;flex-direction:row-reverse} 322.admin-title{font-family:'Syne',sans-serif;font-size:22px;font-weight:700} 323.admin-actions{display:flex;gap:8px} 324.btn-sm{padding:8px 16px;border-radius:8px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;font-weight:500} 325.btn-ghost{background:transparent;border:1px solid var(--gray3);color:var(--gray6)} 326.btn-ghost:hover{border-color:var(--gray4);color:var(--white)} 327.btn-primary{background:var(--accent);border:none;color:var(--black)} 328.btn-primary:hover{background:#00cc6a} 329 330.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:28px} 331.metric{background:var(--gray2);border:1px solid rgba(255,255,255,0.06);border-radius:20px;padding:20px 18px 16px;text-align:right;position:relative;overflow:hidden;transition:transform 0.15s} 332.metric:hover{transform:translateY(-2px)} 333.metric-icon{font-size:22px;margin-bottom:12px;display:block} 334.metric-val{font-family:system-ui,sans-serif;font-size:42px;font-weight:800;line-height:1;letter-spacing:-1px} 335.metric-label{font-size:11px;color:var(--gray5);margin-top:6px;text-transform:uppercase;letter-spacing:0.5px;font-weight:500} 336.metric-delta{font-size:11px;margin-top:10px;padding-top:10px;border-top:1px solid rgba(255,255,255,0.06)} 337.metric-bar{height:3px;border-radius:2px;margin-top:14px;background:rgba(255,255,255,0.08)} 338.metric-bar-fill{height:100%;border-radius:2px;transition:width 0.6s ease} 339 340.sources-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:24px} 341.source-card{background:var(--gray2);border:1px solid var(--gray3);border-radius:var(--radius);padding:16px;text-align:right} 342.source-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;flex-direction:row-reverse} 343.source-name{font-size:13px;font-weight:500} 344.source-status{display:flex;align-items:center;gap:5px;font-size:11px} 345.source-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0} 346.dot-green{background:var(--accent);animation:pulse 2s infinite} 347.dot-amber{background:var(--accent2)} 348.source-stat{font-size:11px;color:var(--gray5)} 349.source-bar{height:3px;background:var(--gray3);border-radius:2px;margin-top:10px;overflow:hidden} 350.source-fill{height:100%;border-radius:2px;background:var(--accent)} 351 352.table-wrap{background:var(--gray2);border:1px solid var(--gray3);border-radius:var(--radius);overflow:hidden} 353.table-header{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;border-bottom:1px solid var(--gray3);flex-direction:row-reverse} 354.table-title{font-size:13px;font-weight:500} 355.table-search input{background:var(--gray3);border:none;border-radius:8px;padding:6px 12px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;width:180px;direction:rtl} 356table{width:100%;border-collapse:collapse} 357th{font-size:10px;font-weight:500;letter-spacing:1px;text-transform:uppercase;color:var(--gray5);padding:10px 18px;text-align:right;border-bottom:1px solid var(--gray3)} 358td{font-size:12px;padding:11px 18px;border-bottom:1px solid rgba(255,255,255,0.04);color:var(--gray6);vertical-align:middle;text-align:right} 359tr:last-child td{border-bottom:none} 360tr:hover td{background:rgba(255,255,255,0.02)} 361.td-store{color:var(--white);font-weight:500} 362.td-code{font-family:monospace;font-size:11px;background:var(--gray3);padding:2px 8px;border-radius:4px;letter-spacing:1px;color:var(--gray6);direction:ltr;display:inline-block} 363.td-actions{display:flex;gap:6px;justify-content:flex-end} 364.td-btn{background:var(--gray3);border:none;color:var(--gray5);border-radius:6px;padding:4px 10px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif} 365.td-btn:hover{color:var(--white);background:var(--gray4)} 366.td-btn.danger:hover{color:#ff6060}
366 367.td-btn.page-active{background:var(--accent);color:var(--black);font-weight:600} 368 369.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(80px);background:var(--accent);color:var(--black);padding:10px 20px;border-radius:20px;font-size:13px;font-weight:500;transition:transform 0.3s;pointer-events:none;z-index:999;font-family:'DM Sans',sans-serif} 370.toast.show{transform:translateX(-50%) translateY(0)} 371#search-btn{transition:background 0.2s;min-width:110px} 372#search-btn.loading{opacity:0.6;pointer-events:none;cursor:default} 373@keyframes spin{to{transform:rotate(360deg)}} 374.spinner{display:inline-block;width:11px;height:11px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:spin 0.7s linear infinite;margin-left:6px;vertical-align:-1px} 375.ai-banner{background:rgba(0,232,122,0.07);border:1px solid rgba(0,232,122,0.2);border-radius:16px;padding:18px 28px;margin:0 20px 28px;font-size:16px;color:var(--gray6);display:none;direction:rtl;text-align:right;line-height:1.6} 376.ai-banner.show{display:block} 377.ai-banner strong{color:var(--accent);font-size:17px} 378.thinking-dots span{width:7px;height:7px;border-radius:50%;background:var(--accent);animation:tdot 1.2s infinite} 379.thinking-dots{display:inline-flex;gap:3px;align-items:center;vertical-align:middle;margin-left:4px} 380.thinking-dots span{width:5px;height:5px;border-radius:50%;background:var(--accent);animation:tdot 1.2s infinite} 381.thinking-dots span:nth-child(2){animation-delay:0.2s} 382.thinking-dots span:nth-child(3){animation-delay:0.4s} 383@keyframes tdot{0%,80%,100%{opacity:0.2;transform:scale(0.8)}40%{opacity:1;transform:scale(1)}} 384 385/* ââ Mobile admin adaptations (⤠640px) ââââââââââââââââââââââââââââââââââââ */ 386@media (max-width: 640px) { 387 388 /* Single column layout */ 389 .admin-layout { 390 grid-template-columns: 1fr; 391 min-height: calc(100vh - 48px); 392 position: relative; 393 } 394 395 /* Sidebar â hidden slide-in drawer from the right */ 396 .sidebar { 397 position: fixed; 398 top: 0; right: -280px; 399 width: 260px; 400 height: 100vh; 401 z-index: 1000; 402 border-left: 1px solid var(--gray3); 403 padding: 60px 0 20px; 404 transition: right 0.28s cubic-bezier(0.4,0,0.2,1); 405 box-shadow: none; 406 overflow-y: auto; 407 } 408 .sidebar.mob-open { 409 right: 0; 410 box-shadow: -8px 0 32px rgba(0,0,0,0.6); 411 } 412 413 /* Backdrop overlay */ 414 #mob-backdrop { 415 position: fixed; 416 inset: 0; 417 background: rgba(0,0,0,0.5); 418 z-index: 999; 419 } 420 #mob-backdrop.show { display: block; } 421 422 /* Hamburger button */ 423 #mob-menu-btn { 424 display: flex; 425 align-items: center; 426 justify-content: center; 427 width: 36px; height: 36px; 428 background: var(--gray2); 429 border: 1px solid var(--gray3); 430 border-radius: 8px; 431 cursor: pointer; 432 font-size: 18px; 433 color: var(--white); 434 flex-shrink: 0; 435 } 436 437 /* Close button inside drawer */ 438 #mob-close-btn { 439 display: flex !important; 440 position: absolute; 441 top: 14px; left: 14px; 442 width: 28px; height: 28px; 443 align-items: center; justify-content: center; 444 background: var(--gray3); 445 border: none; 446 border-radius: 6px; 447 color: var(--gray5); 448 font-size: 14px; 449 cursor: pointer; 450 } 451 452 /* Main area */ 453 .admin-main { padding: 14px 12px; } 454 455 /* Show mobile menu bar */ 456 #mob-menu-bar { display: flex !important; } 457 458 /* Header */ 459 .admin-header { 460 flex-direction: column; 461 align-items: flex-end; 462 gap: 10px; 463 margin-bottom: 16px; 464 } 465 .admin-title { font-size: 17px; } 466 467 /* Metrics: 2Ã2 */ 468 .metrics { 469 grid-template-columns: repeat(2, 1fr); 470 gap: 8px; 471 margin-bottom: 16px; 472 } 473 .metric { padding: 14px 12px 12px; border-radius: 14px; } 474 .metric-val { font-size: 30px; } 475 .metric-icon { font-size: 18px; margin-bottom: 8px; } 476 477 /* Sources grid: 1 column */ 478 .sources-grid { 479 grid-template-columns: 1fr; 480 margin-bottom: 16px; 481 } 482 483 /* Analytics panels: stack vertically */ 484 #view-dashboard > div[style*="grid-template-columns:1fr 1fr"] { 485 display: flex !important; 486 flex-direction: column !important; 487 gap: 12px !important; 488 margin: 14px 0 !important; 489 } 490 491 /* Analytics inner layout: stack chart + list */ 492 #analytics-panel > div[style*="grid-template-columns:1fr 280px"], 493 #speed-panel > div[style*="grid-template-columns:1fr 280px"] { 494 display: flex !important; 495 flex-direction: column !important; 496 gap: 12px !important; 497 } 498 499 /* Speed cards: 2 per row */ 500 #speed-cards, 501 #analytics-grid { 502 grid-template-columns: repeat(2, 1fr) !important; 503 } 504 505 /* Tables: horizontal scroll */ 506 .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; } 507 table { min-width: 480px; } 508 509 /* Table padding tighter */ 510 th { padding: 8px 10px; font-size: 9px; } 511 td { padding: 8px 10px; font-size: 11px; } 512 513 /* Hide less important columns on mobile */ 514 .col-source, .col-notes, .col-min, 515 th.col-source, td.col-source, 516 th.col-notes, td.col-notes, 517 th.col-min, td.col-min { display: none; } 518 519 /* Table header search */ 520 .table-header { flex-wrap: wrap; gap: 8px; padding: 10px 12px; } 521 .table-search input { width: 140px; } 522 523 /* Action buttons: smaller */ 524 .td-btn { padding: 4px 8px; font-size: 10px; } 525 526 /* Admin search/filter row */ 527 #adm-search-row { flex-wrap: wrap; gap: 6px; } 528 #adm-search-row input, 529 #adm-search-row select { font-size: 12px; } 530 531 /* Pagination */ 532 #adm-pagination { font-size: 11px; gap: 4px; } 533} 534 535/* ââ body.is-mobile â mirrors @media block for iframe preview ââ */ 536 537body.is-mobile .admin-layout { 538 grid-template-columns: 1fr; 539 min-height: calc(100vh - 48px); 540 position: relative; 541} 542body.is-mobile .sidebar { 543 position: fixed; 544 top: 0; right: -280px; 545 width: 260px; 546 height: 100vh; 547 z-index: 1000; 548 border-left: 1px solid var(--gray3); 549 padding: 60px 0 20px; 550 transition: right 0.28s cubic-bezier(0.4,0,0.2,1); 551 box-shadow: none; 552 overflow-y: auto; 553} 554body.is-mobile .sidebar.mob-open { 555 right: 0; 556 box-shadow: -8px 0 32px rgba(0,0,0,0.6); 557} 558body.is-mobile #mob-backdrop { 559 position: fixed; 560 inset: 0; 561 background: rgba(0,0,0,0.5); 562 z-index: 999; 563 display: none; 564} 565body.is-mobile #mob-backdrop.show { display: block; } 566body.is-mobile #mob-menu-btn { 567 display: flex; 568 align-items: center; 569 justify-content: center; 570 width: 36px; height: 36px; 571 background: var(--gray2); 572 border: 1px solid var(--gray3); 573 border-radius: 8px; 574 cursor: pointer; 575 font-size: 18px; 576 color: var(--white); 577 flex-shrink: 0; 578} 579body.is-mobile #mob-close-btn { 580 display: flex !important; 581 position: absolute; 582 top: 14px; left: 14px; 583 width: 28px; height: 28px; 584 align-items: center; justify-content: center; 585 background: var(--gray3); 586 border: none; 587 border-radius: 6px; 588 color: var(--gray5); 589 font-size: 14px; 590 cursor: pointer; 591} 592body.is-mobile .admin-main { padding: 14px 12px; } 593body.is-mobile #mob-menu-bar { display: flex !important; } 594body.is-mobile .admin-header { 595 flex-direction: column; 596 align-items: flex-end; 597 gap: 10px; 598 margin-bottom: 16px; 599} 600body.is-mobile .admin-title { font-size: 17px; } 601body.is-mobile .metrics { 602 grid-template-columns: repeat(2, 1fr); 603 gap: 8px; 604 margin-bottom: 16px; 605} 606body.is-mobile .metric { padding: 14px 12px 12px; border-radius: 14px; } 607body.is-mobile .metric-val { font-size: 30px; } 608body.is-mobile .metric-icon { font-size: 18px; margin-bottom: 8px; } 609body.is-mobile .sources-grid { 610 grid-template-columns: 1fr; 611 margin-bottom: 16px; 612} 613body.is-mobile #view-dashboard > div[style*="grid-template-columns:1fr 1fr"] { 614 display: flex !important; 615 flex-direction: column !important; 616 gap: 12px !important; 617 margin: 14px 0 !important; 618} 619body.is-mobile #analytics-panel > div[style*="grid-template-columns:1fr 280px"], 620body.is-mobile #speed-panel > div[style*="grid-template-columns:1fr 280px"] { 621 display: flex !important; 622 flex-direction: column !important; 623 gap: 12px !important; 624} 625body.is-mobile #speed-cards, 626body.is-mobile #analytics-grid { 627 grid-template-columns: repeat(2, 1fr) !important; 628} 629body.is-mobile .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; } 630body.is-mobile table { min-width: 480px; } 631body.is-mobile th { padding: 8px 10px; font-size: 9px; } 632body.is-mobile td { padding: 8px 10px; font-size: 11px; } 633body.is-mobile .col-source, body.is-mobile .col-notes, body.is-mobile .col-min, 634body.is-mobile th.col-source, body.is-mobile td.col-source, 635body.is-mobile th.col-notes, body.is-mobile td.col-notes, 636body.is-mobile th.col-min, body.is-mobile td.col-min { display: none; } 637body.is-mobile .table-header { flex-wrap: wrap; gap: 8px; padding: 10px 12px; } 638body.is-mobile .table-search input { width: 140px; } 639body.is-mobile .td-btn { padding: 4px 8px; font-size: 10px; }
640body.is-mobile #adm-search-row { flex-wrap: wrap; gap: 6px; } 641body.is-mobile #adm-search-row input, 642body.is-mobile #adm-search-row select { font-size: 12px; } 643body.is-mobile #adm-pagination { font-size: 11px; gap: 4px; } 644 645/* ââ Reveal modal âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 646#reveal-modal-overlay { 647 display: none; position: fixed; inset: 0; 648 background: rgba(0,0,0,0.55); 649 backdrop-filter: blur(3px); 650 -webkit-backdrop-filter: blur(3px); 651 z-index: 6000; 652 align-items: center; justify-content: center; 653} 654#reveal-modal-overlay.active { display: flex; } 655#reveal-modal { 656 background: #0c1f10; 657 border: 1px solid rgba(0,232,122,0.25); 658 border-radius: 20px; 659 width: 92%; max-width: 460px; 660 text-align: center; position: relative; overflow: hidden; 661 box-shadow: 0 12px 50px rgba(0,0,0,0.55); 662 animation: rm-in 0.22s ease; 663 direction: rtl; 664} 665@keyframes rm-in { 666 from { opacity:0; transform:scale(0.93) translateY(12px); } 667 to { opacity:1; transform:scale(1) translateY(0); } 668} 669/* Header */ 670#rm-header { 671 padding: 16px 20px 14px; 672 border-bottom: 1px solid rgba(255,255,255,0.07); 673 position: relative; 674} 675#rm-header-title { 676 font-family: system-ui, sans-serif; font-size: 17px; font-weight: 800; 677 color: #fff; letter-spacing: -0.3px; text-align: center; 678} 679#rm-close { 680 position: absolute; top: 50%; left: 16px; transform: translateY(-50%); 681 background: none; border: none; color: rgba(255,255,255,0.35); 682 font-size: 22px; cursor: pointer; line-height: 1; padding: 2px 4px; 683 transition: color 0.15s; 684} 685#rm-close:hover { color: #fff; }
686/* Body */ 687#rm-body { padding: 20px 50px 16px; } 688#rm-logo { 689 height: 64px; width: fit-content; min-width: 64px; max-width: 220px; 690 border-radius: 14px; overflow: hidden; margin: 0 auto 14px; 691 background: rgba(255,255,255,0.07); 692 display: flex; align-items: center; justify-content: center; 693 box-shadow: 0 2px 10px rgba(0,0,0,0.3); 694} 695#rm-logo img { height: 100%; width: auto; max-width: 220px; display: block; object-fit: contain; } 696#rm-discount { 697 font-family: system-ui, sans-serif; font-size: 34px; font-weight: 800; 698 color: #fff; margin-bottom: 6px; direction: rtl; letter-spacing: -0.5px; 699} 700#rm-notes { 701 font-size: 14px; color: rgba(255,255,255,0.55); 702 margin-bottom: 14px; font-family: 'DM Sans', sans-serif; 703 line-height: 1.5; direction: rtl; 704} 705/* Code row */ 706#rm-code-wrap { 707 display: flex; justify-content: center; margin-bottom: 18px; 708} 709#rm-code-wrap .cc-code-row { 710 gap: 10px; margin-bottom: 0; 711 min-width: 220px; max-width: 100%; 712} 713#rm-code-wrap .cc-code { 714 font-size: 28px !important; padding: 16px 22px; letter-spacing: 4px; 715 background: var(--gray3) !important; 716 border-color: rgba(0,232,122,0.5) !important; 717} 718#rm-code-wrap .cc-code .cc-code-revealed { 719 font-size: 28px !important; letter-spacing: 4px !important; 720} 721#rm-code-wrap .cc-copy { 722 border-style: solid; border-color: rgba(0,232,122,0.5); 723 background: rgba(0,232,122,0.08); opacity: 1; 724 width: 58px; height: auto; flex-shrink: 0; 725} 726#rm-code-wrap .cc-copy svg { width: 20px; height: 20px; } 727#rm-code-wrap .cc-copy:hover { 728 border-color: rgba(0,232,122,0.85); background: rgba(0,232,122,0.2); 729} 730/* Go button â narrow pill, centered */ 731#rm-go-btn-wrap { text-align: center; margin-bottom: 20px; } 732#rm-go-btn { 733 display: block; width: fit-content; margin: 0 auto; 734 background: #00e87a; color: #000; 735 border: none; border-radius: 8px; 736 padding: 13px 36px; font-size: 15px; font-weight: 700; 737 cursor: pointer; text-decoration: none; 738 font-family: system-ui, sans-serif; 739 transition: filter 0.15s, transform 0.1s; 740} 741#rm-go-btn:hover { filter: brightness(1.1); transform: scale(1.02); } 742/* Vote row */ 743#rm-vote-label { 744 font-size: 14px; color: rgba(255,255,255,0.5); 745 margin-bottom: 10px; font-family: 'DM Sans', sans-serif; font-weight: 500; 746} 747#rm-vote-row { 748 display: flex; justify-content: center; gap: 14px; margin-bottom: 4px; 749} 750#rm-vote-row .vote-btn { font-size: 14px; padding: 8px 20px; gap: 7px; } 751#rm-vote-row .vote-btn svg { width: 16px; height: 16px; } 752/* Footer â RTL: store link on left, views on right */ 753#rm-footer { 754 padding: 11px 18px; 755 border-top: 1px solid rgba(255,255,255,0.07); 756 display: flex; align-items: center; justify-content: space-between; 757 gap: 10px; 758} 759#rm-views { 760 font-size: 11px; color: rgba(255,255,255,0.35); 761 font-family: 'DM Sans', sans-serif; white-space: nowrap; 762} 763#rm-more-link { 764 font-size: 12px; color: var(--accent); 765 font-family: 'DM Sans', sans-serif; font-weight: 600; 766 text-decoration: none; white-space: nowrap; transition: opacity 0.15s; 767 direction: rtl; 768} 769#rm-more-link:hover { opacity: 0.75; } 770/* Toast */ 771#rm-toast { 772 position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%) translateY(12px); 773 background: #00e87a; color: #000; font-family: system-ui, sans-serif; 774 font-size: 13px; font-weight: 700; padding: 8px 20px; border-radius: 50px; 775 white-space: nowrap; pointer-events: none; 776 opacity: 0; transition: opacity 0.2s, transform 0.2s; 777} 778#rm-toast.visible { 779 opacity: 1; transform: translateX(-50%) translateY(0); 780} 781</style> 782</head> 783<body> 784 785<!-- PREPROD BANNER â injected by JS if ENVIRONMENT=preprod --> 786<div id="preprod-banner" style="display:none;position:fixed;top:0;left:0;right:0;z-index:99999;background:#f59e0b;color:#000;text-align:center;padding:6px 16px;font-size:12px;font-weight:700;letter-spacing:0.5px;box-shadow:0 2px 8px rgba(0,0,0,0.3)"> 787 ð§ PRE-PROD â ס×××ת ××××§× ×××× Â· ×× ××××ר ××תר ×רש×× 788</div> 789 790<div class="nav"> 791 <div class="nav-logo" onclick="showPage('public')" style="cursor:pointer">×§×פ××<span>AI</span></div> 792 <div class="nav-links" id="main-nav-links"> 793 <button class="nav-link active" onclick="showPage('public')">ר×ש×</button> 794 <a href="/extension" class="nav-link">ת×סף Chrome</a> 795 <a href="/about" class="nav-link">××××ת</a> 796 <a href="/contact" class="nav-link">צ×ר קשר</a> 797 </div> 798 <button class="theme-toggle" id="theme-btn" onclick="toggleTheme()" title="××××£ ××¦× ×ª×¦×××">
799 <svg id="theme-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg> 800 </button> 801 <button class="burger-btn" onclick="_navDrawerOpen()" title="תפר××"> 802 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg> 803 </button> 804</div> 805 806<!-- Mobile nav drawer --> 807<div class="nav-backdrop" id="nav-backdrop" onclick="_navDrawerClose()"></div> 808<div class="nav-drawer" id="nav-drawer"> 809 <div class="nav-drawer-header"> 810 <span>תפר××</span> 811 <button class="nav-drawer-close" onclick="_navDrawerClose()">â</button> 812 </div> 813 <button class="nav-link active" onclick="showPage('public');_navDrawerClose()">ר×ש×</button> 814 <a href="/extension" class="nav-link">ת×סף Chrome</a> 815 <a href="/about" class="nav-link">××××ת</a> 816 <a href="/contact" class="nav-link">צ×ר קשר</a> 817</div> 818 819<!-- PUBLIC --> 820<div class="page active" id="page-public"> 821 <div class="hero"> 822 <div class="hero-bg"></div> 823 <div class="hero-badge"><span></span>×××¤×¢× ×¢× ××× AI - ×תע××× ×× ×©×¢×</div> 824 <h1>×× ××§×פ×× ××.<br><em>× ×צ××× ×××××××ת.</em></h1> 825 <p>×××× × ××××××ת×ת ש×× × ×¡×רקת ×§××צ×ת ×××ר×, × ×××××ר×× ×××ª×¨× ××צע×× 24/7 - ××× ×©×× ×ª×¤×¡×¤×¡× ××£ ×× ×× ××¤× × ×ר××ש×.</p> 826 </div> 827 <div class="stats-row"> 828 <div class="stat"><div class="stat-n" id="stat-total">â</div><div class="stat-l">×§×פ×× ×× ××××ר</div></div> 829 <div class="stat"><div class="stat-n" id="stat-stores">â</div><div class="stat-l">×× ×××ת ××××ר</div></div> 830 <div class="stat"><div class="stat-n">×× ×©×¢×</div><div class="stat-l">×¢×××× ×××××××</div></div> 831 <div class="stat"><div class="stat-n" id="stat-pct">â</div><div class="stat-l">××××ת×× ××¢×××××</div></div> 832 </div> 833 <div style="padding:0 32px 48px;display:flex;flex-direction:column;align-items:center"> 834 <div class="search-box" style="margin:0;width:100%"> 835 <input type="text" id="pub-search" placeholder="×פש ××¤× ×תר ×× ××ת×. ××××××: ksp.co.il ×× × ×××§" onin
835put="filterPub()" onkeydown="if(event.key==='Enter')aiSearch()"/> 836 <button id="search-btn" onclick="aiSearch()"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="6"/><line x1="15" y1="15" x2="21" y2="21"/><path fill="currentColor" stroke="none" d="M17 1l.8 2.4 2.4.8-2.4.8L17 7.4l-.8-2.4L13.8 4.2l2.4-.8z"/><path fill="currentColor" stroke="none" d="M21 9.5l.5 1.5 1.5.5-1.5.5-.5 1.5-.5-1.5-1.5-.5 1.5-.5z"/></svg>××פ×ש AI</button> 837 </div> 838 <div class="search-mode-toggle"> 839 <div class="mode-toggle-track"> 840 <button class="mode-toggle-opt active" id="mode-opt-fast" onclick="setSearchMode('fast')" title="××פ×ש ×××ר ××××ר ש×× × â ת×××× ×××××ת">â¡ ××פ×ש AI</button> 841 <button class="mode-toggle-opt" id="mode-opt-ai" onclick="setSearchMode('ai')" title="ס×רק ×ת ×××× ××¨× × ×××× ××ת â ת×צ××ת ×קס×××××ת">ð ××פ×ש ×ס××× <span class="mode-badge">×××</span></button> 842 </div> 843 </div> 844 </div> 845 846 <div class="ai-banner" id="ai-banner"></div> 847 848 <!-- Loading screen (shown while searching) --> 849 <div id="mini-game-wrap" style="display:none;margin:-8px 20px 0;padding:24px 20px;background:var(--gray2);border:1px solid var(--gray3);border-top:none;border-radius:0"> 850 851 <!-- Skeleton cards --> 852 <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;margin-bottom:20px" id="skel-grid"> 853 <!-- skeletons injected by JS --> 854 </div> 855 856 <!-- Tips carousel --> 857 <div style="background:var(--gray3);border-radius:14px;padding:16px 24px"> 858 <div style="display:flex;align-items:center;justify-content:center;gap:14px"> 859 <div id="tip-icon-main" style="font-size:26px;flex-shrink:0">ð¡</div> 860 <div id="tip-text-main" style="font-size:15px;color:var(--gray6);line-height:1.5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></div> 861 </div> 862 <div style="margin-top:12px;display:flex;justify-content:center"> 863 <div style="width:80px;height:2px;background:var(--gray4);border-radius:2px;overflow:hidden"> 864 <div id="tip-progress-bar" style="height:2px;background:var(--accent);border-radius:2px;width:0%"></div> 865 </div> 866 </div> 867 </div> 868 </div> 869 <!-- Search results (shown after search, below game) --> 870 <div id="search-results-wrap" style="display:none;margin:-1px 20px 28px;background:var(--gray2);border:1px solid var(--gray3);border-top:none;border-radius:0 0 16px 16px;padding:20px 0 8px"> 871 <div class="coupon-grid" id="search-results-grid" style="padding:0 16px"></div> 872 </div> 873 874 <!-- ââ Store homepage (Layout F) ââ --> 875 <div class="pub-hp-wrap" style="max-width:1100px;margin:0 auto;padding:0 32px"> 876 <div id="pub-store-daily" style="margin-bottom:32px"></div> 877 <div id="pub-store-filter" style="display:none"></div> 878 <div style="height:1px;background:var(--gray3);margin-bottom:28px"></div> 879 <div id="pub-store-grid" style="display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-bottom:24px"></div> 880 <div style="text-align:center;margin-bottom:40px"> 881 <p style="color:var(--gray5);font-size:15px;text-align:center;margin:0">××¢×× ×××× ×× ×××ת ××××××ת...</p> 882 </div> 883 </div> 884 885</div> 886 887<!-- ADMIN --> 888<div class="page" id="page-admin"> 889 <div class="admin-layout"> 890 <div class="admin-main"> 891 <!-- Mobile menu toggle â hidden on desktop via CSS --> 892 <div id="mob-menu-bar" style="align-items:center;justify-content:space-between;margin-bottom:14px"> 893 <button id="mob-menu-btn" onclick="openMobMenu()">â°</button> 894 <span id="mob-view-title" style="font-family:'Syne',sans-serif;font-size:16px;font-weight:700">×ש××ר×</span> 895 </div> 896 <div id="view-dashboard"> 897 <div class="admin-header"> 898 <div class="admin-actions"> 899 <button class="btn-sm btn-ghost" onclick="adminLogout()" title="××ª× ×ª×§ ××× ××××" style="color:#ff6b6b;border-color:rgba(255,107,107,0.3)">ðª ×צ×××</button> 900 </div> 901 <div class="admin-title">×ש××ר×</div> 902 </div> 903 <div class="metrics"> 904 <div class="metric" style="background:linear-gradient(135deg,#0d2b1a,var(--gray2))">
905 <span class="metric-icon">ð¢</span> 906 <div class="metric-val" id="adm-active" style="color:#00e87a">â</div> 907 <div class="metric-label">×§×פ×× ×× ×¤×¢××××</div> 908 <div class="metric-delta" id="adm-active-delta" style="color:#00e87a">×××¢×...</div> 909 <div class="metric-bar"><div class="metric-bar-fill" id="adm-active-bar" style="width:0%;background:#00e87a"></div></div> 910 </div> 911 <div class="metric" style="background:linear-gradient(135deg,#0d1f2b,var(--gray2))"> 912 <span class="metric-icon">ðª</span> 913 <div class="metric-val" id="adm-stores" style="color:#63b3ed">â</div> 914 <div class="metric-label">×× ×××ת ×××¢×§×</div> 915 <div class="metric-delta" id="adm-unknown-delta" style="color:var(--gray5)">×××¢×...</div> 916 <div class="metric-bar"><div class="metric-bar-fill" id="adm-stores-bar" style="width:60%;background:#63b3ed"></div></div> 917 </div> 918 <div class="metric" style="background:linear-gradient(135deg,#2b1f0a,var(--gray2))"> 919 <span class="metric-icon">â </span> 920 <div class="metric-val" id="adm-pct" style="color:#ffe45e">â</div> 921 <div class="metric-label">××××ת×× ××¢×××××</div> 922 <div class="metric-delta" id="adm-pct-delta" style="color:var(--gray5)">×××¢×...</div> 923 <div class="metric-bar"><div class="metric-bar-fill" id="adm-pct-bar" style="width:0%;background:#ffe45e"></div></div> 924 </div> 925 <div class="metric" style="background:linear-gradient(135deg,#1f0d2b,var(--gray2))"> 926 <span class="metric-icon">ð·ï¸</span> 927 <div class="metric-val" id="adm-total" style="color:#b794f4">â</div> 928 <div class="metric-label">ס×"× ×§×פ×× ××</div> 929 <div class="metric-delta" id="adm-inactive-delta" style="color:var(--gray5)">×××¢×...</div> 930 <div class="metric-bar"><div class="metric-bar-fill" id="adm-total-bar" style="width:80%;background:#b794f4"></div></div> 931 </div> 932 </div> 933 <!-- ââ Analytics panels â side by side ââââââââââââââââââââââââââââââââ --> 934 <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:24px 0"> 935 936 <!-- ð Usage Analytics --> 937 <div id="analytics-panel-slot"><div id="analytics-panel" style="padding:20px 24px;background:var(--gray2);border:1px solid var(--gray3);border-radius:var(--radiuslg)"> 938 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;gap:12px"> 939 <span style="font-family:'Syne',sans-serif;font-size:16px;font-weight:700;color:var(--white);white-space:nowrap">ð ×× ××××קס ש×××ש</span> 940 <div style="display:flex;gap:3px;background:var(--gray3);border-radius:10px;padding:3px;flex-shrink:0;flex-wrap:wrap"> 941 <button class="atab" data-period="all" onclick="setAtab(this,'all')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×××</button> 942 <button class="atab" data-period="year" onclick="setAtab(this,'year')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×©× ×ª×</button> 943 <button class="atab" data-period="month" onclick="setAtab(this,'month')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×××ש×</button> 944 <button class="atab on" data-period="week" onclick="setAtab(this,'week')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:var(--gray4);color:var(--white)">ש×××¢×</button> 945 <button class="atab" data-period="day" onclick="setAtab(this,'day')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">××××</button> 946 <button class="atab" data-period="custom" onclick="setAtab(this,'custom')" data-dp-trigger style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">ð </button> 947 </div> 948 </div> 949 <div id="analytics-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:10px;margin-bottom:16px"></div> 950 <div style="background:var(--gray3);border-radius:12px;padding:16px 18px"> 951 <div style="font-size:12px;font-weight:600;color:var(--gray6);margin-bottom:12px">ð פע×××ת ××××¨× ×××</div> 952 <div style="position:relative;height:180px;width:100%;direction:ltr">
952<canvas id="chart-activity"></canvas></div> 953 </div> 954 <div style="margin-top:14px;background:var(--gray3);border-radius:12px;padding:14px 16px"> 955 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;gap:8px;flex-wrap:wrap"> 956 <div style="font-size:12px;font-weight:600;color:var(--gray6)">ð ××פ×ש×× × ×¤×צ××</div> 957 <div style="display:flex;gap:2px;background:var(--gray2);border-radius:8px;padding:2px" id="popsearch-period-bar"></div> 958 </div> 959 <div style="display:flex;gap:2px;background:var(--gray2);border-radius:8px;padding:2px;margin-bottom:10px;width:fit-content"> 960 <button id="tsTab-unique" onclick="setTopSearchTab('unique')" style="padding:3px 10px;border:none;border-radius:6px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:var(--gray4);color:var(--white)">××××××</button> 961 <button id="tsTab-grouped" onclick="setTopSearchTab('grouped')" style="padding:3px 10px;border:none;border-radius:6px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×××××</button> 962 </div> 963 <div id="top-searches"></div> 964 </div> 965 </div></div><!-- /analytics-panel-slot --> 966 967 <!-- â¡ Search Speed --> 968 <div id="speed-panel-slot"><div id="speed-panel" style="padding:20px 24px;background:var(--gray2);border:1px solid var(--gray3);border-radius:var(--radiuslg)"> 969 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;gap:12px"> 970 <span style="font-family:'Syne',sans-serif;font-size:16px;font-weight:700;color:var(--white);white-space:nowrap">â¡ ××× × ××פ×ש</span> 971 <div style="display:flex;gap:3px;background:var(--gray3);border-radius:10px;padding:3px;flex-shrink:0;flex-wrap:wrap"> 972 <button class="stab" data-period="all" onclick="setStab(this,'all')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×××</button> 973 <button class="stab" data-period="year" onclick="setStab(this,'year')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×©× ×ª×</button> 974 <button class="stab" data-period="month" onclick="setStab(this,'month')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×××ש×</button> 975 <button class="stab on" data-period="week" onclick="setStab(this,'week')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:var(--gray4);color:var(--white)">ש×××¢×</button> 976 <button class="stab" data-period="day" onclick="setStab(this,'day')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">××××</button> 977 <button class="stab" data-period="custom" onclick="setStab(this,'custom')" data-dp-trigger style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">ð </button> 978 </div> 979 </div> 980 <!-- Search mode toggle --> 981 <div style="display:flex;gap:4px;margin-bottom:14px;background:var(--gray3);border-radius:10px;padding:3px;width:fit-content"> 982 <button id="smode-ai" onclick="setSpeedMode('ai')" style="padding:5px 14px;border:none;border-radius:8px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;background:var(--gray4);color:var(--white)">â¡ ××פ×ש AI</button> 983 <button id="smode-fast" onclick="setSpeedMode('fast')" style="padding:5px 14px;border:none;border-radius:8px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">ð ××פ×ש ×ס××× ×××</button> 984 </div> 985 <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:10px;margin-bottom:16px" id="speed-cards"></div> 986 <div style="background:var(--gray3);border-radius:12px;padding:16px 18px"> 987 <div style="font-size:12px;font-weight:600;color:var(--gray6);margin-bottom:12px" id="speed-chart-label">â±ï¸ ××× ××פ×ש ××××¨× ××× (×©× ××ת)</div> 988 <div style="position:relative;height:180px;width:100%;direction:ltr">
988<canvas id="chart-speed"></canvas></div> 989 </div> 990 <div style="margin-top:14px;background:var(--gray3);border-radius:12px;padding:14px 16px"> 991 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:8px;flex-wrap:wrap"> 992 <div style="font-size:12px;font-weight:600;color:var(--gray6)" id="slowest-label">ð¢ ×××פ×ש×× ××××××× ×××תר</div> 993 <div style="display:flex;gap:2px;background:var(--gray2);border-radius:8px;padding:2px" id="slowest-period-bar"></div> 994 </div> 995 <div id="speed-slowest"></div> 996 </div> 997 </div></div><!-- /speed-panel-slot --> 998 999 <!-- ð« Popup Analytics --> 1000 <div id="popup-panel-slot"><div id="popup-panel" style="padding:20px 24px;background:var(--gray2);border:1px solid var(--gray3);border-radius:var(--radiuslg)"> 1001 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;gap:12px"> 1002 <span style="font-family:'Syne',sans-serif;font-size:16px;font-weight:700;color:var(--white);white-space:nowrap">ð« פ×פ×פ ×§×פ×× ××</span> 1003 <div style="display:flex;gap:3px;background:var(--gray3);border-radius:10px;padding:3px;flex-shrink:0;flex-wrap:wrap"> 1004 <button class="ptab" data-period="all" onclick="setPopupTab(this,'all')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×××</button> 1005 <button class="ptab" data-period="year" onclick="setPopupTab(this,'year')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×©× ×ª×</button> 1006 <button class="ptab" data-period="month" onclick="setPopupTab(this,'month')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×××ש×</button> 1007 <button class="ptab on" data-period="week" onclick="setPopupTab(this,'week')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:var(--gray4);color:var(--white)">ש×××¢×</button> 1008 <button class="ptab" data-period="day" onclick="setPopupTab(this,'day')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">××××</button> 1009 <button class="ptab" data-period="custom" onclick="setPopupTab(this,'custom')" data-dp-trigger style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">ð </button> 1010 </div> 1011 </div> 1012 <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:10px;margin-bottom:16px" id="popup-cards"></div> 1013 <div style="background:var(--gray3);border-radius:12px;padding:14px 16px"> 1014 <div style="font-size:12px;font-weight:600;color:var(--gray6);margin-bottom:10px">ð ×× ×××ת ×¢× ××× ×ר×× ×¤×פ×פ××</div> 1015 <div id="popup-top-stores"></div> 1016 </div> 1017 </div></div><!-- /popup-panel-slot --> 1018 1019 <div id="ext-panel-slot"><div id="ext-panel" style="padding:20px 24px;background:var(--gray2);border:1px solid var(--gray3);border-radius:var(--radiuslg)"> 1020 <!-- Time filter --> 1021 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:8px"> 1022 <div style="font-size:14px;font-weight:700;color:var(--white)">ת×סף Chrome â × ×ª×× × ×©×××ש</div> 1023 <div style="display:flex;gap:4px"> 1024 <button class="etab" data-period="all" onclick="setEtab(this,'all')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×××</button> 1025 <button class="etab" data-period="month" onclick="setEtab(this,'month')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">×××ש×</button> 1026 <button class="etab on" data-period="week" onclick="setEtab(this,'week')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:var(--gray4);color:var(--white)">ש×××¢×</button> 1027 <button class="etab" data-period="day" onclick="setEtab(this,'day')" style="padding:4px 10px;border:none;border-radius:8px;font-size:11px;cursor:pointer;font-family:'DM Sans',sans-serif;background:transparent;color:var(--gray5)">××××</button> 1028 </div> 1029 </div> 1030 1031 <!-- Two stat panels side by side --> 1032 <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px"> 1033 <div style="background:var(--gray3);border-radius:10px;padding:14px"> 1034 <div style="font-size:12px;font-weight:600;color:var(--gray5);margin-bottom:10px">𧩠פ×פ×פ ת×סף</div> 1035 <div id="ext-stats-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:8px"></div> 1036 </div> 1037 <div style="background:var(--gray3);border-radius:10px;padding:14px"> 1038 <div style="font-size:12px;font-weight:600;color:var(--gray5);margin-bottom:10px">ð פ×פ×פ ×¡× ×§× ××ת</div> 1039 <div id="cart-stats-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:8px"></div> 1040 </div> 1041 </div> 1042 1043 <!-- Top stores (combined, with paging) --> 1044 <div style="border-top:1px solid var(--gray3);padding-top:16px;margin-bottom:20px"> 1045 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px"> 1046 <div style="font-size:12px;font-weight:600;color:var(--gray5)">ðª ×× ×××ת ××××××ת â ×××§×ר××</div> 1047 <div style="display:flex;align-items:center;gap:6px"> 1048 <button id="ext-stores-prev" onclick="extStoresPage(-1)" style="background:var(--gray3);border:none;color:var(--gray5);border-radius:6px;padding:2px 8px;cursor:pointer;font-size:13px">â¹</button>
1049 <span id="ext-stores-page-label" style="font-size:11px;color:var(--gray5)"></span> 1050 <button id="ext-stores-next" onclick="extStoresPage(1)" style="background:var(--gray3);border:none;color:var(--gray5);border-radius:6px;padding:2px 8px;cursor:pointer;font-size:13px">âº</button> 1051 </div> 1052 </div> 1053 <div id="ext-top-stores" style="display:flex;flex-direction:column;gap:6px"></div> 1054 </div> 1055 1056 <!-- Comparison chart --> 1057 <div style="border-top:1px solid var(--gray3);padding-top:16px"> 1058 <div style="font-size:12px;font-weight:600;color:var(--gray5);margin-bottom:14px">ð ×ש×××× â פ×פ×פ ת×סף vs ×¡× ×§× ××ת</div> 1059 <div style="display:flex;gap:16px;margin-bottom:12px"> 1060 <div style="display:flex;align-items:center;gap:6px"><div style="width:10px;height:10px;border-radius:2px;background:#00e87a"></div><div style="font-size:11px;color:var(--gray5)">פ×פ×פ ת×סף</div></div> 1061 <div style="display:flex;align-items:center;gap:6px"><div style="width:10px;height:10px;border-radius:2px;background:#60a5fa"></div><div style="font-size:11px;color:var(--gray5)">×¡× ×§× ××ת</div></div> 1062 </div> 1063 <div id="ext-comparison-chart"></div> 1064 </div> 1065 1066 </div></div><!-- /ext-panel-slot --> 1067 1068 </div><!-- end analytics grid --> 1069 <div class="sources-grid"> 1070 <div class="source-card"> 1071 <div class="source-top"><div class="source-status"><div class="source-dot dot-green"></div><span style="color:var(--accent)">פע××</span></div><div class="source-name">××פ×ש AI</div></div> 1072 <div class="source-stat" id="src-search-stat">×××¢×...</div> 1073 <div class="source-bar"><div class="source-fill" id="src-search-bar" style="width:0%"></div></div> 1074 </div> 1075 <div class="source-card"> 1076 <div class="source-top"><div class="source-status"><div class="source-dot dot-green"></div><span style="color:var(--accent)">פע××</span></div><div class="source-name">×ס×ס ×× ×ª×× ××</div></div> 1077 <div class="source-stat" id="src-db-stat">×××¢×...</div> 1078 <div class="source-bar"><div class="source-fill" id="src-db-bar" style="width:0%"></div></div> 1079 </div> 1080 <div class="source-card"> 1081 <div class="source-top"><div class="source-status"><div class="source-dot" id="src-tg-dot" style="background:var(--gray4)"></div><span id="src-tg-status" style="color:var(--gray5)">×××¢×...</span></div><div class="source-name">××××¨× / × ×××××ר××</div></div> 1082 <div class="source-stat" id="src-tg-stat" style="color:var(--gray5)">×××¢×...</div> 1083 <div class="source-bar"><div class="source-fill" id="src-tg-bar" style="width:0%"></div></div> 1084 </div> 1085 <div class="source-card" onclick="_goInstagram()" style="cursor:pointer"> 1086 <div class="source-top"><div class="source-status"><div class="source-dot" id="src-ig-dot" style="background:var(--gray4)"></div><span id="src-ig-status" style="color:var(--gray5)">×××¢×...</span></div><div class="source-name">××× ×¡××ר×</div></div> 1087 <div class="source-stat" id="src-ig-stat" style="color:var(--gray5)">×××¢×...</div> 1088 <div class="source-bar"><div class="source-fill" id="src-ig-bar" style="width:0%"></div></div> 1089 </div> 1090 </div> 1091 <div class="table-wrap"> 1092 <div class="table-header"> 1093 <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap"> 1094 <input type="text" placeholder="××פ×ש..." oninput="filterAdmin(this.value)" style="background:var(--gray3);border:none;border-radius:8px;padding:6px 12px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;width:160px;direction:rtl"/> 1095 <select onchange="admSort(this)" style="background:var(--gray3);border:none;border-radius:8px;padding:6px 10px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;cursor:pointer"> 1096 <option value="newest">ð ××× ××ש</option> 1097 <option value="oldest">ð° ××× ×ש×</option> 1098 <option value="name">ð¤ ×©× ×× ×ת</option> 1099 <option value="disc-pct">% ×× ×× ×××××××</option> 1100 <option value="disc-ils">
1100⪠×× ×× ×××××¢</option> 1101 <option value="source">ð ××§×ר</option> 1102 </select> 1103 <select onchange="admCat(this)" style="background:var(--gray3);border:none;border-radius:8px;padding:6px 10px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;cursor:pointer"> 1104 <option value="">×× ××§×××ר××ת</option> 1105 <option value="Fashion">×××¤× ×</option> 1106 <option value="Tech">××× ××××××</option> 1107 <option value="Food">××××</option> 1108 <option value="Travel">× ×¡××¢×ת</option> 1109 <option value="Vacation">××פש×</option> 1110 <option value="Home">××ת</option> 1111 <option value="Beauty">××פ×</option> 1112 <option value="Health">×ר×××ת</option> 1113 <option value="Kids">×××××</option> 1114 <option value="Sports">ספ×ר×</option> 1115 <option value="Leisure">×¤× ××</option> 1116 <option value="Finance">פ×× × ×¡×</option> 1117 <option value="Other">××ר</option> 1118 </select> 1119 </div> 1120 <div class="table-title">××××צ×× ××ר×× ××</div> 1121 </div> 1122 <table><thead><tr> 1123 <th style="width:36px;color:var(--gray5)">#</th><th style="width:28px"></th><th>×× ×ת</th><th>×§××</th><th>×× ××</th><th class="col-notes">×ער×ת</th><th>×§×××ר××</th><th class="col-source">××§×ר</th><th class="col-source">××× × ×צ×</th><th>× ×צ×</th><th>פ×רס×</th><th>פע×××ת</th> 1124 </tr></thead><tbody id="admin-tbody"></tbody></table> 1125 <div id="admin-pagination"></div> 1126 </div> 1127 </div> 1128 <div id="view-coupons" style="display:none"> 1129 <div class="admin-header"> 1130 <div class="admin-actions"><button class="btn-sm btn-primary" onclick="openAddCouponModal()">+ ××סף ××× ×ת</button></div> 1131 <div class="admin-title">×× ××§×פ×× ××</div> 1132 </div> 1133 <div class="table-wrap"> 1134 <div class="table-header"> 1135 <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap"> 1136 <input type="text" placeholder="××פ×ש..." oninput="filterAdmin(this.value)" style="background:var(--gray3);border:none;border-radius:8px;padding:6px 12px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;width:160px;direction:rtl"/> 1137 <select onchange="admSort(this)" style="background:var(--gray3);border:none;border-radius:8px;padding:6px 10px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;cursor:pointer"> 1138 <option value="newest">ð ××× ××ש</option> 1139 <option value="oldest">ð° ××× ×ש×</option> 1140 <option value="name">ð¤ ×©× ×× ×ת</option> 1141 <option value="disc-pct">% ×× ×× ×××××××</option> 1142 <option value="disc-ils">⪠×× ×× ×××××¢</option> 1143 <option value="source">ð ××§×ר</option> 1144 </select> 1145 <select onchange="admCat(this)" style="background:var(--gray3);border:none;border-radius:8px;padding:6px 10px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;cursor:pointer"> 1146 <option value="">×× ××§×××ר××ת</option> 1147 <option value="Fashion">×××¤× ×</option><option value="Tech">××× ××××××</option> 1148 <option value="Food">××××</option><option value="Travel">× ×¡××¢×ת</option> 1149 <option value="Vacation">××פש×</option><option value="Home">××ת</option> 1150 <option value="Beauty">××פ×</option><option value="Health">×ר×××ת</option> 1151 <option value="Kids">×××××</option><option value="Sports">ספ×ר×</option> 1152 <option value="Leisure">×¤× ××</option><option value="Finance">פ×× × ×¡×</option> 1153 <option value="Other">××ר</option> 1154 </select> 1155 <select onchange="admDisc(this)" style="background:var(--gray3);border:none;border-radius:8px;padding:6px 10px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;cursor:pointer"> 1156 <option value="">×× ×××§×ר×ת</option> 1157 <option value="primary">ð ××פ×ש ×שת×ש</option> 1158 <option value="secondary">ð ××פ×ש ××©× ×</option> 1159 <option value="auto">ð¤ ×××××××</option> 1160 <option value="telegram">âï¸ ×××ר×</option> 1161 </select> 1162 <select onchange="admGeo(this)" style="background:var(--gray3);border:none;border-radius:8px;padding:6px 10px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;cursor:pointer"> 1163 <option value="all">ð ×× ××תר××</option> 1164 <option value="il">ð®ð± ×שר×××</option> 1165 <option value="intl">ð ××× ×××××</option> 1166 </select> 1167 </div> 1168 <div class="table-title" id="adm-table-title">â ×§×פ×× ××</div> 1169 </div> 1170 <table><thead><tr><th style="width:36px;color:var(--gray5)">#</th><th style="width:28px"></th><th>×× ×ת</th><th>×§××</th><th>×× ××</th><th>×ער×ת</th><th>×§×××ר××</th><th>××§×ר</th><th>××× × ×צ×</th><th>× ×צ×</th><th>פ×רס×</th><th>פע×××ת</th></tr></thead><tbody id="coupons-tbody"></tbody></table> 1171 <div id="coupons-pagination"></div> 1172 </div> 1173 </div> 1174 <div id="view-sources" style="display:none"> 1175 <div class="admin-header"> 1176 <div class="admin-actions"><button class="btn-sm btn-primary" onclick="addSourcePrompt('telegram')">+ ××סף ער××¥</button></div> 1177 <div class="admin-title">××§×ר×ת ×××ר×</div> 1178 </div> 1179 1180 <!-- Userbot listener card --> 1181 <div id="userbot-card" style="margin:0 0 18px;background:var(--gray2);border:1px solid var(--gray3);border-radius:12px;padding:16px 20px;display:flex;align-items:center;justify-content:space-between;gap:16px"> 1182 <div style="display:flex;align-items:center;gap:14px"> 1183 <div id="userbot-dot" style="width:10px;height:10px;border-radius:50%;background:var(--gray4);flex-shrink:0;transition:background 0.3s"></div> 1184 <div> 1185 <div style="font-size:14px;font-weight:600;color:var(--white)">Telegram Listener</div> 1186 <div id="userbot-subtitle" style="font-size:12px;color:var(--gray5);margin-top:2px">×××¢×...</div> 1187 </div> 1188 </div> 1189 <div style="display:flex;align-items:center;gap:10px"> 1190 <div id="userbot-stats" style="font-size:11px;color:var(--gray5);text-align:left"></div> 1191 <!-- Toggle switch --> 1192 <label style="display:flex;align-items:center;gap:8px;
1192cursor:pointer;user-select:none"> 1193 <span id="userbot-toggle-label" style="font-size:12px;color:var(--gray5)">××××</span> 1194 <div style="position:relative;width:44px;height:24px" onclick="toggleUserbot()"> 1195 <input type="checkbox" id="userbot-toggle" style="opacity:0;width:0;height:0;position:absolute"> 1196 <div id="userbot-track" style="position:absolute;inset:0;border-radius:12px;background:var(--gray4);transition:background 0.2s;cursor:pointer"></div> 1197 <div id="userbot-thumb" style="position:absolute;top:3px;right:3px;width:18px;height:18px;border-radius:50%;background:white;transition:transform 0.2s;box-shadow:0 1px 4px rgba(0,0,0,0.3)"></div> 1198 </div> 1199 </label> 1200 </div> 1201 </div> 1202 <div class="table-wrap"> 1203 <div class="table-header"> 1204 <div class="table-title">ער×צ×× ××××ר××</div> 1205 <select id="tg-sort" onchange="_renderTelegramTable(_srcData)" style="background:var(--gray3);border:none;border-radius:8px;padding:5px 10px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;cursor:pointer"> 1206 <option value="count">â ××× ×¤×¢××</option> 1207 <option value="recent">â ×¢××× ×</option> 1208 <option value="name">ð¤ ש×</option> 1209 </select> 1210 </div> 1211 <table><thead><tr><th style="width:36px;color:var(--gray5)">#</th><th>×©× ×¢×¨××¥</th><th>ס××´× ×§×פ×× ××</th><th style="color:#ffe45e">××ש×× (7 ××××)</th><th>×§×פ×× ××ר××</th><th>ס×××ס</th><th>פע×××ת</th></tr></thead> 1212 <tbody id="tg-sources-tbody"> 1213 <tr><td colspan="5" style="color:var(--gray5);text-align:center;padding:20px">×××¢×...</td></tr> 1214 </tbody></table> 1215 </div> 1216 </div> 1217 1218 <div id="view-coupon-sites" style="display:none"> 1219 <div class="admin-header"> 1220 <div class="admin-actions"><button class="btn-sm btn-primary" onclick="addTrustedSitePrompt()">+ ××סף ×תר</button></div> 1221 <div class="admin-title">××ª×¨× ×§×פ×× ×× ××××</div> 1222 </div> 1223 <div class="table-wrap"> 1224 <div class="table-header"> 1225 <div class="table-title">×תר×× ××××× ×× ×סר×קת ×§×פ×× ××</div> 1226 </div> 1227 <table><thead><tr> 1228 <th style="width:36px;color:var(--gray5)">#</th> 1229 <th>ש×</th> 1230 <th>×ת××ת</th> 1231 <th>ת×× ×ת URL ××× ×ת</th> 1232 <th>×××××¥</th> 1233 <th>ס×××ס</th> 1234 <th>פע×××ת</th> 1235 </tr></thead> 1236 <tbody id="coupon-sites-tbody"> 1237 <tr><td colspan="7" style="color:var(--gray5);text-align:center;padding:20px">×××¢×...</td></tr> 1238 </tbody></table> 1239 </div> 1240 </div> 1241 1242 <div id="view-automation-scans" style="display:none"> 1243 <div class="admin-header"> 1244 <div class="admin-actions"><button class="btn-sm btn-ghost" onclick="loadAutomationScans()">âº ×¨×¢× ×</button></div> 1245 <div class="admin-title">סר××§×ת ××××××××ת</div> 1246 </div> 1247 1248 <!-- Job B: Trusted coupon sites --> 1249 <div class="table-wrap" style="margin-bottom:24px"> 1250 <div class="table-header"> 1251 <div class="table-title">ð· Job B â סר×קת ××ª×¨× ×§×פ×× ×× (×¤×¢× ×××××)</div> 1252 <div style="display:flex;align-items:center;gap:14px"> 1253 <span id="job-b-status" style="font-size:12px;color:var(--gray5)"></span> 1254 <label style="display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none"> 1255 <span id="job-b-toggle-label" style="font-size:12px;color:var(--gray5)">×××¢×â¦</span> 1256 <div id="job-b-toggle-wrap" style="position:relative;width:44px;height:24px;opacity:0.4;pointer-events:none" onclick="toggleJobB()"> 1257 <input type="checkbox" id="job-b-toggle" style="opacity:0;width:0;height:0;position:absolute"> 1258 <div id="job-b-toggle-track" style="position:absolute;inset:0;border-radius:12px;background:var(--gray4);transition:background 0.2s;cursor:pointer"></div> 1259 <div id="job-b-toggle-thumb" style="position:absolute;top:3px;right:3px;width:18px;height:18px;border-radius:50%;background:white;transition:transform 0.2s;box-shadow:0 1px 4px rgba(0,0,0,0.3)"></div> 1260 </div> 1261 </label> 1262 </div> 1263 </div> 1264 1265 <!-- Job B Config Panel --> 1266 <div style="display:flex;flex-wrap:wrap;gap:16px;padding:12px 16px 16px;border-bottom:1px solid var(--gray3)"> 1267 <label style="display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--gray5);min-width:140px">
1268 <span>ð¤ ×קס׳ ×××§× ×× ×ת××××</span> 1269 <input id="cfg-max-tokens" type="number" min="100" max="8192" style="background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:5px 8px;color:var(--white);font-size:13px;width:100%" value="1500"> 1270 </label> 1271 <label style="display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--gray5);min-width:140px"> 1272 <span>ð ×קס׳ ×¢××××× ××תר</span> 1273 <input id="cfg-max-urls" type="number" min="5" max="500" style="background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:5px 8px;color:var(--white);font-size:13px;width:100%" value="50"> 1274 </label> 1275 <label style="display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--gray5);min-width:140px"> 1276 <span>ð IL Coupon ×קס׳ ×¢×××××</span> 1277 <input id="cfg-il-coupon-max" type="number" min="5" max="500" style="background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:5px 8px;color:var(--white);font-size:13px;width:100%" value="50"> 1278 </label> 1279 <label style="display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--gray5);min-width:140px"> 1280 <span>ð ×ª×§×¨× ××תר (קר×××ת)</span> 1281 <input id="cfg-per-site-cap" type="number" min="1" max="100" style="background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:5px 8px;color:var(--white);font-size:13px;width:100%" value="15" oninput="updateJobBEstCost()"> 1282 </label> 1283 <label style="display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--gray5);min-width:140px"> 1284 <span>ðµ תקרת ×¢××ת ($)</span> 1285 <input id="cfg-max-cost" type="number" min="0.5" max="50" step="0.5" style="background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:5px 8px;color:var(--white);font-size:13px;width:100%" value="5" oninput="updateJobBEstCost()"> 1286 </label> 1287 <div style="display:flex;flex-direction:column;justify-content:flex-end;gap:4px"> 1288 <button class="btn-sm btn-primary" onclick="saveJobBConfig()" id="job-b-config-save-btn">ש××ר</button> 1289 <div id="job-b-config-status" style="font-size:11px;color:var(--gray5);display:none"></div> 1290 <div id="job-b-est-cost" style="font-size:11px;color:var(--gray5)"></div> 1291 </div> 1292 </div> 1293 1294 <div style="padding:12px 16px;font-size:13px;color:var(--gray5);line-height:1.7"> 1295 ס×רק ×× ×תר ×<b style="color:var(--white)">××ª×¨× ×§×פ×× ×× ××××</b> ×¤×¢× ××××× ×-<b style="color:var(--white)">02:00 שע×× ×שר××</b>.<br> 1296 ×× ×תר × ×¡×¨×§ ×¤×¢× ××ת â ×× ××¤× ×× ×ת. ××××¥ ×§×פ×× ×× ××× ××פ×× ×פע××××. 1297 </div> 1298 <div id="job-b-log" style="margin:0 16px 14px;padding:12px 16px;background:var(--gray2);border:1px solid var(--gray3);border-radius:10px;font-size:12px;display:none"> 1299 <div style="display:flex;gap:20px;flex-wrap:wrap;margin-bottom:8px"> 1300 <span><span style="color:var(--gray5)">ר××¦× ××ר×× ×: </span><b id="job-b-log-time" style="color:var(--white)">â</b></span> 1301 <span><span style="color:var(--gray5)">ס×××ס: </span><b id="job-b-log-status">â</b></span> 1302 <span><span style="color:var(--gray5)">×תר×× ×©× ×¡×¨×§×: </span><b id="job-b-log-sites" style="color:var(--accent)">â</b></span> 1303 <span><span style="color:var(--gray5)">×§×פ×× ×× ×©× ×צ××: </span><b id="job-b-log-coupons" style="color:var(--accent)">â</b></span> 1304 <span><span style="color:var(--gray5)">×¢××ת ××ש: </span><b id="job-b-log-cost" style="color:#fc8181">â</b></span> 1305 </div> 1306 <div id="job-b-log-errors" style="display:none"> 1307 <div style="color:#fc8181;font-weight:600;margin-bottom:4px">ש××××ת:</div> 1308 <ul id="job-b-log-errors-list" style="margin:0;padding-right:16px;color:#fc8181;line-height:1.8"></ul> 1309 </div> 1310 </div> 1311 <table><thead><tr> 1312 <th>#</th><th>×תר</th><th>ש×××</th> 1313 <th style="color:var(--gray5)">×¢××××× × ×¡×¨×§×</th> 1314 <th style="color:#ffe45e">×§×פ×× ×× ××ש××</th> 1315 <th style="color:var(--gray5)">××ר ×§×××</th> 1316 <th>ש××××ת</th> 1317 <th style="color:#f6ad55">×××§× ×× in/out</th> 1318 <th style="color:#fc8181">×¢××ת $</th> 1319 <th>ס×××ס</th> 1320 </tr></thead> 1321 <tbody id="job-b-tbody"><tr><td colspan="10" style="color:var(--gray5);text-align:center;padding:16px">×××¢×...</td></tr></tbody></table> 1322 <div style="padding:10px 16px 14px"> 1323 <div style="font-size:11px;color:var(--gray5);margin-bottom:6px">××ס××ר×× (90 ×××)</div> 1324 <div id="job-b-history" style="color:var(--gray5);font-size:12px">×××¢×...</div> 1325 </div> 1326 </div> 1327 1328 <!-- Job C: Per-store DDG --> 1329 <div class="table-wrap"> 1330 <div class="table-header"> 1331 <div class="table-title">ð Job C â סר×קת ×× ×××ת DDG (×¤×¢× ×××××, 60 ×©× ×³ ××× ×× ×ת)</div>
1332 <span id="job-c-status" style="font-size:12px;color:var(--gray5)"></span> 1333 </div> 1334 <div style="padding:12px 16px;font-size:13px;color:var(--gray5);line-height:1.7"> 1335 ×ר××¥ <code style="background:var(--gray3);padding:2px 6px;border-radius:4px">discover(store)</code> ××× ×× ×ת פע×××, ×ס××ר ××¤× <b style="color:var(--white)">last_scanned_at</b> â ×××©× ×ת ×××תר ×§×××.<br> 1336 ×ר××× ×©× <b style="color:var(--white)">60 ×©× ××ת</b> ××× ×× ×× ×ת ××× ×עת ×ס××ת DDG. 1337 </div> 1338 <div id="job-c-log" style="margin:0 16px 14px;padding:12px 16px;background:var(--gray2);border:1px solid var(--gray3);border-radius:10px;font-size:12px;display:none"> 1339 <div style="display:flex;gap:20px;flex-wrap:wrap;margin-bottom:8px"> 1340 <span><span style="color:var(--gray5)">ר××¦× ××ר×× ×: </span><b id="job-c-log-time" style="color:var(--white)">â</b></span> 1341 <span><span style="color:var(--gray5)">ס×××ס: </span><b id="job-c-log-status">â</b></span> 1342 <span><span style="color:var(--gray5)">×× ×××ת ×©× ×¡×¨×§×: </span><b id="job-c-log-stores" style="color:var(--accent)">â</b></span> 1343 <span><span style="color:var(--gray5)">×§×פ×× ×× ×©× ×צ××: </span><b id="job-c-log-coupons" style="color:var(--accent)">â</b></span> 1344 </div> 1345 <div id="job-c-log-errors" style="display:none"> 1346 <div style="color:#fc8181;font-weight:600;margin-bottom:4px">ש××××ת:</div> 1347 <ul id="job-c-log-errors-list" style="margin:0;padding-right:16px;color:#fc8181;line-height:1.8"></ul> 1348 </div> 1349 </div> 1350 <div style="padding:0 16px 14px;display:flex;gap:24px;flex-wrap:wrap"> 1351 <div style="font-size:13px"><span style="color:var(--gray5)">ס××´× ×× ×××ת פע×××ת: </span><b id="job-c-total" style="color:var(--white)">â</b></div> 1352 <div style="font-size:13px"><span style="color:var(--gray5)">× ×¡×¨×§× ××××: </span><b id="job-c-scanned-today" style="color:var(--accent)">â</b></div> 1353 <div style="font-size:13px"><span style="color:var(--gray5)">××ת×× ×ת ×סר××§×: </span><b id="job-c-pending" style="color:#f6c90e">â</b></div> 1354 <div style="font-size:13px"><span style="color:var(--gray5)">×××× ×ת×ר: </span><b id="job-c-next" style="color:var(--gray5)">â</b></div> 1355 </div> 1356 <table><thead><tr> 1357 <th>#</th> 1358 <th>×× ×ת</th> 1359 <th>סר××§× ××ר×× ×</th> 1360 <th style="color:var(--accent)">ס××´× ×§×פ×× ××</th> 1361 <th style="color:var(--gray5)">××¤× × ×¡×¨××§×</th> 1362 <th style="color:#ffe45e">×§×פ×× ×× ××ש××</th> 1363 <th>ס×××ס</th> 1364 </tr></thead> 1365 <tbody id="job-c-tbody"><tr><td colspan="7" style="color:var(--gray5);text-align:center;padding:16px">×××¢×...</td></tr></tbody></table> 1366 1367 <!-- Daily summary --> 1368 <div style="margin:16px 0 0"> 1369 <div style="font-size:12px;font-weight:600;color:var(--gray6);margin-bottom:10px">ð ××ס××ר×× ××××ת</div> 1370 <div id="job-c-history" style="color:var(--gray5);font-size:12px">×××¢×...</div> 1371 </div> 1372 </div> 1373 </div> 1374 1375 <div id="view-web" style="display:none"> 1376 <div class="admin-header"> 1377 <div class="admin-actions"> 1378 <button class="btn-sm btn-primary" onclick="addSourcePrompt('web')">+ ××סף ×תר</button> 1379 </div> 1380 <div class="admin-title">×תר×× ×©× ×¡×¨×§×</div> 1381 </div> 1382 <!-- IL/International filter --> 1383 <div style="display:flex;gap:6px;padding:0 0 14px;flex-wrap:wrap"> 1384 <button class="sort-tab on" id="web-geo-all" onclick="_setWebGeo('all',this)" >×××</button> 1385 <button class="sort-tab" id="web-geo-il" onclick="_setWebGeo('il',this)" >×שר××× ð®ð±</button> 1386 <button class="sort-tab" id="web-geo-intl" onclick="_setWebGeo('intl',this)" >××× ××××× ð</button> 1387 </div> 1388 <div class="table-wrap"> 1389 <div class="table-header"> 1390 <div class="table-title">×תר×× ×××¢×§×</div> 1391 <select id="web-sort" onchange="WEB.page=0;_renderWebTable(_srcData)" style="background:var(--gray3);border:none;border-radius:8px;padding:5px 10px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;cursor:pointer"> 1392 <option value="new">ð ××ש ×§×××</option> 1393 <option value="count">â ××× ×¤×¢××</option> 1394 <option value="recent">â ×¢××× ×</option> 1395 <option value="name">ð¤ ש×</option> 1396 </select> 1397 </div> 1398 <table><thead><tr><th style="width:36px;color:var(--gray5)">#</th><th>×תר</th><th>×§×פ×× ×× ×©× ×צ××</th><th>סר××§× ××ר×× ×</th><th>ת×ר×× ××ספ×</th><th>× ×××§</th><th>פע×××ת</th></tr></thead> 1399 <tbody id="web-sources-tbody"> 1400 <tr><td colspan="7" style="color:var(--gray5);text-align:center;padding:20px">×××¢×...</td></tr> 1401 </tbody></table> 1402 <div id="web-pagination"></div> 1403 </div> 1404 </div> 1405 1406 <div id="view-instagram" style="display:none"> 1407 <div class="admin-header"> 1408 <div class="admin-actions"> 1409 <button class="btn-sm btn-ghost" onclick="igTestLogin(this)">ð ××××§ ××××ר</button> 1410 <button class="btn-sm btn-primary" id="ig-scan-all-btn" onclick="igScanAll(this)">ð¸ סר××§ ×××</button> 1411 </div> 1412 <div class="admin-title">××× ×¤×××× ×¡×¨××</div> 1413 </div> 1414 <div class="table-wrap"> 1415 <div class="table-header"> 1416 <div class="table-title" id="ig-table-title">×××¢× ×¤×¨×פ××××...</div> 1417 <select id="ig-sort" onchange="_renderInstagramTable()" style="background:var(--gray3);border:none;border-radius:8px;padding:5px 10px;font-size:12px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;cursor:pointer"> 1418 <option value="count">â ××× ×¤×¢××</option> 1419 <option value="name">ð¤ ש×</option> 1420 <option value="recent">â ×¢××× ×</option> 1421 </select> 1422 </div> 1423 <table><thead><tr> 1424 <th style="width:36px;color:var(--gray5)">#</th> 1425 <th>×©× / ×× ××</th> 1426 <th>×§×פ×× ×× ×©× ×צ××</th> 1427 <th>סר××§× ××ר×× ×</th> 1428 <th>ס×××ס</th> 1429 <th>פע×××ת</th> 1430 </tr></thead> 1431 <tbody id="ig-sources-tbody"> 1432 <tr><td colspan="6" style="color:var(--gray5);text-align:center;padding:24px">×××¢×...</td></tr> 1433 </tbody></table> 1434 </div> 1435 </div> 1436 1437 <div id="view-no-url" style="display:none"> 1438 <div class="admin-header"> 1439 <div class="admin-actions">
1440 <button class="btn-sm btn-ghost" onclick="loadNoUrlCoupons()">âº ×¨×¢× ×</button> 1441 </div> 1442 <div class="admin-title">â ï¸ ××× ×תר ××¢×</div> 1443 </div> 1444 <div style="padding:0 0 14px;font-size:13px;color:var(--gray6);line-height:1.6"> 1445 ×§×פ×× ×× ×©× ×©××¨× ×××§×ר×ת ש××× ×× × ×××××× ×¢×××¨× â ××פת×ר "××¦× ××צע ××תר" ××סתר. 1446 ××סף ×××××× ××× ×× ×ת ××× ×תק×, ×× ×××§ ×× ×× ×ª×× ×©×××. 1447 </div> 1448 <div style="display:flex;gap:6px;padding:0 0 14px;flex-wrap:wrap"> 1449 <button class="sort-tab nourl-geo-btn on" onclick="_noUrlSetGeo('all',this)">×××</button> 1450 <button class="sort-tab nourl-geo-btn" onclick="_noUrlSetGeo('il',this)">×שר××× ð®ð±</button> 1451 <button class="sort-tab nourl-geo-btn" onclick="_noUrlSetGeo('intl',this)">××× ××××× ð</button> 1452 </div> 1453 <div class="table-wrap"> 1454 <table id="nourl-table"> 1455 <thead><tr> 1456 <th style="width:36px;color:var(--gray5)">#</th> 1457 <th>×× ×ת + ×§×פ×× ××</th> 1458 <th style="min-width:185px">×××××× (×ת××§××)</th> 1459 <th colspan="4">פע×××ת</th> 1460 </tr></thead> 1461 <tbody id="nourl-tbody"> 1462 <tr><td colspan="7" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr> 1463 </tbody> 1464 </table> 1465 <div id="nourl-pagination"></div> 1466 </div> 1467 </div> 1468 1469 <div id="view-newsletters" style="display:none"> 1470 <div class="admin-header"> 1471 <div class="admin-actions"> 1472 <button class="btn-sm btn-ghost" onclick="scanNewsletters(this,7)">ð¬ סר××§ 7 ××××</button> 1473 <button class="btn-sm btn-ghost" onclick="scanNewsletters(this,30)">ð¬ סר××§ 30 ××××</button> 1474 <button class="btn-sm btn-ghost" onclick="loadNewsletterStores(true)">âº ×¨×¢× × ×¨×©×××</button> 1475 </div> 1476 <div class="admin-title">âï¸ × ×××××ר××</div> 1477 </div> 1478 <div style="padding:20px 24px 8px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px"> 1479 <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap"> 1480 <div style="font-size:13px;font-weight:600;color:var(--white)">ð§ <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="aecdc1dbdec1c0cfc79adbeec9c3cfc7c280cdc1c3">[email protected]</a></div> 1481 <div style="display:inline-flex;align-items:center;gap:5px;background:rgba(0,232,122,0.12);border:1px solid rgba(0,232,122,0.3);border-radius:20px;padding:3px 10px;font-size:11px;color:var(--accent)"> 1482 <span style="width:6px;height:6px;border-radius:50%;background:var(--accent);display:inline-block;animation:pulse 2s infinite"></span> 1483 ס×רק ××××××× Â· ×× 1484 <select id="nl-interval-select" onchange="setNlInterval(this)" 1485 style="background:transparent;border:none;color:var(--accent);font-size:11px;cursor:pointer;outline:none;padding:0 2px"> 1486 <option value="900">15 ××§×ת</option> 1487 <option value="1800">30 ××§×ת</option> 1488 <option value="3600" selected>שע×</option> 1489 <option value="7200">2 שע×ת</option> 1490 <option value="21600">6 שע×ת</option> 1491 <option value="43200">12 שע×ת</option> 1492 <option value="86400">24 שע×ת</option> 1493 </select> 1494 </div> 1495 </div> 1496 <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center"> 1497 <button id="nl-filter-all" class="sort-tab on" onclick="_nlFilter('all',this)">×××</button> 1498 <button id="nl-filter-il" class="sort-tab" onclick="_nlFilter('il',this)">ð®ð± ×שר×××</button> 1499 <button id="nl-filter-intl" class="sort-tab" onclick="_nlFilter('intl',this)">ð ××× ×××××</button>
1500 <span style="width:1px;height:20px;background:var(--gray3);margin:0 2px"></span> 1501 <button id="nl-filter-reg" class="sort-tab" onclick="_nlFilter('reg',this)">â רש××××</button> 1502 <button id="nl-filter-unreg" class="sort-tab" onclick="_nlFilter('unreg',this)">â ×× ×¨×©××××</button> 1503 <span style="width:1px;height:20px;background:var(--gray3);margin:0 2px"></span> 1504 <select id="nl-sort-sel" onchange="_nlSetSortKey(this.value)" style="background:var(--gray2);color:var(--gray6);border:1px solid var(--gray3);border-radius:8px;padding:3px 8px;font-size:12px;cursor:pointer;outline:none;direction:rtl"> 1505 <option value="status">××××: ס×××ס</option> 1506 <option value="name">××××: ש×</option> 1507 <option value="emails">××××: ××××××</option> 1508 <option value="coupons">××××: ×§×פ×× ××</option> 1509 <option value="date">××××: ת×ר××</option> 1510 </select> 1511 <button id="nl-sort-dir-btn" onclick="_nlToggleDir()" title="××¤× ××××× ××××" style="background:var(--gray2);color:var(--gray6);border:1px solid var(--gray3);border-radius:8px;padding:3px 9px;font-size:13px;cursor:pointer;line-height:1">â</button> 1512 </div> 1513 </div> 1514 <div id="nl-stats-bar" style="padding:0 24px 12px;font-size:12px;color:var(--gray5)">×××¢×...</div> 1515 <div id="nl-result" style="display:none;margin:0 24px 12px;background:var(--gray2);border:1px solid var(--gray3);border-radius:10px;padding:14px 18px;direction:rtl"> 1516 <div id="nl-result-text" style="font-size:13px;color:var(--white)"></div> 1517 </div> 1518 <div class="table-wrap" style="padding:0 24px 16px"> 1519 <table id="nl-stores-table" style="width:100%;border-collapse:collapse"> 1520 <thead> 1521 <tr style="border-bottom:1px solid var(--gray3)"> 1522 <th style="text-align:center;padding:10px 8px;font-size:12px;color:var(--gray5);font-weight:500;width:36px">#</th> 1523 <th onclick="_nlSort('name')" style="text-align:right;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500;cursor:pointer;user-select:none">×× ×ת â</th> 1524 <th onclick="_nlSort('status')" style="text-align:center;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500;cursor:pointer;user-select:none">ס×××ס â</th> 1525 <th onclick="_nlSort('emails')" style="text-align:center;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500;cursor:pointer;user-select:none">×××××× â</th> 1526 <th onclick="_nlSort('coupons')" style="text-align:center;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500;cursor:pointer;user-select:none">×§×פ×× ×× â</th> 1527 <th onclick="_nlSort('date')" style="text-align:center;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500;cursor:pointer;user-select:none">×××× ××ר×× â</th> 1528 <th style="text-align:center;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500">ש×תפ×ת</th> 1529 <th style="text-align:center;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500">פע×××ת</th> 1530 </tr> 1531 </thead> 1532 <tbody id="nl-stores-tbody"> 1533 <tr><td colspan="8" style="text-align:center;padding:40px;color:var(--gray5)">×××¢×...</td></tr> 1534 </tbody> 1535 </table> 1536 </div> 1537 <div id="nl-pagination" style="display:flex;align-items:center;justify-content:center;gap:6px;margin:0 24px 32px;flex-wrap:wrap"></div> 1538 </div> 1539 1540 <div id="view-affiliate" style="display:none"> 1541 <div class="admin-header"> 1542 <div class="admin-actions"> 1543 <button class="btn-sm btn-ghost" onclick="_affRender()">âº ×¨×¢× ×</button> 1544 </div> 1545 <div class="admin-title">ð¤ ת××× ××ת ש×תפ××</div> 1546 </div> 1547 <div id="aff-stats-bar" style="padding:12px 24px 0;font-size:12px;color:var(--gray5)"></div> 1548 <div style="padding:10px 24px 6px;display:flex;gap:6px;flex-wrap:wrap;align-items:center"> 1549 <button id="aff-f-all" class="sort-tab on" onclick="_affSetFilter('all',this)">×××</button> 1550 <button id="aff-f-net" class="sort-tab" onclick="_affSetFilter('network',this)">ð רשת×ת</button> 1551 <button id="aff-f-dir" class="sort-tab" onclick="_affSetFilter('direct',this)">ð¯ ×ש×ר×ת</button> 1552 <button id="aff-f-il" class="sort-tab" onclick="_affSetFilter('il',this)">ð®ð± ×שר×××</button>
1553 <span style="width:1px;height:20px;background:var(--gray3);margin:0 2px"></span> 1554 <button id="aff-f-reg" class="sort-tab" onclick="_affSetFilter('registered',this)">â רש××××</button> 1555 <button id="aff-f-pend" class="sort-tab" onclick="_affSetFilter('pending',this)">â³ ××××ª× ×</button> 1556 <button id="aff-f-none" class="sort-tab" onclick="_affSetFilter('none',this)">â ×× ×ת××ת×</button> 1557 <span style="width:1px;height:20px;background:var(--gray3);margin:0 2px"></span> 1558 <select id="aff-sort-sel" onchange="_affSetSortKey(this.value)" style="background:var(--gray2);color:var(--gray6);border:1px solid var(--gray3);border-radius:8px;padding:3px 8px;font-size:12px;cursor:pointer;outline:none;direction:rtl"> 1559 <option value="status">××××: ס×××ס</option> 1560 <option value="name">××××: ש×</option> 1561 <option value="commission">××××: ×¢×××</option> 1562 </select> 1563 <button id="aff-sort-dir-btn" onclick="_affToggleDir()" title="××¤× ××××× ××××" style="background:var(--gray2);color:var(--gray6);border:1px solid var(--gray3);border-radius:8px;padding:3px 9px;font-size:13px;cursor:pointer;line-height:1">â</button> 1564 </div> 1565 <div class="table-wrap" style="padding:12px 24px 32px"> 1566 <table style="width:100%;border-collapse:collapse"> 1567 <thead> 1568 <tr style="border-bottom:1px solid var(--gray3)"> 1569 <th style="text-align:right;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500">ת××× ×ת</th> 1570 <th style="text-align:right;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500">×× ×××ת ×××ס×ת</th> 1571 <th style="text-align:center;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500">×¢×××</th> 1572 <th style="text-align:center;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500">ס×××ס</th> 1573 <th style="text-align:center;padding:10px 12px;font-size:12px;color:var(--gray5);font-weight:500">פע×××</th> 1574 </tr> 1575 </thead> 1576 <tbody id="aff-tbody"></tbody> 1577 </table> 1578 </div> 1579 </div> 1580 1581 <div id="view-pending" style="display:none"> 1582 <div class="admin-header"> 1583 <div class="admin-actions"> 1584 <button class="btn-sm btn-ghost" onclick="loadPendingView()">âº ×¨×¢× ×</button> 1585 <button class="btn-sm btn-ghost" id="backfill-btn" onclick="runBackfill(this)" title="סר××§ ×§×פ×× ×× ×§××××× ×¢× ×¡×××ס unknown ×××§×ר×ת ×× ×××ר×× ×××¢×ר ×××ª× ×××××§×">ð ××× ××§×ר×ת ××ש××</button> 1586 <span style="display:flex;gap:4px;margin-right:8px;border-right:1px solid var(--gray3);padding-right:10px"> 1587 <button id="pending-geo-all" class="btn-sm" style="background:var(--accent);color:#000" onclick="_setPendingGeo('all',this)">×××</button> 1588 <button id="pending-geo-il" class="btn-sm btn-ghost" onclick="_setPendingGeo('il',this)">ð®ð± ×שר××</button> 1589 <button id="pending-geo-int" class="btn-sm btn-ghost" onclick="_setPendingGeo('int',this)">ð ××× ×××××</button> 1590 </span> 1591 </div> 1592 <div class="admin-title">â³ ×§×פ×× ×× ×××××§×</div> 1593 </div> 1594 <div class="table-wrap"> 1595 <div class="table-header"> 1596 <div class="table-title" id="pending-table-title">×××¢×...</div> 1597 </div> 1598 <table><thead><tr> 1599 <th style="width:36px;color:var(--gray5)">#</th> 1600 <th>×××××× ××§×ר</th> 1601 <th style="text-align:center">ס×× ××§×ר</th> 1602 <th style="text-align:center">×ספר ×§×פ×× ××</th> 1603 <th>×× ×××ת ××××××</th> 1604 <th>××××××ת ×§××××</th> 1605 <th>פע×××ת</th> 1606 </tr></thead> 1607 <tbody id="pending-tbody"> 1608 <tr><td colspan="7" style="color:var(--gray5);text-align:center;padding:24px">×××¢×...</td></tr> 1609 </tbody></table> 1610 </div> 1611 </div> 1612 1613 <!-- ââ Users view âââââââââââââââââââââââââââââââââââââââ -->
1614 <div id="view-users" class="admin-view" style="display:none"> 1615 <div class="admin-header"> 1616 <div class="admin-actions"> 1617 <button class="btn-sm btn-primary" onclick="_openAddUserModal()">+ ××סף ×שת×ש</button> 1618 </div> 1619 <div class="admin-title">×שת×ש××</div> 1620 </div> 1621 <div style="background:var(--gray2);border:1px solid var(--gray3);border-radius:12px;overflow:hidden;margin-bottom:24px"> 1622 <table style="width:100%;border-collapse:collapse"> 1623 <thead><tr style="border-bottom:1px solid var(--gray3)"> 1624 <th style="padding:12px 16px;text-align:right;font-size:12px;color:var(--gray5);font-weight:500">#</th> 1625 <th style="padding:12px 16px;text-align:right;font-size:12px;color:var(--gray5);font-weight:500">×©× ×שת×ש</th> 1626 <th style="padding:12px 16px;text-align:right;font-size:12px;color:var(--gray5);font-weight:500">תפק××</th> 1627 <th style="padding:12px 16px;text-align:right;font-size:12px;color:var(--gray5);font-weight:500">× ×צר</th> 1628 <th style="padding:12px 16px;text-align:right;font-size:12px;color:var(--gray5);font-weight:500">×× ××¡× ××ר×× ×</th> 1629 <th style="padding:12px 16px;text-align:right;font-size:12px;color:var(--gray5);font-weight:500">IP</th> 1630 <th style="padding:12px 16px;text-align:right;font-size:12px;color:var(--gray5);font-weight:500">פע×××ת</th> 1631 </tr></thead> 1632 <tbody id="users-tbody"></tbody> 1633 </table> 1634 </div> 1635 </div> 1636 1637 <div id="view-settings" style="display:none"> 1638 <div class="admin-header"> 1639 <div class="admin-title">×××ר×ת</div> 1640 </div> 1641 <div class="table-wrap" style="padding:24px 20px"> 1642 <p style="color:var(--gray5);font-size:14px">×××ר×ת ××ער×ת ×××× ×××× ×ת ×קר××.</p> 1643 </div> 1644 </div> 1645 1646 <div id="view-logs" style="display:none"> 1647 <div class="admin-header"> 1648 <div class="admin-actions"> 1649 <button class="btn-sm btn-ghost" onclick="loadLogs()" title="×¨×¢× ×">âº ×¨×¢× ×</button> 1650 </div> 1651 <div class="admin-title">××××× â ×× ××שת×ש××</div> 1652 </div> 1653 <div style="padding:0 20px 24px"> 1654 <!-- Device filter bar --> 1655 <div id="log-device-filter" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px"></div> 1656 <!-- Summary chips --> 1657 <div id="log-summary" style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px"></div> 1658 <!-- Feed --> 1659 <div id="log-lines" style="overflow-x:auto;border:1px solid var(--gray3);border-radius:12px">×××¢× ×××××...</div> 1660 <!-- Pagination --> 1661 <div id="log-pagination" style="display:flex;align-items:center;justify-content:center;gap:6px;margin-top:16px;flex-wrap:wrap"></div> 1662 </div> 1663 </div> 1664 1665 <!-- ââ HIDE MECHANISM VIEW âââââââââââââââââââââââââââââââ --> 1666 <div id="view-hide-mechanism" style="display:none;padding:28px 24px"> 1667 <div class="admin-header" style="margin-bottom:24px"> 1668 <div class="admin-title">ð¨ ×× ×× ×× ×סתרת ×§×× ×§×פ××</div> 1669 </div> 1670 1671 <!-- Code reveal options --> 1672 <div style="margin-bottom:16px"> 1673 <div class="section-label" style="padding:0;margin-bottom:6px">ð ××ר ס×× ××: ××× ×סת×ר×× ×ת ××§××?</div> 1674 <div style="font-size:13px;color:var(--gray5);margin-bottom:24px">×××¥ ×¢× ×× ×§×× ××× ××ש××£ â ××§×× ××עתק ×××××××ת</div> 1675 </div> 1676 1677 <style> 1678 .hm-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:20px; } 1679 .hm-col { display:flex; flex-direction:column; gap:10px; } 1680 .hm-label { text-align:center; font-size:11px; font-weight:700; color:var(--accent); letter-spacing:.5px; text-transform:uppercase; padding-bottom:4px; border-bottom:1px solid var(--gray3); } 1681 .hm-name { text-align:center; font-size:14px; color:#fff; font-weight:600; } 1682 .hm-desc { text-align:center; font-size:11px; color:var(--gray5); line-height:1.5; min-height:36px; } 1683 .hm-card { background:var(--gray1); border-radius:16px; border:1px solid var(--gray3); padding:20px 16px 16px; display:flex; flex-direction:column; gap:10px; position:relative; } 1684 .hm-store-row { display:flex; align-items:center; gap:8px; justify-content:flex-end; } 1685 .hm-logo { width:28px; height:28px; border-radius:6px; background:var(--gray3); display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; color:#fff; flex-shrink:0; } 1686 .hm-storename { font-size:13px; color:var(--gray5); } 1687 .hm-badge { position:absolute; top:12px; left:12px; background:var(--gray3); border-radius:6px; padding:2px 7px; font-size:10px; color:var(--gray5); } 1688 .hm-discount { font-size:26px; font-weight:800; color:#fff; text-align:center; line-height:1.1; } 1689 .hm-discount small { font-size:14px; font-weight:600; color:var(--gray5); } 1690 .hm-votes { display:flex; gap:8px; } 1691 .hm-vote { display:flex; align-items:center; gap:4px; background:var(--gray2); border:1px solid var(--gray3); border-radius:8px; padding:4px 10px; font-size:12px; color:var(--gray5); cursor:pointer; } 1692 .hm-vote.up { border-color:rgba(0,232,122,.3); color:var(--accent); } 1693 .hm-vote.dn { border-color:rgba(255,77,77,.3); color:#ff4d4d; } 1694 .hm-rec { background:rgba(0,232,122,.12); border:1px solid rgba(0,232,122,.3); color:var(--accent); font-size:10px; font-weight:700; padding:2px 8px; border-radius:20px; text-align:center; } 1695 /* Shared code box */ 1696 .hm-code-box { background:var(--gray2); border:1px solid var(--gray3); border-radius:10px; padding:10px 12px; font-family:monospace; font-size:15px;
1696 color:var(--accent); letter-spacing:2px; text-align:center; font-weight:700; display:flex; align-items:center; gap:8px; cursor:pointer; transition:border-color .2s; position:relative; overflow:hidden; } 1697 .hm-code-box:hover { border-color:rgba(0,232,122,.4); } 1698 /* 1 â Slide Banner */ 1699 @keyframes hm-shimmer2 { 0%{background-position:-200% 0} 100%{background-position:200% 0} } 1700 .hm-strip-wrap { position:relative; border-radius:10px; overflow:hidden; cursor:pointer; } 1701 .hm-strip { position:absolute; inset:0; background:linear-gradient(135deg,#182818,#1e381e); border:1px solid rgba(0,232,122,.4); border-radius:10px; display:flex; align-items:center; justify-content:center; gap:8px; font-size:12px; font-weight:700; color:var(--accent); overflow:hidden; transition:transform .38s cubic-bezier(.4,0,.2,1), opacity .28s; } 1702 .hm-strip::before { content:''; position:absolute; inset:0; background:linear-gradient(90deg,transparent,rgba(0,232,122,.07),transparent); background-size:200% 100%; animation:hm-shimmer2 2.2s infinite; } 1703 .hm-strip-arr { font-size:14px; opacity:.55; } 1704 /* 2 â Blur Focus */ 1705 .hm-blur-wrap2 { cursor:zoom-in !important; } 1706 .hm-blur-wrap2 .hm-blur-code { filter:blur(8px); transition:filter .4s ease; user-select:none; pointer-events:none; } 1707 .hm-blur-wrap2:hover .hm-blur-code { filter:blur(4px); } 1708 /* 3 â Split Doors */ 1709 .hm-doors-wrap { position:relative; border-radius:10px; overflow:hidden; cursor:pointer; } 1710 .hm-door { position:absolute; top:0; bottom:0; background:var(--gray3); border:1px solid var(--gray4); display:flex; align-items:center; transition:transform .4s cubic-bezier(.4,0,.2,1); } 1711 .hm-door-l { left:0; right:50%; justify-content:center; border-radius:10px 0 0 10px; border-right:none; } 1712 .hm-door-r { left:50%; right:0; justify-content:center; border-radius:0 10px 10px 0; border-left:none; } 1713 /* 4 â Night Vision */ 1714 .hm-scan-wrap { position:relative; border-radius:10px; overflow:hidden; cursor:pointer; } 1715 .hm-scan-overlay { position:absolute; inset:0; background:repeating-linear-gradient(0deg,rgba(0,0,0,.52) 0px,rgba(0,0,0,.52) 1px,transparent 1px,transparent 3px),rgba(0,14,4,.8); border-radius:10px; display:flex; align-items:center; justify-content:center; font-size:10px; color:rgba(0,232,122,.5); letter-spacing:2.5px; font-family:monospace; border:1px solid rgba(0,232,122,.18); transition:opacity .35s; pointer-events:none; } 1716 /* 5 â Stamp Lift */ 1717 @keyframes hm-stamp-out { 0%{opacity:1;transform:rotate(-10deg) scale(1) translateY(0)} 100%{opacity:0;transform:rotate(-28deg) scale(1.5) translateY(-60px)} } 1718 .hm-stamp-wrap { position:relative; border-radius:10px; cursor:pointer; } 1719 .hm-stamp { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; z-index:2; } 1720 .hm-stamp-inner { border:2.5px solid rgba(200,40,40,.75); border-radius:5px; padding:4px 12px; transform:rotate(-10deg); color:rgba(210,50,50,.85); font-size:11px; font-weight:800; letter-spacing:3px; font-family:monospace; text-transform:uppercase; background:rgba(0,0,0,.7); white-space:nowrap; } 1721 /* 6 â Eye Toggle (password-style) */ 1722 .hm-eye-btn { color:var(--gray5); cursor:pointer; flex-shrink:0; display:flex; align-items:center; transition:color .2s; opacity:.7; } 1723 .hm-eye-btn:hover, .hm-code-box:hover .hm-eye-btn { color:var(--accent); opacity:1; } 1724 .hm-eye-hidden { flex:1; display:flex; align-items:center; font-size:14px; transition:opacity .2s; } 1725 .hm-eye-dots { color:var(--gray5); letter-spacing:4px; } 1726 .hm-eye-peek { color:var(--accent); font-weight:700; letter-spacing:2px; } 1727 .hm-eye-revealed { display:none; color:var(--accent); font-weight:700; letter-spacing:2px; flex:1; text-align:center; } 1728 </style> 1729 1730 <div class="hm-grid"> 1731 1732 <!-- 1 Slide Banner --> 1733 <div class="hm-col"> 1734 <div class="hm-label">×פשר×ת 1</div> 1735 <div class="hm-name">Slide Banner</div> 1736 <div class="hm-desc">רצ××¢× ×ר××§× ×¢× ×©××ר ×××¡× ×ת ××§×× â ×××צ×× ×××× × ××קת ×××¢××</div> 1737 <div class="hm-card">
1738 <span class="hm-badge">××××</span> 1739 <div class="hm-store-row"><span class="hm-storename">adika</span><div class="hm-logo" style="background:#b06020">A</div></div> 1740 <div class="hm-discount">10% <small>×× ××</small></div> 1741 <div class="hm-strip-wrap" onclick="hmRevealStrip(this,'ADIKA10')"> 1742 <div class="hm-code-box" style="direction:ltr"> 1743 <span style="color:var(--accent)">â</span> 1744 <span style="color:var(--accent);font-weight:700;letter-spacing:2px;flex:1;text-align:center">ADIKA10</span> 1745 </div> 1746 <div class="hm-strip"> 1747 <span class="hm-strip-arr">â</span> 1748 <span>×קש ××ש×פת ××§××</span> 1749 <span class="hm-strip-arr">â¶</span> 1750 </div> 1751 </div> 1752 <div class="hm-votes"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">0 ð</div></div> 1753 </div> 1754 </div> 1755 1756 <!-- 2 Blur Focus --> 1757 <div class="hm-col"> 1758 <div class="hm-label">×פשר×ת 2</div> 1759 <div class="hm-name">Blur Focus</div> 1760 <div class="hm-desc">××§×× ×××ש×ש ××××××× â hover ×××× ××¢×, ××××¦× ××שפת</div> 1761 <div class="hm-card"> 1762 <span class="hm-badge">××¤× × 3 ××××</span> 1763 <div class="hm-store-row"><span class="hm-storename">bug</span><div class="hm-logo" style="background:#0055cc">B</div></div> 1764 <div class="hm-discount">15% <small>×× ××</small></div> 1765 <div class="hm-code-box hm-blur-wrap2" style="direction:ltr" onclick="hmRevealBlurFocus(this,'BUG15')"> 1766 <span style="color:var(--accent);flex-shrink:0">â</span> 1767 <span class="hm-blur-code" style="color:var(--accent);font-weight:700;letter-spacing:2px;flex:1;text-align:center">BUG15</span> 1768 </div> 1769 <div class="hm-votes"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">0 ð</div></div> 1770 </div> 1771 </div> 1772 1773 <!-- 3 Split Doors --> 1774 <div class="hm-col"> 1775 <div class="hm-label">×פשר×ת 3</div> 1776 <div class="hm-name">Split Doors</div> 1777 <div class="hm-desc">×©× × ×ª×¨×ס×× × ×¤×ª××× ×צ×××× ××× ××ת×ת ××¢××ת</div> 1778 <div class="hm-card"> 1779 <span class="hm-badge">××¤× × ××××××</span> 1780 <div class="hm-store-row"><span class="hm-storename">fox</span><div class="hm-logo" style="background:#222">F</div></div> 1781 <div class="hm-discount">20% <small>×× ××</small></div> 1782 <div class="hm-doors-wrap" onclick="hmRevealDoors(this,'FOX20')"> 1783 <div class="hm-code-box" style="direction:ltr"> 1784 <span style="color:var(--accent)">â</span> 1785 <span style="color:var(--accent);font-weight:700;letter-spacing:2px;flex:1;text-align:center">FOX20</span> 1786 </div> 1787 <div class="hm-door hm-door-l"> 1788 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="15 18 9 12 15 6"/></svg> 1789 </div> 1790 <div class="hm-door hm-door-r"> 1791 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg> 1792 </div> 1793 </div> 1794 <div class="hm-votes"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">0 ð</div></div> 1795 </div> 1796 </div> 1797 1798 <!-- 4 Night Vision --> 1799 <div class="hm-col"> 1800 <div class="hm-label">×פשר×ת 4</div> 1801 <div class="hm-name">Night Vision</div> 1802 <div class="hm-desc">ש××ת סר××§× ×¦×××ת ××¢× ××§×× â ××××¦× ×ס××¨× ×ת ×פ×××ר</div> 1803 <div class="hm-card">
1804 <span class="hm-badge">××¤× × 5 ××××</span> 1805 <div class="hm-store-row"><span class="hm-storename">ivory</span><div class="hm-logo" style="background:#3a3a3a">I</div></div> 1806 <div class="hm-discount">15% <small>×× ××</small></div> 1807 <div class="hm-scan-wrap" onclick="hmRevealScan(this,'IVORY15')"> 1808 <div class="hm-code-box" style="direction:ltr"> 1809 <span style="color:var(--accent)">â</span> 1810 <span style="color:var(--accent);font-weight:700;letter-spacing:2px;flex:1;text-align:center">IVORY15</span> 1811 </div> 1812 <div class="hm-scan-overlay">[ ENCRYPTED ]</div> 1813 </div> 1814 <div class="hm-votes"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">0 ð</div></div> 1815 </div> 1816 </div> 1817 1818 <!-- 5 Stamp Lift --> 1819 <div class="hm-col"> 1820 <div class="hm-label">×פשר×ת 5</div> 1821 <div class="hm-name">Stamp Lift</div> 1822 <div class="hm-desc">××ת×ת "ס×××" ××××× ××××ס×× â ×××צ×× ×××× ×¢×¤× ×××§××£</div> 1823 <div class="hm-card"> 1824 <span class="hm-badge">××××</span> 1825 <div class="hm-store-row"><span class="hm-storename">factory54</span><div class="hm-logo" style="background:#1a1a2e">54</div></div> 1826 <div class="hm-discount">50⪠<small>×× ××</small></div> 1827 <div class="hm-stamp-wrap" onclick="hmRevealStamp(this,'FAC50')"> 1828 <div class="hm-code-box" style="direction:ltr"> 1829 <span style="color:var(--accent)">â</span> 1830 <span style="color:var(--accent);font-weight:700;letter-spacing:2px;flex:1;text-align:center">FAC50</span> 1831 </div> 1832 <div class="hm-stamp"> 1833 <div class="hm-stamp-inner">ס×××</div> 1834 </div> 1835 </div> 1836 <div class="hm-votes"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">0 ð</div></div> 1837 </div> 1838 </div> 1839 1840 <!-- 6 Eye Toggle --> 1841 <div class="hm-col"> 1842 <div class="hm-label">×פשר×ת 6</div> 1843 <div class="hm-name">Eye Toggle</div> 1844 <div class="hm-desc">××× ×©×× ×¡×ס×× â ×××צ×× ×¢× ×¢×× ×ר×××× ×ת ××§××</div> 1845 <div class="hm-rec">⦠××× ××× ××××××××</div> 1846 <div class="hm-card"> 1847 <span class="hm-badge">××××</span> 1848 <div class="hm-store-row"><span class="hm-storename">terminalx</span><div class="hm-logo" style="background:#111">TX</div></div> 1849 <div class="hm-discount">5% <small>×× ××</small></div> 1850 <div class="hm-code-box" style="direction:ltr;cursor:pointer" onclick="hmRevealEye(this,'TERMX5')"> 1851 <span style="color:var(--accent);flex-shrink:0">â</span> 1852 <span class="hm-eye-hidden"><span class="hm-eye-dots">â¢â¢â¢â¢</span><span class="hm-eye-peek">X5</span></span> 1853 <span class="hm-eye-revealed">TERMX5</span> 1854 <span class="hm-eye-btn" title="××¦× ×§××"> 1855 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> 1856 </span> 1857 </div> 1858 <div class="hm-votes"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">2 ð</div></div> 1859 </div> 1860 </div> 1861 1862 </div> 1863 1864 <!-- ââ Button styles ââ --> 1865 <div style="margin-top:40px"> 1866 <div class="section-label" style="padding:0;margin-bottom:6px">ð±ï¸ ס×× ×× ×פת×ר â 6 ×פשר×××ת</div> 1867 <div style="font-size:13px;color:var(--gray5);margin-bottom:20px">×××× ×¡×× ×× ×××¨× ×× ×××××ת ××××× ×©×× ×פת×ר ×××××¦× ×ª×ש××£ ×ת ××§××?</div> 1868 <style> 1869 /* shared btn-demo internals */ 1870 .hm-btn-box { font-family:monospace; font-size:15px; padding:11px 14px; border-radius:8px; display:flex; align-items:center; gap:8px; cursor:pointer; direction:ltr; transition:filter .15s, transform .1s; user-select:none; position:relative; overflow:hidden; } 1871 .hm-btn-box:active { transform:scale(0.97); } 1872 .hm-btn-masked { display:flex; align-items:center; flex:1; gap:0; } 1873 .hm-btn-dots { color:rgba(255,255,255,0.5); font-size:16px; font-weight:700; letter-spacing:3px; } 1874 .hm-btn-end { color:#00e87a; font-weight:800; font-size:16px; letter-spacing:3px; } 1875 .hm-btn-eye { color:#888; display:flex; align-items:center; flex-shrink:0; transition:color .2s; }
1876 .hm-btn-box:hover .hm-btn-eye { color:#00e87a; } 1877 1878 /* 1 â Solid Fill */ 1879 .hm-btn-solid { background:#00e87a; border:1px solid #00e87a; } 1880 .hm-btn-solid .hm-btn-dots { color:rgba(0,0,0,0.45); } 1881 .hm-btn-solid .hm-btn-end { color:#003d1f; } 1882 .hm-btn-solid .hm-btn-eye { color:rgba(0,0,0,0.5); } 1883 .hm-btn-solid:hover { filter:brightness(1.08); } 1884 1885 /* 2 â Pill CTA */ 1886 .hm-btn-pill { background:rgba(0,232,122,0.1); border:2px solid rgba(0,232,122,0.75); border-radius:50px; padding:10px 18px; } 1887 .hm-btn-pill:hover { background:rgba(0,232,122,0.18); border-color:#00e87a; } 1888 1889 /* 3 â Scan Line (animated sweep) */ 1890 @keyframes hm-scan { 0%{left:-40%} 100%{left:130%} } 1891 .hm-btn-scan { background:rgba(0,232,122,0.07); border:1px solid rgba(0,232,122,0.35); } 1892 .hm-btn-scan::before { content:''; position:absolute; top:0; bottom:0; width:35%; background:linear-gradient(90deg,transparent,rgba(0,232,122,0.25),transparent); animation:hm-scan 1.6s ease-in-out infinite; pointer-events:none; } 1893 .hm-btn-scan:hover { background:rgba(0,232,122,0.13); } 1894 1895 /* 4 â Raised / 3-D */ 1896 .hm-btn-raised { background:rgba(0,232,122,0.1); border:1px solid rgba(0,232,122,0.5); box-shadow:0 4px 0 rgba(0,80,40,0.7), 0 6px 14px rgba(0,0,0,0.35); transform:translateY(-2px); } 1897 .hm-btn-raised:hover { background:rgba(0,232,122,0.16); box-shadow:0 5px 0 rgba(0,80,40,0.7), 0 8px 18px rgba(0,0,0,0.35); } 1898 .hm-btn-raised:active { transform:translateY(2px); box-shadow:0 1px 0 rgba(0,80,40,0.7); } 1899 1900 /* 5 â Frosted Glass */ 1901 .hm-btn-frost { background:rgba(0,232,122,0.06); border:1px solid rgba(0,232,122,0.22); border-radius:12px; box-shadow:inset 0 1px 0 rgba(255,255,255,0.07), 0 0 24px rgba(0,232,122,0.1); backdrop-filter:blur(6px); } 1902 .hm-btn-frost:hover { background:rgba(0,232,122,0.11); box-shadow:inset 0 1px 0 rgba(255,255,255,0.1), 0 0 32px rgba(0,232,122,0.18); border-color:rgba(0,232,122,0.4); } 1903 1904 /* 6 â Pulse Ring */ 1905 @keyframes hm-ring-pulse { 0%{box-shadow:0 0 0 0 rgba(0,232,122,0.55),0 0 0 0 rgba(0,232,122,0.25)} 70%{box-shadow:0 0 0 7px rgba(0,232,122,0),0 0 0 14px rgba(0,232,122,0)} 100%{box-shadow:0 0 0 0 rgba(0,232,122,0)} } 1906 .hm-btn-pulse { background:rgba(0,232,122,0.07); border:1.5px solid rgba(0,232,122,0.5); animation:hm-ring-pulse 1.9s ease-out infinite; } 1907 .hm-btn-pulse:hover { background:rgba(0,232,122,0.14); border-color:#00e87a; } 1908 </style> 1909 1910 <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px"> 1911 1912 <!-- btn 1 â Solid --> 1913 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 1914 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">1 â Solid Fill</div> 1915 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">רקע ×ר××§ ××× â ××× ×ר×ר ש×× ×פת×ר</div> 1916 <div class="hm-btn-box hm-btn-solid" onclick="hmRevealBtn(this,'CODE15')"> 1917 <span class="hm-btn-masked"><span class="hm-btn-dots">â¢â¢â¢â¢</span><span class="hm-btn-end">15</span></span> 1918 <span class="hm-btn-eye"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span> 1919 </div> 1920 <div class="hm-votes" style="margin-top:8px"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">0 ð</div></div> 1921 </div> 1922 1923 <!-- btn 2 â Pill --> 1924 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 1925 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">2 â Pill CTA</div> 1926 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">פ×× ××¢××× ×¢× ×ס×רת ×ר××§× ×¢××</div> 1927 <div class="hm-btn-box hm-btn-pill" onclick="hmRevealBtn(this,'SUMMER20')">
1928 <span class="hm-btn-masked"><span class="hm-btn-dots">â¢â¢â¢â¢â¢â¢</span><span class="hm-btn-end">20</span></span> 1929 <span class="hm-btn-eye"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span> 1930 </div> 1931 <div class="hm-votes" style="margin-top:8px"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">0 ð</div></div> 1932 </div> 1933 1934 <!-- btn 3 â Scan --> 1935 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 1936 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">3 â Scan Line</div> 1937 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">×§× ××ר ×¢××ר â ×ר××ש ××× ×¡×¨××§×</div> 1938 <div class="hm-btn-box hm-btn-scan" onclick="hmRevealBtn(this,'ZARA10')"> 1939 <span class="hm-btn-masked"><span class="hm-btn-dots">â¢â¢â¢â¢</span><span class="hm-btn-end">10</span></span> 1940 <span class="hm-btn-eye"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span> 1941 </div> 1942 <div class="hm-votes" style="margin-top:8px"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">0 ð</div></div> 1943 </div> 1944 1945 <!-- btn 4 â Raised --> 1946 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 1947 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">4 â Raised / 3D</div> 1948 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">×¦× ×ª×ת×× â ×פת×ר פ××× ×¢× ×¢×××§</div> 1949 <div class="hm-btn-box hm-btn-raised" onclick="hmRevealBtn(this,'BUG15')"> 1950 <span class="hm-btn-masked"><span class="hm-btn-dots">â¢â¢â¢â¢</span><span class="hm-btn-end">15</span></span> 1951 <span class="hm-btn-eye"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span> 1952 </div> 1953 <div class="hm-votes" style="margin-top:8px"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">0 ð</div></div> 1954 </div> 1955 1956 <!-- btn 5 â Frosted --> 1957 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 1958 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">5 â Frosted Glass</div> 1959 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">×××××ת ×××ש×שת ×¢× ×××ר ×ר××§</div> 1960 <div class="hm-btn-box hm-btn-frost" onclick="hmRevealBtn(this,'FOX5')"> 1961 <span class="hm-btn-masked"><span class="hm-btn-dots">â¢â¢â¢â¢</span><span class="hm-btn-end">X5</span></span> 1962 <span class="hm-btn-eye"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span> 1963 </div> 1964 <div class="hm-votes" style="margin-top:8px"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">0 ð</div></div> 1965 </div> 1966 1967 <!-- btn 6 â Pulse Ring --> 1968 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 1969 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">6 â Pulse Ring</div> 1970 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">××עת ×ר××§× ×¤××¢×ת â ××ש×ת ×ת ××¢××</div> 1971 <div class="hm-btn-box hm-btn-pulse" onclick="hmRevealBtn(this,'KSP30')">
1972 <span class="hm-btn-masked"><span class="hm-btn-dots">â¢â¢â¢â¢</span><span class="hm-btn-end">30</span></span> 1973 <span class="hm-btn-eye"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span> 1974 </div> 1975 <div class="hm-votes" style="margin-top:8px"><div class="hm-vote dn">0 ð</div><div class="hm-vote up">0 ð</div></div> 1976 </div> 1977 1978 </div> 1979 </div> 1980 1981 <!-- ââ Shine effects ââ --> 1982 <div style="margin-top:36px"> 1983 <div class="section-label" style="padding:0;margin-bottom:6px">⨠××¤×§× ×××ר ×××ר ×ש××¤× â 12 ×פשר×××ת</div> 1984 <div style="font-size:13px;color:var(--gray5);margin-bottom:20px">×××¥ ×¢× ×× ×§×× ××× ×ר××ת ×ת ×××¤×§× ×××ש</div> 1985 <style> 1986 /* 1 Silver sweep */ 1987 @keyframes hm-shine-silver { 0%{background-position:-200% center} 100%{background-position:200% center} } 1988 .hm-shine-1 { background:linear-gradient(90deg,var(--accent) 0%,#fff 40%,#aef7d3 60%,var(--accent) 100%); background-size:200% auto; -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; animation:hm-shine-silver .7s ease forwards; } 1989 /* 2 Glow pulse */ 1990 @keyframes hm-shine-glow { 0%,100%{text-shadow:0 0 0 transparent} 40%{text-shadow:0 0 14px rgba(0,232,122,.9),0 0 30px rgba(0,232,122,.5)} } 1991 .hm-shine-2 { animation:hm-shine-glow .8s ease forwards; color:var(--accent)!important; } 1992 /* 3 Flash background */ 1993 @keyframes hm-shine-flash { 0%{background:rgba(0,232,122,.0)} 20%{background:rgba(0,232,122,.28)} 100%{background:rgba(0,232,122,.0)} } 1994 .hm-shine-3-wrap { animation:hm-shine-flash .6s ease forwards; } 1995 /* 4 Gold sweep */ 1996 @keyframes hm-shine-gold { 0%{background-position:-200% center} 100%{background-position:200% center} } 1997 .hm-shine-4 { background:linear-gradient(90deg,#f59e0b 0%,#fff8c5 35%,#fbbf24 60%,#f59e0b 100%); background-size:200% auto; -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; animation:hm-shine-gold .7s ease forwards; } 1998 /* 5 Neon flicker */ 1999 @keyframes hm-shine-neon { 0%{opacity:1} 10%{opacity:.4} 20%{opacity:1} 30%{opacity:.6} 45%{opacity:1} 60%{opacity:.3} 75%{opacity:1} 100%{opacity:1} } 2000 .hm-shine-5 { animation:hm-shine-neon .7s ease forwards; color:#00e87a!important; text-shadow:0 0 10px rgba(0,232,122,.8),0 0 24px rgba(0,232,122,.4); } 2001 /* 6 Scale bounce */ 2002 @keyframes hm-shine-scale { 0%{transform:scale(1)} 30%{transform:scale(1.12)} 60%{transform:scale(.97)} 80%{transform:scale(1.04)} 100%{transform:scale(1)} } 2003 .hm-shine-6 { animation:hm-shine-scale .55s cubic-bezier(.4,0,.2,1) forwards; display:inline-block; } 2004 /* 7 Warp In â text stretches from center outward */ 2005 @keyframes hm-shine-warp { 0%{transform:scaleX(0.1);opacity:0} 65%{transform:scaleX(1.07)} 82%{transform:scaleX(0.97)} 100%{transform:scaleX(1);opacity:1} } 2006 .hm-shine-7 { animation:hm-shine-warp .45s cubic-bezier(.4,0,.2,1) forwards; display:inline-block; color:var(--accent); } 2007 /* 8 Rainbow Cycle â hue spins through spectrum */ 2008 @keyframes hm-shine-rainbow { 0%{filter:hue-rotate(0deg) brightness(1.6)} 40%{filter:hue-rotate(140deg) brightness(2)} 80%{filter:hue-rotate(280deg) brightness(1.4)} 100%{filter:hue-rotate(360deg) brightness(1)} } 2009 .hm-shine-8 { animation:hm-shine-rainbow .75s ease forwards; color:var(--accent); } 2010 /* 9 Ink Reveal â blurry â sharp, letters compress */ 2011 @keyframes hm-shine-ink { 0%{filter:blur(9px);opacity:0;letter-spacing:8px} 55%{letter-spacing:2.5px} 100%{filter:blur(0);opacity:1;letter-spacing:1.5px} } 2012 .hm-shine-9 { animation:hm-shine-ink .5s ease-out forwards; display:inline-block; color:var(--accent); } 2013 /* 10 Type Highlight â green strip sweeps behind text */ 2014 @keyframes hm-shine-hl { 0%{background-size:0% 100%} 100%{background-size:100% 100%} } 2015 .hm-shine-10-wrap { background:linear-gradient(90deg,rgba(0,232,122,.22),rgba(0,232,122,.1)); background-repeat:no-repeat; background-size:0% 100%; animation:hm-shine-hl .45s ease forwards; border-radius:4px; } 2016 /* 11 Stamp Pop â scales in from tiny with slight rotation */ 2017 @keyframes hm-shine-stamp { 0%{transform:scale(0.35) rotate(-4deg);opacity:0}
2017 55%{transform:scale(1.1) rotate(1deg);opacity:1} 75%{transform:scale(0.96) rotate(-0.5deg)} 100%{transform:scale(1) rotate(0deg);opacity:1} } 2018 .hm-shine-11 { animation:hm-shine-stamp .5s cubic-bezier(.4,0,.2,1) forwards; display:inline-block; color:var(--accent); } 2019 /* 12 Slide Up â rises from below, white flash â green */ 2020 @keyframes hm-shine-slideup { 0%{transform:translateY(9px);opacity:0;color:#fff} 35%{color:#fff} 100%{transform:translateY(0);opacity:1;color:var(--accent)} } 2021 .hm-shine-12 { animation:hm-shine-slideup .42s ease-out forwards; display:inline-block; } 2022 </style> 2023 <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px"> 2024 <!-- shine 1 --> 2025 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2026 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">1 â Silver Sweep</div> 2027 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">×רק ××¡×¤× ×¢××ר ×¢× ××§××</div> 2028 <div class="cc-code" id="hs1" onclick="hmShinePlay(1)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs1-t">SUMMER15</span></div> 2029 </div> 2030 <!-- shine 2 --> 2031 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2032 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">2 â Glow Pulse</div> 2033 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">×××ר ×ר××§ ×ס××× ×××ת××ת</div> 2034 <div class="cc-code" id="hs2" onclick="hmShinePlay(2)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs2-t">SUMMER15</span></div> 2035 </div> 2036 <!-- shine 3 --> 2037 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2038 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">3 â Flash</div> 2039 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">×רקע ××××× ×ר××§</div> 2040 <div class="cc-code" id="hs3" onclick="hmShinePlay(3)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs3-t">SUMMER15</span></div> 2041 </div> 2042 <!-- shine 4 --> 2043 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2044 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">4 â Gold Sweep</div> 2045 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">×רק ×××× ×¢××ר ×¢× ××§××</div> 2046 <div class="cc-code" id="hs4" onclick="hmShinePlay(4)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs4-t">SUMMER15</span></div> 2047 </div> 2048 <!-- shine 5 --> 2049 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2050 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">5 â Neon Flicker</div> 2051 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">××§×× ××××× ××× × ××××</div> 2052 <div class="cc-code" id="hs5" onclick="hmShinePlay(5)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs5-t">SUMMER15</span></div> 2053 </div> 2054 <!-- shine 6 --> 2055 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2056 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">6 â Bounce</div> 2057 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">××§×× ×§×פץ ×××ª× ×× ×</div> 2058 <div class="cc-code" id="hs6" onclick="hmShinePlay(6)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs6-t">SUMMER15</span></div> 2059 </div> 2060 <!-- shine 7 --> 2061 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2062 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">7 â Warp In</div> 2063 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">××§×× ×תר×× ×××ר×× ×××צ×</div> 2064 <div class="cc-code" id="hs7" onclick="hmShinePlay(7)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs7-t">SUMMER15</span></div> 2065 </div> 2066 <!-- shine 8 --> 2067 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2068 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">8 â Rainbow Cycle</div> 2069 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">××§×× ×¢××ר ×ת ×ספק×ר××</div> 2070 <div class="cc-code" id="hs8" onclick="hmShinePlay(8)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs8-t">SUMMER15</span></div> 2071 </div> 2072 <!-- shine 9 --> 2073 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2074 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">9 â Ink Reveal</div> 2075 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">×××ש×ש â ×× â ××× ×ת××§××ת</div> 2076 <div class="cc-code" id="hs9" onclick="hmShinePlay(9)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs9-t">SUMMER15</span></div> 2077 </div> 2078 <!-- shine 10 --> 2079 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2080 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">10 â Type Highlight</div> 2081 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">רצ××¢× ×ר××§× × ×סעת ×××××¨× ××§××</div> 2082 <div class="cc-code" id="hs10" onclick="hmShinePlay(10)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs10-t">SUMMER15</span></div> 2083 </div> 2084 <!-- shine 11 --> 2085 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2086 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">11 â Stamp Pop</div> 2087 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">××§×× "×××ת×" â ×§×× ×¢× ×××× ×¢× ×¡××××</div> 2088 <div class="cc-code" id="hs11" onclick="hmShinePlay(11)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs11-t">SUMMER15</span></div> 2089 </div> 2090 <!-- shine 12 --> 2091 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px"> 2092 <div style="font-size:11px;font-weight:700;color:var(--accent);margin-bottom:4px">12 â Slide Up</div> 2093 <div style="font-size:11px;color:var(--gray5);margin-bottom:12px">××§×× ×¢××× ××××× ×¢× ××××§ ×××</div> 2094 <div class="cc-code" id="hs12" onclick="hmShinePlay(12)" style="cursor:pointer;direction:ltr;letter-spacing:1.5px"><span id="hs12-t">SUMMER15</span></div> 2095 </div> 2096 </div> 2097 </div> 2098 </div> 2099 2100 <!-- ââ SEARCH ANIMATION VIEW ââââââââââââââââââââââââââââ --> 2101 <div id="view-search-anim" style="display:none;padding:28px 24px"> 2102 <div class="admin-header" style="margin-bottom:24px"> 2103 <div class="admin-title">â³ ×× ××צ××ת ××פ×ש â ×פשר×××ת ×¢×צ××</div> 2104 </div> 2105 <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:20px;max-width:1100px"> 2106 2107 <!-- Option 1: Live Log --> 2108 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:16px;padding:20px"> 2109 <div style="font-size:13px;font-weight:700;color:var(--accent);margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid var(--gray3)">×פשר×ת 1 â ×××× ××פ×ש ×× ð¥ï¸</div> 2110 <div style="background:#0d0d0d;border-radius:10px;padding:14px 16px;font-family:monospace;font-size:12px;min-height:180px" id="mg-log1"> 2111 <div class="mg-log-line" style="display:flex;align-items:flex-start;gap:8px;margin-bottom:9px;opacity:0;animation:mg-fadeSlide .4s .1s forwards"><span style="flex-shrink:0;width:18px">ð</span><span style="color:#aaa">×ר××× ×©××××ª× ×¢× <strong style="color:#fff">Claude</strong>...</span></div> 2112 <div class="mg-log-line" style="display:flex;align-items:flex-start;gap:8px;margin-bottom:9px;opacity:0;animation:mg-fadeSlide .4s .9s forwards"><span style="flex-shrink:0;width:18px">ð</span><span style="color:#aaa">ש××× <strong style="color:#fff">12</strong> ש×××ת×ת ×-DuckDuckGo...</span></div> 2113 <div class="mg-log-line" style="display:flex;align-items:flex-start;gap:8px;margin-bottom:9px;opacity:0;animation:mg-fadeSlide .4s 1.8s forwards"><span style="flex-shrink:0;width:18px">ð</span><span style="color:#aaa">×× ×ª× <strong style="color:#fff">8 ×פ××</strong> ×××§×××...</span></div> 2114 <div class="mg-log-line" style="display:flex;align-items:flex-start;gap:8px;margin-bottom:9px;opacity:0;animation:mg-fadeSlide .4s 2.7s forwards"><span style="flex-shrink:0;width:18px">ð¤</span><span style="color:#aaa">××××¥ ×§×פ×× ×× ×¢× AI... <span style="color:var(--accent)">â 4</span> × ×צ××</span></div> 2115 <div class="mg-log-line" style="display:flex;align-items:flex-start;gap:8px;margin-bottom:9px;opacity:0;animation:mg-fadeSlide .4s 3.5s forwards"><span style="flex-shrink:0;width:18px">â </span><span style="color:#aaa"><span style="color:var(--accent)">× ×צ×× 9 ×§×פ×× ××</span> ת×× 14.2 ×©× ××ת</span></div> 2116 <div style="margin-top:12px;font-size:11px;color:#444;font-family:monospace"><span style="display:inline-block;width:7px;height:13px;background:var(--accent);margin-left:4px;animation:mg-blink 1s infinite;vertical-align:middle"></span> <span id="mg-log-timer">0.0s</span></div> 2117 </div> 2118 <button onclick="mgReplayLog()" style="display:block;margin:10px auto 0;font-size:11px;background:var(--gray2);border:1px solid var(--gray3);color:var(--gray5);border-radius:6px;padding:4px 12px;cursor:pointer">⺠××¤×¢× ×××ש</button> 2119 </div> 2120 2121 <!-- Option 2: Skeleton --> 2122 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:16px;padding:20px"> 2123 <div style="font-size:13px;font-weight:700;color:var(--accent);margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid var(--gray3)">×פשר×ת 2 â ×ר×××¡× ×©×× (Skeleton) â¨</div> 2124 <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px"> 2125 <div style="background:#1e1e1e;border-radius:12px;padding:14px"><div style="display:flex;gap:10px;align-items:center;margin-bottom:10px"><div style="width:36px;height:36px;border-radius:10px;flex-shrink:0" class="mg-shimmer"></div><div style="flex:1"><div class="mg-shimmer" style="height:10px;border-radius:5px;width:80%;margin-bottom:6px"></div><div class="mg-shimmer" style="height:10px;border-radius:5px;width:55%"></div></div></div><div class="mg-shimmer" style="height:22px;border-radius:8px;width:70%;margin-bottom:10px"></div><div class="mg-shimmer" style="height:36px;border-radius:8px;margin-bottom:10px"></div><div style="display:flex;gap:6px"><div class="mg-shimmer" style="height:8px;border-radius:5px;width:40%"></div><div class="mg-shimmer" style="height:8px;border-radius:5px;width:30%"></div></div></div> 2126 <div style="background:#1e1e1e;border-radius:12px;padding:14px"><div style="display:flex;gap:10px;align-items:center;margin-bottom:10px"><div style="width:36px;height:36px;border-radius:10px;flex-shrink:0" class="mg-shimmer"></div><div style="flex:1"><div class="mg-shimmer" style="height:10px;border-radius:5px;width:65%;margin-bottom:6px"></div><div class="mg-shimmer" style="height:10px;border-radius:5px;width:40%"></div></div></div><div class="mg-shimmer" style="height:22px;border-radius:8px;width:50%;margin-bottom:10px"></div><div class="mg-shimmer" style="height:36px;border-radius:8px;margin-bottom:10px"></div><div style="display:flex;gap:6px"><div class="mg-shimmer" style="height:8px;border-radius:5px;width:35%"></div><div class="mg-shimmer" style="height:8px;border-radius:5px;width:25%"></div></div></div> 2127 <div style="background:#1e1e1e;border-radius:12px;padding:14px"><div style="display:flex;gap:10px;align-items:center;margin-bottom:10px"><div style="width:36px;height:36px;border-radius:10px;flex-shrink:0" class="mg-shimmer"></div><div style="flex:1"><div class="mg-shimmer" style="height:10px;border-radius:5px;width:75%;margin-bottom:6px"></div><div class="mg-shimmer" style="height:10px;border-radius:5px;width:50%"></div></div></div><div class="mg-shimmer" style="height:22px;border-radius:8px;width:60%;margin-bottom:10px"></div><div class="mg-shimmer" style="height:36px;border-radius:8px;margin-bottom:10px"></div><div style="display:flex;gap:6px"><div class="mg-shimmer" style="height:8px;border-radius:5px;width:45%"></div><div class="mg-shimmer" style="height:8px;border-radius:5px;width:30%"></div></div></div> 2128 <div style="background:#1e1e1e;border-radius:12px;padding:14px"><div style="display:flex;gap:10px;align-items:center;margin-bottom:10px"><div style="width:36px;height:36px;border-radius:10px;flex-shrink:0" class="mg-shimmer"></div><div style="flex:1"><div class="mg-shimmer" style="height:10px;border-radius:5px;width:70%;margin-bottom:6px"></div><div class="mg-shimmer" style="height:10px;border-radius:5px;width:45%"></div></div></div><div class="mg-shimmer" style="height:22px;border-radius:8px;width:55%;margin-bottom:10px"></div><div class="mg-shimmer" style="height:36px;border-radius:8px;margin-bottom:10px"></div><div style="display:flex;gap:6px"><div class="mg-shimmer" style="height:8px;border-radius:5px;width:38%"></div><div class="mg-shimmer" style="height:8px;border-radius:5px;width:28%"></div></div></div> 2129 </div> 2130 <div style="text-align:center;margin-top:14px;font-size:12px;color:#555;display:flex;align-items:center;justify-content:center;gap:8px"> 2131 <div><span class="mg-dot-bounce" style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin:0 2px;animation:mg-dot 1.2s infinite"></span><span class="mg-dot-bounce" style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin:0 2px;animation:mg-dot 1.2s .2s infinite"></span><span class="mg-dot-bounce" style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin:0 2px;animation:mg-dot 1.2s .4s infinite"></span></div> 2132 ××פש ×§×פ×× ××... 2133 </div> 2134 </div> 2135 2136 <!-- Option 3: Tips --> 2137 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:16px;padding:20px"> 2138 <div style="font-size:13px;font-weight:700;color:var(--accent);margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid var(--gray3)">×פשר×ת 3 â ××פ×× ×××ס××× ð¡</div> 2139 <div style="position:relative;overflow:hidden;min-height:220px;display:flex;flex-direction:column;align-items:center;justify-content:center"> 2140 <div style="text-align:center;padding:10px"> 2141 <div style="font-size:40px;margin-bottom:14px" id="mg-tip-icon">ð³</div> 2142 <div style="font-size:14px;color:var(--gray6);line-height:1.7;max-width:240px;margin:0 auto" id="mg-tip-text">×ר×××¡× ×שר×× ×ס××××× × ××ª× ×× <strong style="color:#fff">×§×ש××§ × ×סף</strong> ×¢× ×§× ××× ×¢
2142× ×§×× ×§×פ×× â ת××× ×©××× ×××××§!</div> 2143 </div> 2144 <div style="display:flex;gap:6px;justify-content:center;margin-top:18px" id="mg-tip-dots"> 2145 <div style="width:7px;height:7px;border-radius:50%;background:var(--accent)" id="mg-td0"></div> 2146 <div style="width:7px;height:7px;border-radius:50%;background:var(--gray3)" id="mg-td1"></div> 2147 <div style="width:7px;height:7px;border-radius:50%;background:var(--gray3)" id="mg-td2"></div> 2148 <div style="width:7px;height:7px;border-radius:50%;background:var(--gray3)" id="mg-td3"></div> 2149 <div style="width:7px;height:7px;border-radius:50%;background:var(--gray3)" id="mg-td4"></div> 2150 </div> 2151 <div style="width:100%;height:2px;background:var(--gray3);border-radius:2px;margin-top:16px;overflow:hidden"><div style="height:2px;background:var(--accent);border-radius:2px;animation:mg-progress 4s linear infinite" id="mg-tip-bar"></div></div> 2152 <div style="font-size:11px;color:var(--gray5);margin-top:10px;display:flex;align-items:center;gap:6px;justify-content:center"><span class="mg-dot-bounce" style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin:0 2px;animation:mg-dot 1.2s infinite"></span><span class="mg-dot-bounce" style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin:0 2px;animation:mg-dot 1.2s .2s infinite"></span><span class="mg-dot-bounce" style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin:0 2px;animation:mg-dot 1.2s .4s infinite"></span> AI ××פש ×¢××ר×...</div> 2153 </div> 2154 </div> 2155 </div> 2156 <style> 2157 .mg-shimmer { background:linear-gradient(90deg,#2a2a2a 25%,#333 50%,#2a2a2a 75%);background-size:200% 100%;animation:mg-shimmer 1.6s infinite; } 2158 @keyframes mg-shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} } 2159 @keyframes mg-fadeSlide { from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:none} } 2160 @keyframes mg-blink { 0%,100%{opacity:1} 50%{opacity:0} } 2161 @keyframes mg-dot { 0%,80%,100%{transform:scale(.6);opacity:.4} 40%{transform:scale(1);opacity:1} } 2162 @keyframes mg-progress { from{width:0%} to{width:100%} } 2163 </style> 2164 </div> 2165 2166 <!-- ââ EXPIRED VIEW âââââââââââââââââââââââââââââââââââââ --> 2167 <div id="view-expired" style="display:none;padding:28px 24px"> 2168 <div class="admin-header" style="margin-bottom:24px"> 2169 <div class="admin-title">â° ×¤× ×ª××§×£ â ××ר ס×× ×× ×ª×¦×××</div> 2170 </div> 2171 <div style="font-size:13px;color:var(--gray5);margin-bottom:24px">×××¨× 10 ×××× ××××× ×צ××ר××, ××§×פ×× ×ס××× ×"×¤× ×ª××§×£" â ××ר ×ת ×ס×× ×× ×©××ª× ××¢×××£</div> 2172 <div class="coupon-grid" id="grid-expired-demo-top" style="padding:0"></div> 2173 </div> 2174 2175 <!-- ââ MOLE GAME VIEW ââââââââââââââââââââââââââââââââââââââ --> 2176 <div id="view-mole" style="display:none;padding:28px 24px"> 2177 <div class="admin-header" style="margin-bottom:24px"> 2178 <div class="admin-title">ð¯ ×ש××§×× â תפ×ס ×§×פ×× ××!</div> 2179 </div> 2180 <div id="wam-view-inner" style="text-align:center;max-width:400px;margin:0 auto"> 2181 <div style="font-size:13px;color:var(--gray5);margin-bottom:16px">תפ×ס ×ת ××§×פ×× ×× ×©×§×פצ×× â ××× × ×§×××ת תצ××ר?</div> 2182 <div style="font-size:22px;font-weight:700;color:var(--accent);margin-bottom:16px" id="wam-view-score">0 × ×§×××ת</div> 2183 <div id="wam-view-grid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;max-width:320px;margin:0 auto 20px"></div> 2184 <div style="display:flex;gap:10px;justify-content:center"> 2185 <button class="btn-sm btn-ghost" onclick="wamStart()">â¶ ×ת××</button> 2186 <button class="btn-sm btn-ghost" onclick="wamStop()">â עצ×ר</button> 2187 </div> 2188 </div> 2189 </div> 2190 2191 <!-- ââ LOAD ANIMATIONS VIEW (real in-search game) ââââââââ --> 2192 <div id="view-minigames" style="display:none;padding:28px 24px"> 2193 <div class="admin-header" style="margin-bottom:24px"> 2194 <div class="admin-title">ð¬ ×× ××צ××ת ××¢×× × â ×× ×©×¨×¥ ×××× ××פ×ש</div> 2195 </div> 2196 <div style="max-width:700px"> 2197 <!-- Skeleton grid --> 2198 <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;margin-bottom:20px" id="demo-skel-grid"></div> 2199 <!-- Tips panel --> 2200 <div style="background:var(--gray3);border-radius:14px;padding:16px 24px"> 2201 <div style="display:flex;align-items:center;justify-content:center;gap:14px"> 2202 <div id="demo-tip-icon" style="font-size:26px;flex-shrink:0">ð¡</div> 2203 <div id="demo-tip-text" style="font-size:15px;color:var(--gray6);line-height:1.5"></div> 2204 </div> 2205 <div style="margin-top:12px;display:flex;justify-content:center"> 2206 <div style="width:80px;height:2px;background:var(--gray4);border-radius:2px;overflow:hidden"> 2207 <div id="demo-tip-bar" style="height:2px;background:var(--accent);border-radius:2px;width:0%"></div> 2208 </div> 2209 </div> 2210 </div> 2211 <div style="margin-top:16px;display:flex;gap:10px"> 2212 <button class="btn-sm btn-ghost" onclick="demoGameStart()">â¶ ×פע×</button> 2213 <button class="btn-sm btn-ghost" onclick="demoGameStop()">â עצ×ר</button> 2214 </div> 2215 </div> 2216 </div> 2217 2218 <!-- ââ DEBUGGING VIEW âââââââââââââââââââââââââââââââââââ --> 2219 <!-- ââ Store Pages Demo âââââââââââââââââââââââââââââââââââââââââââââââ --> 2220 <div id="view-store-pages-demo" style="display:none;padding:28px 24px"> 2221 <div class="admin-header" style="margin-bottom:20px"> 2222 <div class="admin-title">ðª ×××§×פ â ××¤× ×× ×××ת ×××××</div> 2223 </div> 2224 <div style="background:rgba(0,232,122,0.07);border:1px solid rgba(0,232,122,0.2);border-radius:12px;padding:16px 20px;margin-bottom:20px;font-size:13px;color:var(--gray6);line-height:1.7"> 2225 ש×××©× ×¢×צ×××× ×©×× ×× ×××¤× ×× ×ת. ××ר ×× ×ת + ×¢×צ×× ××× ×ר××ת ×××§×פ ×××. <strong style="color:var(--accent)">××× ××××</strong> â ××× ×©×× ×××× ××§×× ×× ×× ×ª×× ××. 2226 </div> 2227 2228 <!-- Layout selector --> 2229 <div style="margin-bottom:16px"> 2230 <div style="font-size:11px;color:var(--gray5);margin-bottom:8px;font-weight:600;letter-spacing:0.5px">×¢×צ××</div> 2231 <div style="display:flex;gap:8px;flex-wrap:wrap"> 2232 <button id="sd-layout-a" onclick="_sdSetLayout('a')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;
2232cursor:pointer;font-family:'DM Sans',sans-serif">A â Hero + ×ר××</button> 2233 <button id="sd-layout-b" onclick="_sdSetLayout('b')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif">B â ס××××ר + רש×××</button> 2234 <button id="sd-layout-c" onclick="_sdSetLayout('c')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif">C â ××× ××× SEO</button> 2235 <button id="sd-layout-d" onclick="_sdSetLayout('d')" style="padding:8px 18px;background:var(--accent);color:#000;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif;font-weight:600">D â ס××××ר ×ש×פר â </button> 2236 </div> 2237 </div> 2238 2239 <!-- Store selector --> 2240 <div style="margin-bottom:24px"> 2241 <div style="font-size:11px;color:var(--gray5);margin-bottom:8px;font-weight:600;letter-spacing:0.5px">×× ×ת</div> 2242 <div style="display:flex;gap:8px;flex-wrap:wrap" id="sd-store-btns"> 2243 <button onclick="_sdSetStore('ksp')" style="padding:8px 18px;background:var(--accent);color:#000;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif;font-weight:600">KSP</button> 2244 <button onclick="_sdSetStore('superpharm')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif">ס×פר פ×ר×</button> 2245 <button onclick="_sdSetStore('aliexpress')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif">AliExpress</button> 2246 <button onclick="_sdSetStore('asos')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif">ASOS</button> 2247 <button onclick="_sdSetStore('castro')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif">קס×ר×</button> 2248 </div> 2249 </div> 2250 2251 <div id="store-demo-content" style="border:1px solid var(--gray3);border-radius:14px;overflow:hidden;min-height:400px"></div> 2252 </div> 2253 2254 <!-- ââ Home Page Demo âââââââââââââââââââââââââââââââââââââââââââââââ --> 2255 <div id="view-home-demo" style="display:none;padding:28px 24px"> 2256 <div class="admin-header" style="margin-bottom:20px"> 2257 <div class="admin-title">ð ×××§×פ â ×¢××× ×××ת</div> 2258 </div> 2259 <div style="background:rgba(0,232,122,0.07);border:1px solid rgba(0,232,122,0.2);border-radius:12px;padding:16px 20px;margin-bottom:20px;font-size:13px;color:var(--gray6);line-height:1.7"> 2260 5 ×¢×צ×××× ×©×× ×× ××¢××× ×××ת ×¢× ×× ×××ת ×××§×× ×§×פ×× ×× ××××××. ××××¦× ×¢× ×× ×ת ××¤× × ×-/store/. <strong style="color:var(--accent)">××× ××××</strong> 2261 </div> 2262 <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:24px"> 2263 <button id="hp-btn-a" onclick="_hpSetLayout('a')" style="padding:8px 18px;background:var(--accent);color:#000;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif;font-weight:600">A â ×ר×× ×× ×××ת</button> 2264 <button id="hp-btn-b" onclick="_hpSetLayout('b')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif">B â ש×ר×ת ×§×××ר××</button> 2265 <button id="hp-btn-c" onclick="_hpSetLayout('c')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;
2265cursor:pointer;font-family:'DM Sans',sans-serif">C â ער××× + ×ר××</button> 2266 <button id="hp-btn-d" onclick="_hpSetLayout('d')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif">D â ××××××× ××× ×××</button> 2267 <button id="hp-btn-e" onclick="_hpSetLayout('e')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif">E â ×§×××ר××ת ר×ש×ת</button> 2268 <button id="hp-btn-f" onclick="_hpSetLayout('f')" style="padding:8px 18px;background:var(--gray3);color:var(--white);border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif">F â ×§×××× â </button> 2269 </div> 2270 <div id="hp-demo-content" style="border:1px solid var(--gray3);border-radius:14px;overflow:hidden;min-height:500px"></div> 2271 </div> 2272 2273 <!-- ââ Contact Messages âââââââââââââââââââââââââââââââââââââââââââââââ --> 2274 <div id="view-contact-messages" style="display:none;padding:28px 24px"> 2275 <div class="admin-header" style="margin-bottom:20px"> 2276 <div class="admin-title">âï¸ ××××¢×ת צ×ר קשר</div> 2277 </div> 2278 <div id="contact-messages-list" style="display:flex;flex-direction:column;gap:12px"> 2279 <div style="color:var(--gray5);text-align:center;padding:40px">×××¢×...</div> 2280 </div> 2281 </div> 2282 2283 <div id="view-debug" style="display:none;padding:28px 24px"> 2284 <div class="admin-header" style="margin-bottom:20px"> 2285 <div class="admin-actions"> 2286 <button class="btn-sm btn-ghost" id="debug-clear-btn" onclick="debugClear()">â × ×§×</button> 2287 </div> 2288 <div class="admin-title">ð Pipeline Debugger â ××××× ××××</div> 2289 </div> 2290 2291 <!-- Run bar --> 2292 <div style="display:flex;gap:10px;align-items:center;margin-bottom:16px;flex-direction:row-reverse"> 2293 <input id="debug-query-input" type="text" placeholder="×©× ×× ×ת, ××ש×: fox" dir="rtl" 2294 style="flex:1;max-width:320px;background:var(--gray2);border:1px solid var(--gray3);border-radius:8px;padding:8px 14px;font-size:13px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif" 2295 onkeydown="if(event.key==='Enter')debugRun()" /> 2296 <button id="debug-run-btn" onclick="debugRun()" 2297 style="padding:8px 20px;background:var(--accent);color:#000;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:'DM Sans',sans-serif"> 2298 â¶ ×רץ 2299 </button> 2300 <span id="debug-status" style="font-size:12px;color:var(--gray5)"></span> 2301 </div> 2302 2303 <!-- Terminal --> 2304 <div id="debug-terminal" 2305 style="background:#0d0d0d;border:1px solid #222;border-radius:12px;padding:14px 16px;font-family:'Courier New',monospace;font-size:12px;line-height:1.7;min-height:420px;max-height:65vh;overflow-y:auto;white-space:pre-wrap;word-break:break-all;color:#ccc"> 2306 <span style="color:#555">// ×××¥ ×רץ ××× ×ר××ת ×ת ×-pipeline ×פע×××</span> 2307 </div> 2308 2309 <!-- Result report --> 2310 <div id="debug-result" style="margin-top:20px;display:none"> 2311 <!-- Section title --> 2312 <div style="font-family:'Syne',sans-serif;font-size:16px;font-weight:700;color:var(--white);margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid var(--gray3)"> 2313 ð ××× ×ª×צ××ת 2314 </div> 2315 <!-- Stats chips --> 2316 <div id="debug-stats" style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:18px"></div> 2317 <!-- Source breakdown --> 2318 <div id="debug-sources" style="margin-bottom:18px"></div> 2319 <!-- Main table --> 2320 <div id="debug-table-wrap" style="background:var(--gray2);border:1px solid var(--gray3);border-radius:12px;overflow:hidden"> 2321 <div style="padding:12px 16px;border-bottom:1px solid var(--gray3);font-size:12px;font-weight:600;color:var(--gray5)"> 2322 ×§×פ×× ×× ×©× ×צ×× 2323 </div> 2324 <div style="overflow-x:auto"> 2325 <table id="debug-table" style="width:100%;border-collapse:collapse;font-size:12px"> 2326 <thead> 2327 <tr style="background:var(--gray1)"> 2328 <th style="padding:9px 14px;text-align:right;color:var(--gray5);font-weight:500;white-space:nowrap">×× ×ת</th> 2329 <th style="padding:9px 14px;text-align:right;color:var(--gray5);font-weight:500;white-space:nowrap">×§××</th> 2330 <th style="padding:9px 14px;text-align:right;color:var(--gray5);font-weight:500;white-space:nowrap">×× ××</th> 2331 <th style="padding:9px 14px;text-align:right;color:var(--gray5);font-weight:500;white-space:nowrap">×§×××ר××</th> 2332 <th style="padding:9px 14px;text-align:right;color:var(--gray5);font-weight:500">×ער×ת</th> 2333 <th style="padding:9px 14px;text-align:right;color:var(--gray5);font-weight:500;white-space:nowrap">××§×ר</th> 2334 </tr> 2335 </thead> 2336 <tbody id="debug-table-body"></tbody> 2337 </table> 2338 </div> 2339 </div> 2340 </div> 2341 </div> 2342 2343 <div id="view-null-domain" style="display:none"> 2344 <div class="admin-header"> 2345 <div class="admin-actions"> 2346 <button class="btn-sm btn-ghost" onclick="loadNullDomainStores()">âº ×¨×¢× ×</button> 2347 <button id="nd-delete-orphans-btn" class="btn-sm btn-ghost" onclick="_ndDeleteOrphans()" 2348 style="color:#f97316;border-color:#7c2d12"> 2349 ð§¹ ×××§ ×§×פ×× ×× ×ת×××× 2350 </button> 2351 <button id="nd-delete-btn" class="btn-sm" onclick="_ndDeleteSelected()" 2352 style="display:none;background:#7f1d1d;color:#fca5a5;border:1px solid #991b1b"> 2353 ð ×××§ × ××ר×× 2354 </button> 2355 </div> 2356 <div class="admin-title">
2356ð ××× ××××××</div> 2357 </div> 2358 <div style="padding:0 0 14px;font-size:13px;color:var(--gray6);line-height:1.6"> 2359 ×× ×××ת ש×סר ××× ×××××× â ××§×פ×× ×× ×©××× ××סתר×× ×××שת×ש×× ×¢× ×©×ת××§×. 2360 </div> 2361 <div class="table-wrap"> 2362 <table id="nulldomain-table"> 2363 <thead><tr> 2364 <th style="width:36px;text-align:center"> 2365 <input type="checkbox" id="nd-select-all" onchange="_ndToggleAll(this.checked)" 2366 style="cursor:pointer;width:15px;height:15px;accent-color:var(--accent)"> 2367 </th> 2368 <th style="width:36px;color:var(--gray5)">#</th> 2369 <th style="min-width:130px">×©× ×× ××× <span style="color:#fc8181">*</span></th> 2370 <th style="min-width:120px">×©× ×¢××¨× <span style="color:#fc8181">*</span></th> 2371 <th style="width:60px;text-align:center">×§×פ×× ××</th> 2372 <th>ש××ת × ×ספ×× (aliases)</th> 2373 <th style="min-width:160px">××§×ר</th> 2374 <th style="min-width:160px">×××××× <span style="color:#fc8181">*</span></th> 2375 <th style="width:150px"></th> 2376 </tr></thead> 2377 <tbody id="nulldomain-tbody"> 2378 <tr><td colspan="9" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr> 2379 </tbody> 2380 </table> 2381 </div> 2382 </div> 2383 2384 <div id="view-trusted-international" style="display:none"> 2385 <div class="admin-header"> 2386 <div class="admin-actions"> 2387 <button class="btn-sm btn-primary" onclick="_tiAddPrompt()">+ ××סף ××××××</button> 2388 <button class="btn-sm btn-ghost" onclick="loadTrustedInternational()">âº ×¨×¢× ×</button> 2389 </div> 2390 <div class="admin-title">ð ×תר×× ××× ××××××× ××רש××</div> 2391 </div> 2392 <div style="padding:0 0 14px;font-size:13px;color:var(--gray6);line-height:1.6"> 2393 רש××ת ×××××× ×× ××× ××××××× ×©××תר ××ס××£ ××× ×§×פ×× ×× ××¨× promocodis.co.il. ×תר×× ×©××× × ×רש×××, ××× × ×שר×××× ×××× ××× ×ª××× ××¢×ר×ת â ××××××. 2394 </div> 2395 <div class="table-wrap"> 2396 <table id="trusted-intl-table"> 2397 <thead><tr> 2398 <th style="width:36px;color:var(--gray5)">#</th> 2399 <th>××××××</th> 2400 <th>× ×סף</th> 2401 <th style="width:100px;text-align:center">פע×××ת</th> 2402 </tr></thead> 2403 <tbody id="trusted-intl-tbody"> 2404 <tr><td colspan="4" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr> 2405 </tbody> 2406 </table> 2407 </div> 2408 </div> 2409 2410 <div id="view-blocked-sites" style="display:none"> 2411 <div class="admin-header"> 2412 <div class="admin-actions"> 2413 <button class="btn-sm btn-ghost" onclick="loadBlockedSites()">âº ×¨×¢× ×</button> 2414 </div> 2415 <div class="admin-title">ð« ×תר×× ×ס××××</div> 2416 </div> 2417 <div style="padding:0 0 14px;font-size:13px;color:var(--gray6);line-height:1.6"> 2418 ×תר×× ×©× ×ס×× â ×§×פ×× ×× ××× ×× ××ספ×. × ××ª× ×××××§ ×§×פ×× ×× ×§××××× ×©××××¢× ××תר×× ×××. 2419 </div> 2420 <div class="table-wrap"> 2421 <table id="blocked-sites-table"> 2422 <thead><tr> 2423 <th style="width:36px;color:var(--gray5)">#</th> 2424 <th>×תר</th> 2425 <th>ת×××ר ××תר</th> 2426 <th>××× × ×ס×</th> 2427 <th style="width:130px;text-align:center">פע×××ת</th> 2428 </tr></thead> 2429 <tbody id="blocked-sites-tbody"> 2430 <tr><td colspan="5" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr> 2431 </tbody> 2432 </table> 2433 </div> 2434 </div> 2435 2436 <div id="view-stores" style="display:none"> 2437 <div class="admin-header"> 2438 <div class="admin-actions"> 2439 <button class="btn-sm btn-ghost" onclick="loadStoresView()">âº ×¨×¢× ×</button> 2440 <input id="stores-search" type="text" placeholder="××פ×ש ×× ×ת..." oninput="_storesFilter()" 2441 style="background:var(--gray2);border:1px solid var(--gray3);border-radius:8px;padding:6px 12px;font-size:12px;color:var(--white);width:180px;direction:rtl"> 2442 <select id="stores-sort-sel" onchange="_storesSetSortKey(this.value)" style="background:var(--gray2);color:var(--gray6);border:1px solid var(--gray3);border-radius:8px;padding:3px 8px;font-size:12px;cursor:pointer;outline:none;direction:rtl"> 2443 <option value="name">××××: ש×</option> 2444 <option value="coupons">××××: ×§×פ×× ××</option> 2445 <option value="domain">
2445××××: ××××××</option> 2446 </select> 2447 <button id="stores-sort-dir-btn" onclick="_storesToggleDir()" title="××¤× ××××× ××××" style="background:var(--gray2);color:var(--gray6);border:1px solid var(--gray3);border-radius:8px;padding:3px 9px;font-size:13px;cursor:pointer;line-height:1">â</button> 2448 <div style="display:flex;gap:4px;margin-right:4px"> 2449 <button id="stores-origin-all" class="btn-sm" onclick="_storesOriginFilter('all')" style="font-size:11px;background:var(--accent);color:#000">×××</button> 2450 <button id="stores-origin-il" class="btn-sm btn-ghost" onclick="_storesOriginFilter('il')" style="font-size:11px">ð®ð± IL</button> 2451 <button id="stores-origin-int" class="btn-sm btn-ghost" onclick="_storesOriginFilter('int')" style="font-size:11px">ð INT</button> 2452 <button id="stores-origin-null" class="btn-sm btn-ghost" onclick="_storesOriginFilter('null')" style="font-size:11px">â ×××</button> 2453 </div> 2454 </div> 2455 <div class="admin-title">ðª ×× ×××ת</div> 2456 </div> 2457 <div class="table-wrap"> 2458 <table id="stores-table"> 2459 <thead><tr> 2460 <th style="width:36px;color:var(--gray5)">#</th> 2461 <th style="min-width:130px">×©× ×× ×ת</th> 2462 <th style="min-width:120px">×©× ×× ×ת ×¢×ר×ת</th> 2463 <th style="width:55px;text-align:center">×§×פ×× ××</th> 2464 <th style="min-width:170px">××××××</th>
2465 <th style="min-width:200px">Aliases</th> 2466 <th style="width:44px;text-align:center">××××</th> 2467 <th style="width:70px;text-align:center">×××× ×</th> 2468 <th style="width:120px"></th> 2469 </tr></thead> 2470 <tbody id="stores-tbody"> 2471 <tr><td colspan="6" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr> 2472 </tbody> 2473 </table> 2474 </div> 2475 <div id="stores-pagination"></div> 2476 </div> 2477 2478 <div id="view-orphan-coupons" style="display:none"> 2479 <div class="admin-header"> 2480 <div class="admin-actions"> 2481 <button class="btn-sm btn-ghost" onclick="loadOrphanCoupons()">âº ×¨×¢× ×</button> 2482 <button id="orphan-search-btn" class="btn-sm" onclick="_orphanSearchStores()" 2483 style="display:none;background:var(--accent);color:#000;border:none;font-weight:600"> 2484 ð ×פש ×× ×××ת 2485 </button> 2486 <button id="orphan-delete-selected-btn" class="btn-sm" onclick="_deleteSelectedOrphans()" 2487 style="display:none;background:#7f1d1d;color:#fca5a5;border:1px solid #991b1b"> 2488 ð ×××§ × ××ר×× (<span id="orphan-selected-count">0</span>) 2489 </button> 2490 <button id="orphan-delete-btn" class="btn-sm" onclick="_deleteOrphanCoupons()" 2491 style="display:none;background:#7f1d1d;color:#fca5a5;border:1px solid #991b1b;opacity:0.6"> 2492 ð ×××§ ××× 2493 </button> 2494 </div> 2495 <div class="admin-title">ð ×§×פ×× ×× ××× ××××××</div> 2496 </div> 2497 <div style="padding:0 0 14px;font-size:13px;color:var(--gray6);line-height:1.6"> 2498 ×§×פ×× ×× ×©××× ×ת ש××× ××× × ×§×××ת ××××ת ××× ×××ת â ×××§×ר ××ש×× (×××ר×, סר××§×, × ×××××ר). ××× ××סתר×× ××שת×ש×× ××ש ×××××§ ××ת×. 2499 </div> 2500 <div class="table-wrap"> 2501 <table id="orphan-table"> 2502 <thead><tr> 2503 <th style="width:32px;padding:8px 4px 8px 10px"> 2504 <input type="checkbox" id="orphan-check-all" onchange="_orphanToggleAll(this.checked)" 2505 style="cursor:pointer;accent-color:var(--accent)"> 2506 </th> 2507 <th>×× ×ת (×× ×§×××ת)</th> 2508 <th>×§××</th> 2509 <th>××§×ר</th> 2510 <th>ת×ר×× ×¡×¨××§×</th> 2511 </tr></thead> 2512 <tbody id="orphan-tbody"> 2513 <tr><td colspan="6" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr> 2514 </tbody> 2515 </table> 2516 </div> 2517 </div> 2518 2519 <!-- orphan store search modal --> 2520 <div id="orphan-search-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:9000;align-items:center;justify-content:center"> 2521 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;width:520px;max-width:95vw;max-height:80vh;display:flex;flex-direction:column"> 2522 <div style="padding:18px 20px 14px;border-bottom:1px solid var(--gray3);display:flex;align-items:center;justify-content:space-between"> 2523 <div> 2524 <div style="font-size:15px;font-weight:600;color:var(--white)">ת×צ××ת ××פ×ש ×× ×××ת</div> 2525 <div id="orphan-modal-sub" style="font-size:12px;color:var(--gray5);margin-top:2px">××פש...</div> 2526 </div> 2527 <button onclick="_orphanModalClose()" style="background:none;border:none;color:var(--gray5);font-size:20px;cursor:pointer;line-height:1">Ã</button> 2528 </div> 2529 <div id="orphan-modal-list" style="overflow-y:auto;flex:1;padding:14px 20px;display:flex;flex-direction:column;gap:8px"> 2530 <div style="color:var(--gray5);text-align:center;padding:24px">××פש ×ת×××ת...</div> 2531 </div> 2532 <div style="padding:14px 20px;border-top:1px solid var(--gray3);display:flex;align-items:center;justify-content:space-between"> 2533 <div id="orphan-modal-summary" style="font-size:12px;color:var(--gray5)"></div> 2534 <div style="display:flex;gap:8px"> 2535 <button onclick="_orphanModalClose()" style="padding:7px 14px;background:var(--gray2);color:var(--gray5);border:1px solid var(--gray3);border-radius:8px;font-size:13px;
2535cursor:pointer">×××××</button> 2536 <button id="orphan-modal-approve" onclick="_orphanModalApprove()" disabled 2537 style="padding:7px 16px;background:#166534;color:#bbf7d0;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;opacity:0.4"> 2538 ×שר ××××× 2539 </button> 2540 </div> 2541 </div> 2542 </div> 2543 </div> 2544 2545 <!-- ââ ××× ×××× × â stores with origin IS NULL âââââââââââââââââââââââââ --> 2546 <div id="view-no-origin" style="display:none"> 2547 <div class="admin-header"> 2548 <div class="admin-actions"> 2549 <button class="btn-sm btn-ghost" onclick="loadNoOriginStores()">âº ×¨×¢× ×</button> 2550 </div> 2551 <div class="admin-title">ð ××× ×××× ×</div> 2552 </div> 2553 <div style="padding:0 0 14px;font-size:13px;color:var(--gray6);line-height:1.6"> 2554 ×× ×××ת שע×××× ×× ×××× ××× ×××× × (origin=NULL). ×ש ×××××§ ××× ×ת ××××××ר IL / INT. 2555 </div> 2556 <div class="table-wrap"> 2557 <table id="no-origin-table"> 2558 <thead><tr> 2559 <th style="width:36px;color:var(--gray5)">#</th> 2560 <th>×©× ×× ×ת</th> 2561 <th>××××××</th> 2562 <th style="width:55px;text-align:center">×§×פ×× ××</th> 2563 <th style="width:160px;text-align:center">×××× ×</th> 2564 </tr></thead> 2565 <tbody id="no-origin-tbody"> 2566 <tr><td colspan="5" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr> 2567 </tbody> 2568 </table> 2569 </div> 2570 <div id="no-origin-pagination"></div> 2571 </div> 2572 2573 <!-- ââ תצ××ת ×§×פ×× ×× â admin coupon preview âââââââââââââââââââââââââââââ --> 2574 <div id="view-coupon-preview" style="display:none"> 2575 <div class="admin-header"> 2576 <div class="admin-actions"> 2577 <button class="btn-sm btn-ghost" onclick="loadCouponPreview()">âº ×¨×¢× ×</button> 2578 <div style="display:flex;gap:6px"> 2579 <button id="cprev-filter-all" class="btn-sm" onclick="_cprevSetFilter('all')" 2580 style="background:var(--accent);color:#000;font-size:11px">×××</button> 2581 <button id="cprev-filter-il" class="btn-sm btn-ghost" onclick="_cprevSetFilter('il')" 2582 style="font-size:11px">ð®ð± ×שר××</button> 2583 <button id="cprev-filter-int" class="btn-sm btn-ghost" onclick="_cprevSetFilter('int')" 2584 style="font-size:11px">ð ××× ×××××</button> 2585 </div> 2586 </div> 2587 <div class="admin-title">ð« תצ××ת ×§×פ×× ××</div> 2588 </div> 2589 <div id="coupon-preview-grid" style="padding:20px;display:grid;grid-template-columns:1fr;gap:8px"> 2590 <div style="color:var(--gray5);text-align:center;grid-column:1/-1;padding:40px">×××¢×...</div> 2591 </div> 2592 <div id="coupon-preview-pagination" style="padding:0 20px 20px"></div> 2593 </div> 2594 2595 <!-- ââ Dedicated analytics views (panels moved here by JS) âââââââââââââ --> 2596 <div id="view-analytics-usage" style="display:none;padding:24px"></div> 2597 <div id="view-analytics-speed" style="display:none;padding:24px"></div> 2598 <div id="view-analytics-popup" style="display:none;padding:24px"></div> 2599 <div id="view-analytics-extension" style="display:none;padding:24px"></div> 2600 <div id="view-analytics-buttons" style="display:none;padding:24px"></div> 2601 <div id="view-user-profiles" style="display:none;padding:24px"></div> 2602 <div id="view-search-intel" style="display:none;padding:24px"></div> 2603 <div id="view-demand-gaps" style="display:none;padding:24px"></div> 2604 <div id="view-traffic-sources" style="display:none;padding:24px"></div> 2605 <div id="view-geo-map" style="display:none;padding:24px"></div> 2606 <div id="view-device-stats" style="display:none;padding:24px"></div> 2607 <div id="view-js-errors" style="display:none;padding:24px"></div> 2608 2609 <!-- ââ ×§×××ר××ת ×× ×××ת âââââââââââââââââââââââââââââââââââââââââââââââ --> 2610 <div id="view-store-categories" style="display:none"> 2611 <div class="admin-header"> 2612 <div class="admin-actions">
2613 <button class="btn-sm btn-ghost" onclick="loadStoreCategoriesView()">âº ×¨×¢× ×</button> 2614 <input id="cat-search" type="text" placeholder="××פ×ש ×× ×ת..." oninput="_catFilter()" 2615 style="background:var(--gray2);border:1px solid var(--gray3);border-radius:8px;padding:6px 12px;font-size:12px;color:var(--white);width:180px;direction:rtl"> 2616 <select id="cat-filter-sel" onchange="_catFilter()" style="background:var(--gray2);color:var(--gray6);border:1px solid var(--gray3);border-radius:8px;padding:3px 8px;font-size:12px;cursor:pointer;outline:none;direction:rtl"> 2617 <option value="all">×××</option> 2618 <option value="empty">××× ×§×××ר××</option> 2619 <option value="has">×¢× ×§×××ר××</option> 2620 </select> 2621 </div> 2622 <div class="admin-title">ð· ×§×××ר××ת ×× ×××ת</div> 2623 </div> 2624 <div style="padding:0 0 14px;font-size:13px;color:var(--gray6);line-height:1.6"> 2625 ×××¥ ×¢× ×ª×× ××§×××ר×× ××× ×××ס××£/××ס×ר. ×ש×× ×× × ×©×ר ××× ×××ר ×××צ×. 2626 </div> 2627 <div class="table-wrap"> 2628 <table id="cat-table"> 2629 <thead><tr> 2630 <th style="width:36px;color:var(--gray5)">#</th> 2631 <th style="min-width:140px">×©× ×× ×ת</th> 2632 <th style="min-width:120px">×©× ×¢×ר×</th> 2633 <th style="min-width:160px">××××××</th> 2634 <th>×§×××ר××ת</th> 2635 <th style="width:60px;text-align:center">ס×××ס</th> 2636 </tr></thead> 2637 <tbody id="cat-tbody"> 2638 <tr><td colspan="6" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr> 2639 </tbody> 2640 </table> 2641 </div> 2642 <div id="cat-pagination"></div> 2643 </div> 2644 2645 </div> 2646 <div id="mob-backdrop" onclick="closeMobMenu()"></div> 2647 <div class="sidebar" id="admin-sidebar"> 2648 <button id="mob-close-btn" onclick="closeMobMenu()">â</button> 2649 <div class="sidebar-section"> 2650 <span class="sidebar-label">××××</span> 2651 <button class="sidebar-item on" onclick="setAdminView(this,'dashboard')"><div class="sidebar-icon" style="background:#1a1a1a">â</div> ×ש××ר×</button> 2652 <button class="sidebar-item" onclick="setAdminView(this,'coupons')"><div class="sidebar-icon" style="background:#1a1a1a">â</div> ×× ××§×פ×× ××<span class="sidebar-count" id="adm-sidebar-count">â</span></button> 2653 <button class="sidebar-item" onclick="setAdminView(this,'stores')"><div class="sidebar-icon" style="background:#0a1a2a">ðª</div> ×× ××× ×××ת<span class="sidebar-count" id="src-stores-count">â</span></button> 2654 <button class="sidebar-item" onclick="setAdminView(this,'store-categories');loadStoreCategoriesView()"><div class="sidebar-icon" style="background:#0a2a1a">ð·</div> ×§×××ר××ת ×× ×××ת<span class="sidebar-count" id="src-cat-count">â</span></button> 2655 </div> 2656 <div class="sidebar-section"> 2657 <span class="sidebar-label">×× ×××××§×</span> 2658 <button class="sidebar-item" onclick="setAdminView(this,'analytics-usage')"><div class="sidebar-icon" style="background:#0d1a2a">ð</div> ×× ××××קס ש×××ש</button> 2659 <button class="sidebar-item" onclick="setAdminView(this,'analytics-speed')"><div class="sidebar-icon" style="background:#0a1a0a">â¡</div> ××× × ××פ×ש</button> 2660 <button class="sidebar-item" onclick="setAdminView(this,'analytics-popup')"><div class="sidebar-icon" style="background:#1a0d0a">ð«</div> פ×פ×פ ×§×פ×× ××</button> 2661 <button class="sidebar-item" onclick="setAdminView(this,'analytics-extension')"><div class="sidebar-icon" style="background:#0a1a10">ð§©</div> ת×סף Chrome</button> 2662 <button class="sidebar-item" onclick="setAdminView(this,'analytics-buttons')"><div class="sidebar-icon" style="background:#1a1a0a">ð±ï¸</div> תפר×× ××פת×ר××</button> 2663 <span class="sidebar-label">×שת×ש××</span> 2664 <button class="sidebar-item" onclick="setAdminView(this,'user-profiles')"><div class="sidebar-icon" style="background:#0d1a2a">ð¤</div> פר×פ××× ×שת×ש××</button> 2665 <button class="sidebar-item" onclick="setAdminView(this,'search-intel')"><div class="sidebar-icon" style="background:#0a1a0a">ð</div> ××פ×ש××</button> 2666 <button class="sidebar-item" onclick="setAdminView(this,'demand-gaps')"><div class="sidebar-icon" style="background:#1a0a0a">ð³ï¸</div> ×××§×ש ××× ×§×פ×× ××</button> 2667 <button class="sidebar-item" onclick="setAdminView(this,'traffic-sources')"><div class="sidebar-icon" style="background:#0a0a1a">ð¦</div> ××§×ר×ת ×ª× ××¢×</button> 2668 <button class="sidebar-item" onclick="setAdminView(this,'geo-map')"><div class="sidebar-icon" style="background:#0a1a10">ðºï¸</div> ×××××רפ××</button> 2669 <button class="sidebar-item" onclick="setAdminView(this,'device-stats')"><div class="sidebar-icon" style="background:#1a1a0a">ð»</div> ××ש×ר×× ××פ××¤× ××</button> 2670 <button class="sidebar-item" onclick="setAdminView(this,'js-errors')"><div class="sidebar-icon" style="background:#1a0a0a">ð</div> ש××××ת JS</button> 2671 </div> 2672 <div class="sidebar-section">
2673 <span class="sidebar-label">××§×ר×ת</span> 2674 <button class="sidebar-item" onclick="setAdminView(this,'sources')"><div class="sidebar-icon" style="background:#1a2a1a">â²</div> ×××ר×<span class="sidebar-count" id="src-tg-count">â</span></button> 2675 <button class="sidebar-item" onclick="setAdminView(this,'coupon-sites')"><div class="sidebar-icon" style="background:#1a2a1a">ð·</div> ××ª×¨× ×§×פ×× ×× ××××<span class="sidebar-count" id="src-coupon-sites-count">â</span></button> 2676 <button class="sidebar-item" onclick="setAdminView(this,'web')"><div class="sidebar-icon" style="background:#2a2a1a">â</div> ×תר×× ×©× ×¡×¨×§×<span class="sidebar-count" id="src-web-count">â</span></button> 2677 <button class="sidebar-item" onclick="setAdminView(this,'instagram')"><div class="sidebar-icon" style="background:#2a1a2a">ð¸</div> ××× ×¤×××× ×¡×¨××<span class="sidebar-count" id="src-ig-count">â</span></button> 2678 <button class="sidebar-item" onclick="setAdminView(this,'affiliate')"><div class="sidebar-icon" style="background:#1a2a1a">ð¤</div> ש×תפ×××ת<span class="sidebar-count" id="src-aff-count">â</span></button> 2679 <button class="sidebar-item" onclick="setAdminView(this,'blocked-sites')"><div class="sidebar-icon" style="background:#2a0a0a">ð«</div> ×תר×× ×ס××××<span class="sidebar-count" id="src-blocked-count">â</span></button> 2680 <button class="sidebar-item" onclick="setAdminView(this,'trusted-international')"><div class="sidebar-icon" style="background:#0a2a0a">ð</div> ××× ××××× ××רש×<span class="sidebar-count" id="src-trusted-intl-count">â</span></button> 2681 </div> 2682 <div class="sidebar-section"> 2683 <span class="sidebar-label">×××××צ××</span> 2684 <button class="sidebar-item" onclick="setAdminView(this,'automation-scans')"><div class="sidebar-icon" style="background:#0a1a2a">ð¤</div> סר××§×ת</button> 2685 <button class="sidebar-item" onclick="setAdminView(this,'newsletters')"><div class="sidebar-icon" style="background:#1a1a2a">â</div> × ×××××ר××<span class="sidebar-count" id="src-nl-count">0</span></button> 2686 </div> 2687 <div class="sidebar-section"> 2688 <span class="sidebar-label">××××§×ת</span> 2689 <button class="sidebar-item" onclick="setAdminView(this,'null-domain')"><div class="sidebar-icon" style="background:#2a1a00">ð</div> ××× ××××××<span class="sidebar-count" id="src-nulldomain-count">â</span></button> 2690 <button class="sidebar-item" onclick="setAdminView(this,'orphan-coupons')"><div class="sidebar-icon" style="background:#2a0a0a">ð</div> ×§×פ×× ×× ××× ××××××<span class="sidebar-count" id="src-orphan-count">â</span></button> 2691 <button class="sidebar-item" onclick="setAdminView(this,'no-origin')"><div class="sidebar-icon" style="background:#1a1a2a">ð</div> ××× ×××× ×<span class="sidebar-count" id="src-noorigin-count">â</span></button> 2692 <button class="sidebar-item" onclick="setAdminView(this,'coupon-preview')"><div class="sidebar-icon" style="background:#0a1a0a">ð«</div> תצ××ת ×§×פ×× ××</button> 2693 <button class="sidebar-item" onclick="setAdminView(this,'debug')"><div class="sidebar-icon" style="background:#0d1a0d">ð</div> Pipeline Debugger</button> 2694 </div> 2695 <div class="sidebar-section"> 2696 <span class="sidebar-label">×ער×ת</span> 2697 <button class="sidebar-item" onclick="setAdminView(this,'users')"><div class="sidebar-icon" style="background:#1a1a2a">ð¥</div> ×שת×ש××</button> 2698 <button class="sidebar-item" onclick="setAdminView(this,'contact-messages');_loadContactMessages()"><div class="sidebar-icon" style="background:#1a1a2a">âï¸</div> ××××¢×ת קשר<span class="sidebar-count" id="contact-badge">â</span></button> 2699 <button class="sidebar-item" onclick="setAdminView(this,'settings')"><div class="sidebar-icon" style="background:#1a1a1a">â</div> ×××ר×ת</button> 2700 <button class="sidebar-item" onclick="setAdminView(this,'logs')"><div class="sidebar-icon" style="background:#1a1a1a">â</div> ×××××</button> 2701 </div> 2702 <div class="sidebar-section">
2703 <span class="sidebar-label">× ×¡××× ×ת</span> 2704 <button class="sidebar-item" onclick="setAdminView(this,'store-pages-demo');_loadStoreDemo()"><div class="sidebar-icon" style="background:#1a2a1a">ðª</div> ××¤× ×× ×××ת<span class="sidebar-count" id="store-demo-badge"></span></button> 2705 <button class="sidebar-item" onclick="setAdminView(this,'home-demo');_hpRender()"><div class="sidebar-icon" style="background:#1a2a2a">ð </div> ×¢××× ××ת</button> 2706 <button class="sidebar-item" onclick="setAdminView(this,'hide-mechanism')"><div class="sidebar-icon" style="background:#1a1a2a">ð¨</div> ×× ×× ×× ×סתר×</button> 2707 <button class="sidebar-item" onclick="setAdminView(this,'expired')"><div class="sidebar-icon" style="background:#2a1a1a">â°</div> ×¤× ×ª××§×£</button> 2708 <button class="sidebar-item" onclick="setAdminView(this,'search-anim')"><div class="sidebar-icon" style="background:#1a1a2a">â³</div> ×× ××צ××ת ××פ×ש</button> 2709 <button class="sidebar-item" onclick="setAdminView(this,'mole')"><div class="sidebar-icon" style="background:#2a1a2a">ð¯</div> ×ש××§××</button> 2710 <button class="sidebar-item" onclick="setAdminView(this,'minigames')"><div class="sidebar-icon" style="background:#1a2a1a">ð¬</div> ×× ××צ××ת ××¢×× ×</button> 2711 </div> 2712 </div> 2713 </div> 2714</div> 2715 2716<div class="toast" id="toast"></div> 2717 2718<!-- ââ Edit Source Modal ââââââââââââââââââââââââââââââââââââââââââââ --> 2719<!-- ââ Edit User Modal ââââââââââââââââââââââââââââââââââââââââââââââââââ --> 2720<div id="edit-user-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:3000;align-items:center;justify-content:center;backdrop-filter:blur(8px)" onclick="if(event.target===this)_closeEditUserModal()"> 2721 <div style="background:var(--gray2);border:1px solid var(--gray3);border-radius:20px;padding:32px;width:360px;max-width:92vw"> 2722 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700;margin-bottom:20px;text-align:right">ער×× ×שת×ש</div> 2723 <input type="hidden" id="eu-id"/> 2724 <div style="margin-bottom:12px;text-align:right"> 2725 <label style="font-size:12px;color:var(--gray5);display:block;margin-bottom:6px">×©× ×שת×ש</label> 2726 <input id="eu-username" type="text" dir="ltr" autocomplete="off" 2727 style="width:100%;padding:12px 14px;background:var(--gray3);border:1px solid var(--gray4);border-radius:10px;color:var(--white);font-size:14px;outline:none;font-family:'DM Sans',sans-serif" 2728 onfocus="this.style.borderColor='var(--accent)'" onblur="this.style.borderColor='var(--gray4)'"/> 2729 </div> 2730 <div style="margin-bottom:12px;text-align:right"> 2731 <label style="font-size:12px;color:var(--gray5);display:block;margin-bottom:6px">תפק××</label> 2732 <select id="eu-role" style="width:100%;padding:12px 14px;background:var(--gray3);border:1px solid var(--gray4);border-radius:10px;color:var(--white);font-size:14px;outline:none;font-family:'DM Sans',sans-serif;cursor:pointer" 2733 onfocus="this.style.borderColor='var(--accent)'" onblur="this.style.borderColor='var(--gray4)'"> 2734 <option value="admin">ð ×××××</option> 2735 <option value="operator">ð§ ××פר××ר</option> 2736 <option value="user">ð¤ ×שת×ש</option> 2737 </select> 2738 </div> 2739 <div style="margin-bottom:16px;text-align:right"> 2740 <label style="font-size:12px;color:var(--gray5);display:block;margin-bottom:6px">ס×ס×× ×××©× <span style="color:var(--gray5);font-weight:400">(×ש×ר ר××§ ××× ×ש××ר ×ת ×× ××××ת)</span></label> 2741 <input id="eu-password" type="password" dir="ltr" autocomplete="new-password" placeholder="â¢â¢â¢â¢â¢â¢â¢â¢" 2742 style="width:100%;padding:12px 14px;background:var(--gray3);border:1px solid var(--gray4);border-radius:10px;color:var(--white);font-size:14px;outline:none;font-family:'DM Sans',sans-serif"
2743 onfocus="this.style.borderColor='var(--accent)'" onblur="this.style.borderColor='var(--gray4)'" 2744 onkeydown="if(event.key==='Enter')_submitEditUser()"/> 2745 </div> 2746 <div id="eu-error" style="display:none;color:#ff6b6b;font-size:13px;margin-bottom:12px;text-align:right"></div> 2747 <div style="display:flex;gap:8px;justify-content:flex-end"> 2748 <button onclick="_closeEditUserModal()" style="padding:10px 18px;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;color:var(--gray6);font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif">×××××</button> 2749 <button onclick="_submitEditUser()" style="padding:10px 18px;background:var(--accent);border:none;border-radius:8px;color:var(--black);font-size:13px;font-weight:700;cursor:pointer;font-family:'DM Sans',sans-serif">ש××ר</button> 2750 </div> 2751 </div> 2752</div> 2753 2754<!-- ââ Login Modal âââââââââââââââââââââââââââââââââââââââââââââââââââââââ --> 2755<div id="login-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.88);z-index:3000;align-items:center;justify-content:center;backdrop-filter:blur(12px)"> 2756 <div style="background:var(--gray2);border:1px solid var(--gray3);border-radius:24px;padding:40px 36px;width:380px;max-width:92vw;text-align:center;box-shadow:0 32px 80px rgba(0,0,0,0.5)"> 2757 <div style="width:52px;height:52px;background:linear-gradient(135deg,rgba(0,232,122,0.15),rgba(0,232,122,0.05));border:1px solid rgba(0,232,122,0.2);border-radius:14px;display:flex;align-items:center;justify-content:center;margin:0 auto 20px;font-size:22px">ð</div> 2758 <div style="font-family:'Syne',sans-serif;font-size:22px;font-weight:800;margin-bottom:4px">×§×פ××<span style="color:var(--accent)">AI</span></div> 2759 <div style="font-size:13px;color:var(--gray5);margin-bottom:28px">×× ××¡× ××××ר ×× ××××</div> 2760 <form id="login-form" onsubmit="submitLogin(event)" autocomplete="on" style="text-align:right"> 2761 <div style="margin-bottom:12px"> 2762 <label style="font-size:12px;color:var(--gray5);display:block;margin-bottom:6px;text-align:right">×©× ×שת×ש</label> 2763 <input id="login-username" type="text" name="username" placeholder="admin" dir="ltr" autocomplete="username" 2764 style="width:100%;padding:13px 16px;background:var(--gray3);border:1px solid var(--gray4);border-radius:10px;color:var(--white);font-size:15px;outline:none;font-family:'DM Sans',sans-serif;text-align:left;transition:border-color 0.2s" 2765 onfocus="this.style.borderColor='var(--accent)'" onblur="this.style.borderColor='var(--gray4)'"/> 2766 </div> 2767 <div style="margin-bottom:16px"> 2768 <label style="font-size:12px;color:var(--gray5);display:block;margin-bottom:6px;text-align:right">ס×ס××</label> 2769 <input id="login-password" type="password" name="password" placeholder="â¢â¢â¢â¢â¢â¢â¢â¢" dir="ltr" autocomplete="current-password" 2770 style="width:100%;padding:13px 16px;background:var(--gray3);border:1px solid var(--gray4);border-radius:10px;color:var(--white);font-size:15px;outline:none;font-family:'DM Sans',sans-serif;text-align:left;transition:border-color 0.2s" 2771 onfocus="this.style.borderColor='var(--accent)'" onblur="this.style.borderColor='var(--gray4)'"/> 2772 </div> 2773 <div id="login-error" style="display:none;background:rgba(255,107,107,0.1);border:1px solid rgba(255,107,107,0.3);border-radius:8px;padding:10px;color:#ff6b6b;font-size:13px;margin-bottom:14px;text-align:center">×©× ×שת×ש ×× ×¡×ס×× ×©×××××</div> 2774 <button type="submit" style="width:100%;padding:14px;background:var(--accent);color:var(--black);border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:'DM Sans',sans-serif;transition:opacity 0.15s" onmouseover="this.style.opacity='0.9'" onmouseout="this.style.opacity='1'">×× ×ס×</button> 2775 </form> 2776 <button onclick="_hideLoginModal()" style="width:100%;padding:10px;background:transparent;color:var(--gray5);border:none;font-size:13px;
2776cursor:pointer;margin-top:10px;font-family:'DM Sans',sans-serif">×××××</button> 2777 </div> 2778</div> 2779 2780<div id="edit-source-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:2000;align-items:center;justify-content:center"> 2781 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:20px;padding:28px 32px;width:min(420px,92vw);direction:rtl;position:relative"> 2782 <button onclick="closeEditSourceModal()" style="position:absolute;top:14px;left:16px;background:var(--gray3);border:none;border-radius:6px;color:var(--gray5);width:28px;height:28px;cursor:pointer;font-size:14px">â</button> 2783 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700;margin-bottom:20px">âï¸ ×¢×¨××ת ××§×ר</div> 2784 <label style="font-size:11px;color:var(--gray5);display:block;margin-bottom:6px">×©× ×ª×¦×××</label> 2785 <input id="es-name-input" placeholder="×©× ×ער××¥" 2786 style="width:100%;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:10px 12px;font-size:13px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;direction:rtl;margin-bottom:14px"/> 2787 <label style="font-size:11px;color:var(--gray5);display:block;margin-bottom:6px">×§×ש×ר</label> 2788 <input id="es-url-input" placeholder="https://t.me/channelname" 2789 oninput="_esUrlChanged(this.value)" 2790 onpaste="setTimeout(()=>_esUrlChanged(this.value),0)" 2791 style="width:100%;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:10px 12px;font-size:13px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;direction:ltr"/> 2792 <div id="es-error" style="color:#fc8181;font-size:12px;margin-top:8px;display:none"></div> 2793 <div style="margin-top:20px;display:flex;gap:10px"> 2794 <button id="es-submit-btn" onclick="submitEditSource()" style="background:var(--accent);color:var(--black);border:none;border-radius:10px;padding:10px 24px;font-size:14px;font-weight:600;cursor:pointer;font-family:'DM Sans',sans-serif">ש××ר</button> 2795 <button onclick="closeEditSourceModal()" style="background:var(--gray3);color:var(--gray6);border:none;border-radius:10px;padding:10px 18px;font-size:14px;cursor:pointer;font-family:'DM Sans',sans-serif">×××××</button> 2796 </div> 2797 </div> 2798</div> 2799 2800<!-- ââ Add Source Modal âââââââââââââââââââââââââââââââââââââââââââââ --> 2801<div id="add-source-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:2000;align-items:center;justify-content:center"> 2802 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:20px;padding:28px 32px;width:min(420px,92vw);direction:rtl;position:relative"> 2803 <button onclick="closeAddSourceModal()" style="position:absolute;top:14px;left:16px;background:var(--gray3);border:none;border-radius:6px;color:var(--gray5);width:28px;height:28px;cursor:pointer;font-size:14px">â</button> 2804 <div id="as-modal-title" style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700;margin-bottom:20px">+ ××סף ער××¥</div> 2805 <label style="font-size:11px;color:var(--gray5);display:block;margin-bottom:6px">×§×ש×ר</label> 2806 <input id="as-url-input" placeholder="t.me/channelname" 2807 onkeydown="if(event.key==='Enter')submitAddSource()" 2808 style="width:100%;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:10px 12px;font-size:13px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;direction:ltr"/> 2809 <div id="as-url-hint" style="font-size:11px;color:var(--gray5);margin-top:6px">××××××: t.me/channelname</div> 2810 <div id="as-error" style="color:#fc8181;font-size:12px;margin-top:8px;display:none"></div> 2811 <div style="margin-top:20px;display:flex;gap:10px"> 2812 <button id="as-submit-btn" onclick="submitAddSource()" style="background:var(--accent);color:var(--black);border:none;border-radius:10px;padding:10px 24px;font-size:14px;font-weight:600;cursor:pointer;font-family:'DM Sans',sans-serif">רש×× ××§×ר</button> 2813 <button onclick="closeAddSourceModal()" style="background:var(--gray3);color:var(--gray6);border:none;border-radius:10px;padding:10px 18px;font-size:14px;cursor:pointer;font-family:'DM Sans',sans-serif">×××××</button> 2814 </div> 2815 </div> 2816</div> 2817 2818<!-- ââ Add Coupon Modal ââââââââââââââââââââââââââââââââââââââââââââââ --> 2819<div id="add-coupon-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:2000;align-items:center;justify-content:center"> 2820 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:20px;padding:28px 32px;width:min(480px,92vw);direction:rtl;position:relative"> 2821 <button onclick="closeAddCouponModal()" style="position:absolute;top:14px;left:16px;background:var(--gray3);border:none;border-radius:6px;color:var(--gray5);width:28px;height:28px;cursor:pointer;font-size:14px">â</button> 2822 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700;margin-bottom:20px">+ ××סף ×§×פ×× ××× ×ת</div> 2823 <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px"> 2824 <div> 2825 <label style="font-size:11px;color:var(--gray5);display:block;margin-bottom:4px">×× ×ת *</label> 2826 <input id="ac-store" placeholder="××××××: Castro" style="width:100%;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;direction:rtl"/> 2827 </div> 2828 <div> 2829 <label style="font-size:11px;color:var(--gray5);display:block;margin-bottom:4px">
2829×§×× (×× ×ש×ר ר××§ ×× ×××)</label> 2830 <input id="ac-code" placeholder="SAVE20 / NO_CODE" style="width:100%;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;direction:ltr;text-transform:uppercase"/> 2831 </div> 2832 <div> 2833 <label style="font-size:11px;color:var(--gray5);display:block;margin-bottom:4px">×× ×× *</label> 2834 <input id="ac-discount" placeholder="20% ×× ×× / ×ש××× ××× ×" style="width:100%;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;direction:rtl"/> 2835 </div> 2836 <div> 2837 <label style="font-size:11px;color:var(--gray5);display:block;margin-bottom:4px">×§×××ר××</label> 2838 <select id="ac-cat" style="width:100%;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;cursor:pointer"> 2839 <option value="Fashion">×××¤× ×</option><option value="Tech">××× ××××××</option> 2840 <option value="Food">××××</option><option value="Travel">× ×¡××¢×ת</option> 2841 <option value="Vacation">××פש×</option><option value="Home">××ת</option> 2842 <option value="Beauty">××פ×</option><option value="Health">×ר×××ת</option> 2843 <option value="Kids">×××××</option><option value="Sports">ספ×ר×</option> 2844 <option value="Other">××ר</option> 2845 </select> 2846 </div> 2847 <div> 2848 <label style="font-size:11px;color:var(--gray5);display:block;margin-bottom:4px">ת××§×£ (××פצ××× ××)</label> 2849 <input id="ac-expiry" type="date" style="width:100%;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif"/> 2850 </div> 2851 <div> 2852 <label style="font-size:11px;color:var(--gray5);display:block;margin-bottom:4px">××× ×××× ×××× ×</label> 2853 <input id="ac-min" placeholder="âª200 ××× ××××" style="width:100%;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;direction:rtl"/> 2854 </div> 2855 <div style="grid-column:1/-1"> 2856 <label style="font-size:11px;color:var(--gray5);display:block;margin-bottom:4px">×ער×ת</label> 2857 <input id="ac-notes" placeholder="×ª× ×××, ×××××ת..." style="width:100%;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--white);outline:none;font-family:'DM Sans',sans-serif;direction:rtl"/> 2858 </div> 2859 </div> 2860 <div style="margin-top:20px;display:flex;gap:10px;justify-content:flex-start"> 2861 <button onclick="submitAddCoupon(this)" style="background:var(--accent);color:var(--black);border:none;border-radius:10px;padding:10px 24px;font-size:14px;font-weight:600;cursor:pointer;font-family:'DM Sans',sans-serif">ש××ר ×§×פ××</button> 2862 <button onclick="closeAddCouponModal()" style="background:var(--gray3);color:var(--gray6);border:none;border-radius:10px;padding:10px 18px;font-size:14px;cursor:pointer;font-family:'DM Sans',sans-serif">×××××</button> 2863 </div> 2864 <div id="ac-error" style="color:#fc8181;font-size:12px;margin-top:10px;display:none"></div> 2865 </div> 2866</div> 2867 2868<footer style="background:var(--gray1);border-top:1px solid var(--gray3);padding:40px 32px;text-align:center;direction:rtl"> 2869 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:800;margin-bottom:8px">×§×פ××<span style="color:var(--accent)">AI</span></div> 2870 <div style="font-size:13px;color:var(--gray5);margin-bottom:24px">×××¤×¢× ×¢× ××× AI â ס×רק ×§×פ×× ×× ××× ×רשת 24/7</div> 2871 <div style="display:flex;justify-content:center;gap:20px;flex-wrap:wrap;font-size:12px;margin-bottom:16px"> 2872 <a href="/" style="color:var(--gray6);text-decoration:none" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--gray6)'">ר×ש×</a> 2873 <span style="color:var(--gray4)">·</span> 2874 <a href="/about" style="color:var(--gray6);text-decoration:none" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--gray6)'">××××ת</a> 2875 <span style="color:var(--gray4)">·</span> 2876 <a href="/extension" style="color:var(--gray6);text-decoration:none" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--gray6)'">ת×סף Chrome</a> 2877 <span style="color:var(--gray4)">·</span> 2878 <a href="/contact" style="color:var(--gray6);text-decoration:none" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--gray6)'">צ×ר קשר</a> 2879 <span style="color:var(--gray4)">·</span> 2880 <a href="/terms" style="color:var(--gray6);text-decoration:none" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--gray6)'">×ª× ×× ×©×××ש</a> 2881 <span style="color:var(--gray4)">·</span> 2882 <a href="/accessibility" style="color:var(--gray6);text-decoration:none" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--gray6)'">×צ×רת × ××ש×ת</a> 2883 <span style="color:var(--gray4)">·</span> 2884 <a href="/sitemap.xml" style="color:var(--gray6);text-decoration:none" onmouseover="this.style.color='var(--accent)'" onmouseout="this.style.color='var(--gray6)'">×פת ××תר</a> 2885 </div> 2886 <div style="font-size:11px;color:var(--gray5)">© 2025 ×§×פ××AI · ×¢××¦× ×¢× â¤ï¸ ××שר××</div> 2887</footer> 2888<!-- Honeypot: invisible to humans, bots follow it and get logged/banned --> 2889<a href="/api/honeypot" style="display:none;visibility:hidden;opacity:0;position:absolute;
2889left:-9999px" aria-hidden="true" tabindex="-1">sitemap-data</a>
vendor: 110 bytes, lines 2889-2891
2889 2890 2891<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script>
2891<script> 2892const _ADMIN_KEY = 'couponai_admin_auth'; // must be defined before any top-level code calls _isAdminAuthed() 2893const COUPONS = []; 2894let ALL_COUPONS = []; // full set for discount sorting 2895let pubCat = '', pubSearch = '', inSearch = false; 2896 2897// Store name -> URL slug. Must match the backend's stores.slug generation 2898// (lowercase, strip space/&/./'/_/- with no separator) so links always hit 2899// the real slug-based /store/{slug} route instead of 404ing. 2900function _storeSlug(name) { return (name || '').toLowerCase().replace(/[ &.'_-]/g, ''); } 2901 2902const COPY_ICON = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>`; 2903const THUMB_UP = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9V5a3 3 0 00-3-3l-4 9v11h11.28a2 2 0 002-1.7l1.38-9a2 2 0 00-2-2.3H14z"/><path d="M7 22H4a2 2 0 01-2-2v-7a2 2 0 012-2h3"/></svg>`; 2904const THUMB_DOWN = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 15v4a3 3 0 003 3l4-9V2H5.72a2 2 0 00-2 1.7l-1.38 9a2 2 0 002 2.3H10z"/><path d="M17 2h2.67A2.31 2.31 0 0122 4v7a2.31 2.31 0 01-2.33 2H17"/></svg>`; 2905 2906// Store name â favicon domain mapping (Hebrew + English) 2907// STORE_DOMAINS is populated at runtime from /api/store-urls (DB-driven, admin-managed). 2908// Do NOT add hardcoded entries here â all storeâdomain mappings must come from the stores table. 2909const STORE_DOMAINS = {}; 2910// Store name â hosted logo URL (Supabase Storage). Populated from /api/store-logos. 2911const STORE_LOGOS = {}; 2912// Store canonical name â Hebrew display name. Populated from /api/store-he. 2913const STORE_HE = {}; 2914// Store canonical name â active coupon count. Populated from /api/store-counts. 2915const STORE_COUNTS = {}; 2916 2917// Title-case a store name: "keds israel" â "Keds Israel" 2918function _titleCase(s) { 2919 return (s || '').replace(/\b\w/g, c => c.toUpperCase()); 2920} 2921 2922// Card display: Hebrew for Israeli domains (.co.il / .il), English title-case otherwise 2923function _storeDisplayCard(store) { 2924 const key = (store || '').toLowerCase(); 2925 const domain = STORE_DOMAINS[key] || ''; 2926 const isIsraeli = domain.endsWith('.co.il') || domain.endsWith('.il'); 2927 if (isIsraeli) return STORE_HE[key] || _titleCase(store); 2928 return _titleCase(store); 2929} 2930 2931// Modal title: "×¢×ר×ת | English" if Hebrew name exists, else just English title-case 2932function _storeDisplayModal(store) { 2933 const key = (store || '').toLowerCase(); 2934 const he = STORE_HE[key] || ''; 2935 const en = _titleCase(store); 2936 return he ? `${he} | ${en}` : en; 2937} 2938 2939// Resolves once loadCoupons() has finished populating STORE_DOMAINS + STORE_LOGOS. 2940let _storeDataReadyResolve; 2941const _storeDataReady = new Promise(r => { _storeDataReadyResolve = r; }); 2942// Legacy placeholder kept so the fetch below can reference it â not used for lookup. 2943const _STORE_DOMAINS_LEGACY_PLACEHOLDER = { 2944 // ââ International ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2945 'iherb':'iherb.com','×××ר×':'iherb.com','×× ×ר×':'iherb.com', 2946 'shein':'shein.com','ש×××':'shein.com', 2947 'zara':'zara.com','××ר×':'zara.com', 2948 'h&m':'hm.com','hm':'hm.com', 2949 'nike':'nike.com','× ×××§':'nike.com','× ×××§×':'nike.com', 2950 'adidas':'adidas.com','××××ס':'adidas.com', 2951 'amazon':'amazon.com','×××××':'amazon.com', 2952 'aliexpress':'aliexpress.com','ali express':'aliexpress.com','×¢×× ×קספרס':'aliexpress.com', 2953 'pandabuy':'pandabuy.com', 2954 'ebay':'ebay.com','×××××':'ebay.com', 2955 'asos':'asos.com', 2956 'mango':'mango.com','×× ××':'mango.com', 2957 'bershka':'bershka.com','×רשק×':'bershka.com', 2958 'pull&bear':'pullandbear.com', 2959 'stradivarius':'stradivarius.com', 2960 'dell':'dell.com','hp':'hp.com','lenovo':'lenovo.com', 2961 'apple':'apple.com','×פ×':'apple.com', 2962 'samsung':'samsung.com','ס×ס×× ×':'samsung.com', 2963 'booking':'booking.com','×××§×× ×':'booking.com', 2964 'airbnb':'airbnb.com','××ר×× ×':'airbnb.com', 2965 'skyscanner':'skyscanner.com','agoda':'agoda.com', 2966 'nespresso':'nespresso.com', 2967 'wix':'wix.com', 2968 // ââ Israeli fashion ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2969 'castro':'castro.com','קס×ר×':'castro.com','castro fashion':'castro.com', 2970 'isracard':'isracard.co.il','×שר××ר×':'isracard.co.il','israelicard':'isracard.co.il','israelicard (×שר××ר×)':'isracard.co.il','isracart':'isracard.co.il', 2971 'terminalx':'terminalx.com','terminal x':'terminalx.com','×ר××× × x':'terminalx.com','×ר××× × ××קס':'terminalx.com','×ר××× ×':'terminalx.com', 2972 'golf':'golf-il.co.il','××××£':'golf-il.co.il','××××£ ×× × ×§×':'golf-il.co.il','golf & co':'golf-il.co.il', 2973 'renuar':'renuar.co.il','×¨× ××ר':'renuar.co.il', 2974 'adika':'adikastyle.com','××××§×':'adikastyle.com','adikastyle':'adikastyle.com','××××§× ×¡××××':'adikastyle.com', 2975 'fox racing':'foxracing.com','fox racing israel':'foxracing.com', 2976 'fox':'fox.co.il','פ×קס':'fox.co.il','פ×קס ×××':'fox.co.il','fox home':'foxhome.co.il', 2977 'delta':'delta.co.il','××ת×':'delta.co.il', 2978 'kitan':'kitan.co.il','××ת×':'kitan.co.il', 2979 'hoodies':'hoodies.co.il', 2980 'factory54':'factory54.co.il','factory 54':'factory54.co.il','פק×××¨× 54':'factory54.co.il', 2981 'like':'like.co.il','××××§':'like.co.il', 2982 'terminal x':'terminalx.com', 2983 // ââ Israeli electronics ââââââââââââââââââââââââââââââââââââââââââââââââââââ 2984 'ksp':'ksp.co.il','קספ':'ksp.co.il','×§× ×ס פ×':'ksp.co.il', 2985 'bug':'bug.co.il','×××':'bug.co.il', 2986 'ivory':'ivory.co.il','×××××ר×':'ivory.co.il', 2987 'zap':'zap.co.il','××פ':'zap.co.il', 2988 'machsaneyhasmal':'payngo.co.il','×××¡× × ×ש××':'payngo.co.il','×××¡× ×':'payngo.co.il','payngo':'payngo.co.il','פ××××':'payngo.co.il', 2989 // ââ Israeli food & pharma ââââââââââââââââââââââââââââââââââââââââââââââââââ 2990 'shufersal':'shufersal.co.il','ש×פרס×':'shufersal.co.il', 2991 'rami levy':'rami-levy.co.il','ר×× ×××':'rami-levy.co.il', 2992 'wolt':'wolt.com','××××':'wolt.com', 2993 '10bis':'10bis.co.il','×ª× ××ס':'10bis.co.il','ten bis':'10bis.co.il', 2994 'super-pharm':'super-pharm.co.il','super pharm':'super-pharm.co.il','ס×פר פ×ר×':'super-pharm.co.il','superpharm':'super-pharm.co.il', 2995 'newpharm':'newpharm.co.il','× ×× ×¤×ר×':'newpharm.co.il', 2996 // ââ Israeli home & retail ââââââââââââââââââââââââââââââââââââââââââââââââââ 2997 'ace':'ace.co.il','×××ס':'ace.co.il', 2998 'ikea':'ikea.com','×××§××':'ikea.com', 2999 'homecenter':'homecenter.co.il','××× ×¡× ×ר':'homecenter.co.il','home center':'homecenter.co.il', 3000 'hamashbir':'hamashbir.co.il','××ש××ר':'hamashbir.co.il','××ש××ר ×צר××':'hamashbir.co.il', 3001 'buy2':'buy2.co.il', 3002 'next':'next.co.il','× ×§×¡×':'next.co.il', 3003 // ââ Israeli travel & hotels ââââââââââââââââââââââââââââââââââââââââââââââââ 3004 'fattal':'fattal.co.il','פת××':'fattal.co.il','×××× ×ת פת××':'fattal.co.il', 3005 'dan hotels':'danhotels.com','×××× ××':'danhotels.com', 3006 'isrotel':'isrotel.co.il','×שר×××':'isrotel.co.il', 3007 'eldan':'eldan.co.il','××××':'eldan.co.il', 3008 'hertz':'hertz.com', 3009 // ââ Israeli books & culture ââââââââââââââââââââââââââââââââââââââââââââââââ 3010 'steimatzky':'steimatzky.co.il','ס×××צק×':'steimatzky.co.il','ס××××¦×§× ×¡×¤×¨××':'steimatzky.co.il', 3011 'tzomet sfarim':'tzomet-sfarim.co.il','צ××ת ספר××':'tzomet-sfarim.co.il', 3012 // ââ Books & entertainment ââââââââââââââââââââââââââââââââââââââââââââââââââ
3013 'olamhakolnoa':'olamhakolnoa.co.il','×¢××× ××§××× ××¢':'yes.co.il','yes':'yes.co.il', 3014 // ââ International deals ââââââââââââââââââââââââââââââââââââââââââââââââââââ 3015 "brad's deals":'bradsdeals.com','bradsdeals':'bradsdeals.com','×ר×ס ×××ס':'bradsdeals.com', 3016 // ââ Fashion & lifestyle ââââââââââââââââââââââââââââââââââââââââââââââââââââ 3017 'nautica':'nautica.com','× ×××××§×':'nautica.com', 3018 'modibodi':'modibodi.com','××××××××':'modibodi.com', 3019 'soho':'soho-fashion.co.il','ס×××':'soho-fashion.co.il', 3020 'naaman':'naaman.co.il','× ×¢××':'naaman.co.il', 3021 'soltam':'soltam.co.il','ס××ת×':'soltam.co.il', 3022 'ivrit':'ivrit.co.il','×¢×ר×ת':'ivrit.co.il','×¢×ר×ת ס××××':'ivrit.co.il', 3023 'timberland':'timberland.com','××××ר×× ×':'timberland.com', 3024 'walgreens':'walgreens.com','××××ר×× ×¡':'walgreens.com', 3025 'uni k wax':'unikwax.com','unikwax':'unikwax.com','××× × ×§×× ×קס':'unikwax.com', 3026 'tolmens':'tolmens.co.il','××××× ×¡':'tolmens.co.il','××××× ×¡ ×××':'tolmens.co.il', 3027 'tyrolar':'tyrolar.co.il','××ר××ר':'tyrolar.co.il', 3028 'mania jeans':'maniajeans.co.il','××× ×× ×\'×× ×¡':'maniajeans.co.il','××× ××':'maniajeans.co.il','mania':'maniajeans.co.il', 3029 'keter':'keter.com','×תר':'keter.com','×תר ספר××':'keter.com', 3030 'yaakovi':'yaakovi.co.il','××¢×§××':'yaakovi.co.il', 3031 // ââ Israeli textiles & other âââââââââââââââââââââââââââââââââââââââââââââââ 3032 'arad textile':'aradtextile.com','×¢×¨× ×קס×××':'aradtextile.com','ער×':'aradtextile.com', 3033 'arad textile / ×¢×¨× ×קס×××':'aradtextile.com','arad textile | ×¢×¨× ×קס×××':'aradtextile.com', 3034 'arad textile ×¢×¨× ×קס×××':'aradtextile.com', 3035 'shilav':'shilav.co.il','ש×××':'shilav.co.il','ש××× (shilav)':'shilav.co.il', 3036 'vardinon':'vardinon.co.il','×ר××× ××':'vardinon.co.il', 3037 'terminal':'terminalx.com','terminal x':'terminalx.com','terminal x | ×ר××× × ××קס':'terminalx.com', 3038 // ââ Israeli more ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3039 'zer4u':'zer4u.co.il','×ר פ×ר ××':'zer4u.co.il','×ר פ×ר ×× (zer4u)':'zer4u.co.il','zaga':'zaga.co.il','×××':'zaga.co.il', 3040 'zaga ×××':'zaga.co.il','zer4u ×ר פ×ר ××':'zer4u.co.il', 3041 'optikana':'optikana.co.il','××פ×××§× ×':'optikana.co.il', 3042 'urban':'urban.co.il','××ר××':'urban.co.il', 3043 'intima':'intima.co.il','××× ××××':'intima.co.il', 3044 'issta':'issta.co.il','××סת×':'issta.co.il', 3045 'alufirst':'alufirst.co.il','×××פ×רס×':'alufirst.co.il', 3046 'elm':'elm.co.il','×××':'elm.co.il','×××':'elm.co.il', 3047 'eshet':'eshet-tours.co.il','×שת ××רס':'eshet-tours.co.il', 3048 'beitely':'beitely.co.il','××ת×××':'beitely.co.il', 3049 'discount bank':'discount.co.il','×× ×§ ××סק×× ×':'discount.co.il', 3050 'hakuk':'hakuk.co.il','××§×××§ ×ת×× ××§':'hakuk.co.il', 3051 'heni':'heni.co.il','×× ×':'heni.co.il', 3052 'hashmalneto':'hashmalneto.co.il','×ש×× × ××':'hashmalneto.co.il', 3053 '247':'twentyfourseven.co.il','×××× ×× ×¤×ר ס××':'twentyfourseven.co.il','twenty4seven':'twentyfourseven.co.il','twentyfourseven':'twentyfourseven.co.il', 3054 'pandazzz':'pandazzz.co.il','×¤× ×× (pandazzz)':'pandazzz.co.il','pandazzz (×¤× ××)':'pandazzz.co.il', 3055 'shamhi':'shamhi.co.il','ש××× ××× ××':'shamhi.co.il', 3056 'tami4':'tami4.co.il','ת××4':'tami4.co.il','ת××4 | tami4':'tami4.co.il', 3057 'lastprice':'lastprice.co.il','lastprice | ××ס×פר××ס':'lastprice.co.il', 3058 'house-design':'house-design.co.il','×××ס ××××××':'house-design.co.il', 3059 'house-design | ×××ס ××××××':'house-design.co.il', 3060 'lovi':'lovi.co.il','lovi ××××':'lovi.co.il', 3061 'steve madden israel':'stevemadden.co.il', 3062 'bmanuka':'bmanuka.co.il','bmanuka / ×××× ××§×':'bmanuka.co.il', 3063 'art depot':'artdepot.co.il','art depot / ××¨× ××פ×':'artdepot.co.il', 3064 'traveling in israel':'traveling-in-israel.com',
3065 'tel aviv museum of art':'tamuseum.org.il', 3066 // ââ International fashion âââââââââââââââââââââââââââââââââââââââââââââââââ 3067 'shein':'shein.com','ש×××':'shein.com', 3068 'nike':'nike.com','× ×××§':'nike.com', 3069 'adidas':'adidas.com','××××ס':'adidas.com', 3070 'adidas israel online | ××××ס ×שר×× ××× ××××':'adidas.co.il', 3071 'adidas israel online':'adidas.co.il', 3072 'puma':'puma.com','פ×××':'puma.com', 3073 'under armour':'underarmour.com','×× ×ר ×ר××ר':'underarmour.com', 3074 'american eagle':'ae.com', 3075 'banana republic':'bananarepublic.com', 3076 'old navy':'oldnavy.com', 3077 'gap':'gap.com', 3078 'express':'express.com', 3079 'pacsun':'pacsun.com', 3080 'lane bryant':'lanebryant.com', 3081 'maurices':'maurices.com', 3082 'urban outfitters':'urbanoutfitters.com', 3083 'fashion nova':'fashionnova.com', 3084 'asos':'asos.com', 3085 'browns fashion':'brownsfashion.com', 3086 'slamjam':'slamjam.com', 3087 'ganni':'ganni.com', 3088 'cettire':'cettire.com', 3089 'giglio':'giglio.com', 3090 'gilt':'gilt.com', 3091 'dolce gabbana':'dolcegabbana.com', 3092 'armani':'armani.com', 3093 'michael kors':'michaelkors.com', 3094 'jimmy choo':'jimmychoo.com','jimmychoo':'jimmychoo.com', 3095 'ugg':'ugg.com','ugg®':'ugg.com', 3096 'crocs':'crocs.com', 3097 'clarks':'clarks.com', 3098 'steve madden':'stevemadden.com', 3099 'charles tyrwhitt':'charlestyrwhitt.com', 3100 'tarte':'tartecosmetics.com', 3101 'quince':'onequince.com', 3102 'yellow the label':'yellowthelabel.com', 3103 'bylt basics':'byltbasics.com', 3104 'drop dead clothing':'dropdead.co.uk', 3105 'general pants co.':'generalpants.com.au', 3106 'rack room shoes':'rackroomshoes.com', 3107 'journeys':'journeys.com', 3108 'mam originals':'mam.com','mam':'mam.com','mam®':'mam.com', 3109 'zaful':'zaful.com','zaful - ××פ××':'zaful.com', 3110 // ââ International tech âââââââââââââââââââââââââââââââââââââââââââââââââââââ 3111 'samsung':'samsung.com', 3112 'hp':'hp.com', 3113 'huion':'huion.com', 3114 'easeus software':'easeus.com', 3115 'hubbleconnected':'hubbleconnected.com', 3116 'red magic':'redmagic.gg','red magic eu store':'redmagic.gg','red magic france':'fr.redmagic.gg', 3117 'red magic germany':'de.redmagic.gg','red magic italy':'it.redmagic.gg','red magic spain':'es.redmagic.gg', 3118 'red magic global':'redmagic.gg', 3119 'trc electronics':'trcelectronics.com', 3120 'airthings':'airthings.com', 3121 'explore scientific':'explorescientific.com', 3122 'spelab auto parts':'spelab.com', 3123 'xpel':'xpel.com', 3124 'geekbuying':'geekbuying.com','geekbuying | ×××§×××× ×':'geekbuying.com', 3125 'tomtop':'tomtop.com','tomtop | ××× ××פ':'tomtop.com', 3126 'banggood':'banggood.com','banggood | ×× ×××':'banggood.com', 3127 'dhgate':'dhgate.com', 3128 'ridge':'ridge.com', 3129 'vitruvian':'vitruvianform.com', 3130 'sharkninja':'sharkninja.com', 3131 'thermoworks':'thermoworks.com', 3132 'topmediaforyou':'topmediaforyou.com','topmediaii':'topmediaii.com','topmediaii (topmediaii)':'topmediaii.com', 3133 'pngtree':'pngtree.com', 3134 // ââ Travel & hotels ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3135 'airbnb':'airbnb.com', 3136 'booking.com':'booking.com', 3137 'hotels.com':'hotels.com', 3138 'expedia':'expedia.com','expedia us':'expedia.com', 3139 'agoda':'agoda.com', 3140 'priceline':'priceline.com', 3141 'orbitz':'orbitz.com', 3142 'onetravel':'onetravel.com', 3143 'lastminute':'lastminute.com', 3144 'viator':'viator.com', 3145 'ihg hotels & resorts':'ihg.com','ihg':'ihg.com', 3146 'marriott':'marriott.com', 3147 'millennium hotels':'millenniumhotels.com', 3148 'motel 6':'motel6.com', 3149 'great value vacations':'greatvaluevacations.com', 3150 'cheapcaribbean.com':'cheapcaribbean.com', 3151 'atlantis the palm':'atlantisthepalm.com', 3152 'air india':'airindia.com', 3153 'cleartrip':'cleartrip.com', 3154 'rentickle':'rentickle.com', 3155 // ââ Beauty & health ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3156 'sephora':'sephora.com', 3157 'ulta':'ulta.com', 3158 'laseraway':'laseraway.com', 3159 'plexaderm':'plexaderm.com', 3160 'forest essentials':'forestessentialsindia.com','forestessentials':'forestessentialsindia.com', 3161 'nykaa fashion':'nykaafashion.com', 3162 'noom':'noom.com', 3163 'betterhelp':'betterhelp.com', 3164 'feelgood contacts':'feelgoodcontacts.com', 3165 'lauricidin':'lauricidin.com', 3166 'lumimeds':'lumimeds.com', 3167 'peptide tech':'peptidetech.com', 3168 'solar buddies':'solarbuddies.com', 3169 'memorypur':'memorypur.com', 3170 'palmonas':'palmonas.com', 3171 'crushed tonic':'crushedtonic.com', 3172 'leaf and flower':'leafandflower.com', 3173 'priv':'priv.com', 3174 'times prime':'timesprime.com', 3175 'patpat':'patpat.com', 3176 'rebound fitness':'reboundfitness.com', 3177 'youfit':'youfit.com', 3178 // ââ Home & garden âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3179 'pottery barn':'potterybarn.com', 3180 'wayfair':'wayfair.com', 3181 'the home depot':'homedepot.com', 3182 'lowes':'lowes.com',"lowe's":'lowes.com', 3183 'overstock':'overstock.com', 3184 'hobby lobby':'hobbylobby.com',
3185 'staples':'staples.com', 3186 'lights4fun':'lights4fun.co.uk', 3187 'myphotobook':'myphotobook.co.uk', 3188 'sproos':'sproos.com', 3189 'gardyn':'mygardyn.com', 3190 'donut media':'donut.media', 3191 // ââ Food & grocery âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3192 'doordash':'doordash.com', 3193 'instacart':'instacart.com', 3194 'sunbasket':'sunbasket.com', 3195 'vitacost':'vitacost.com', 3196 'iherb':'iherb.com', 3197 'petco':'petco.com', 3198 'costco':'costco.com', 3199 // ââ Services & subscriptions âââââââââââââââââââââââââââââââââââââââââââââââ 3200 'amazon':'amazon.com', 3201 'ebay':'ebay.com', 3202 'walmart':'walmart.com', 3203 'target':'target.com','target.com':'target.com', 3204 'tjmaxx':'tjmaxx.com','t.j.maxx':'tjmaxx.com', 3205 'macys':'macys.com',"macy's":'macys.com', 3206 "bloomingdale's":'bloomingdales.com', 3207 'nordstrom rack':'nordstromrack.com', 3208 'kohls':'kohls.com',"kohl's":'kohls.com', 3209 'jcpenney':'jcpenney.com', 3210 'belk':'belk.com', 3211 'rei':'rei.com', 3212 "cabela's":'cabelas.com', 3213 "carter's":'carters.com', 3214 'temu':'temu.com','temu (temu)':'temu.com', 3215 'aliexpress':'aliexpress.com', 3216 'etsy':'etsy.com', 3217 'verizon':'verizon.com', 3218 'at&t':'att.com', 3219 'boost mobile':'boostmobile.com', 3220 'siriusxm':'siriusxm.com', 3221 'mint':'mint.com', 3222 'wall street journal':'wsj.com', 3223 'western union':'westernunion.com', 3224 'godaddy':'godaddy.com', 3225 'namecheap':'namecheap.com', 3226 'bluehost':'bluehost.com', 3227 'hostinger':'hostinger.com', 3228 'bigrock':'bigrock.in', 3229 'expressvpn':'expressvpn.com', 3230 'nordvpn':'nordvpn.com', 3231 'keeper security':'keepersecurity.com', 3232 'duolingo':'duolingo.com', 3233 'busuu':'busuu.com', 3234 'widilo (duolingo cashback)':'widilo.fr','widilo':'widilo.fr', 3235 'shutterfly':'shutterfly.com', 3236 'vistaprint':'vistaprint.com', 3237 'art.com':'art.com', 3238 'ftd':'ftd.com', 3239 'proflowers':'proflowers.com', 3240 '1-800-flowers':'1800flowers.com', 3241 '1-800 contacts':'1800contacts.com', 3242 'cvs':'cvs.com', 3243 'walgreens':'walgreens.com', 3244 'rent the runway':'renttherunway.com', 3245 'vivid seats':'vividseats.com', 3246 'pngtree':'pngtree.com', 3247 'gq box':'gqbox.com', 3248 'military.com':'military.com', 3249 'sabina store':'sabinastore.com','sabina':'sabinastore.com', 3250 'randonautica':'randonautica.com', 3251 'go ape':'goape.co.uk', 3252 'fancy homebody':'fancyhomebody.com', 3253 'ajio':'ajio.com', 3254 'advance auto parts':'advanceautoparts.com', 3255 'tactical gear junkie':'tacticalgearjunkie.com', 3256 'overtime':'overtime.tv', 3257 'djgate':'djgate.com','jd':'jd.com', 3258 'academy':'academy.com', 3259 'tom&co':'tomandco.be', 3260 'sihoo france':'sihoooffice.com', 3261 'gardyn':'mygardyn.com', 3262 'sproos':'sproos.com', 3263 'pandazzz':'pandazzz.co.il', 3264 'mam originals':'mam.com', 3265 'centre pompidou-metz':'centrepompidou-metz.fr', 3266 'forum des images':'forumdesimages.fr', 3267 'the metropolitan museum of art':'metmuseum.org', 3268 'retailmenot':'retailmenot.com', 3269 'kay jewelers':'kay.com', 3270 "d'aniello boutique":'danielloboutique.com', 3271 'luximara':'luximara.com', 3272 'bambini fashion':'bambinifashion.com', 3273 'bhphotovideo':'bhphotovideo.com', 3274 'sbla beauty':'sblabeauty.com', 3275 '7g':'7g.co.il',"7g 7 ×'×":'7g.co.il', 3276 'ksp | ×§×× ×ס פ×':'ksp.co.il','ksp performance':'ksp.co.il','ksp performance | ksp':'ksp.co.il', 3277 'ksp performance ksp':'ksp.co.il', 3278 'adika / ×¢×××§×':'adikastyle.com','adika | ×¢×××§×':'adikastyle.com','adika ×¢×××§×':'adikastyle.com', 3279 'ivory - ×××××ר×':'ivory.co.il','ivory.co.il - ×××××ר×':'ivory.co.il', 3280 'ivoryil':'ivory.co.il','ivory (×××××ר×)':'ivory.co.il', 3281 'soltam | ס××ת×':'soltam.co.il','soltam ס××ת×':'soltam.co.il', 3282 'ס×××ª× (soltam)':'soltam.co.il', 3283 'tollmans dot':"tollmans.co.il","tollman's dot":'tollmans.co.il', 3284 "tollman's dot ××××× 'ס ×××":'tollmans.co.il', 3285 'ace':'ace.co.il','×××ס acr':'ace.co.il','×××ס ace':'ace.co.il', 3286 'lostprice':'lastprice.co.il', 3287 // ââ Remaining fixes âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3288 'jd':'jd.com', 3289 '×××ס':'ace.co.il','ace':'ace.co.il', 3290 '×××':'elm.co.il', 3291 '×× ×§ ××סק×× ×':'discount.co.il','discount bank':'discount.co.il', 3292 '××§×××§ ×ת×× ××§':'hakuk.co.il', 3293 '×× ×':'heni.co.il', 3294 '×××× ×× ×¤×ר ס××':'twentyfourseven.co.il','twenty four seven':'twentyfourseven.co.il', 3295 '××××× ×¡ ×××':'tollmans.co.il',"tollman's dot":'tollmans.co.il',"tollman's dot ××××× 'ס ×××":'tollmans.co.il', 3296 '×שר×××':'isrotel.co.il','isrotel':'isrotel.co.il', 3297 'ס×××':'soho-fashion.co.il','soho fashion':'soho-fashion.co.il', 3298 '×¢×ר×ת':'ivrit.co.il','ivrit':'ivrit.co.il',
3299 '×¢××× ××§××× ××¢':'olamhakolnoa.co.il', 3300 '×ר×××':'carmella.co.il','carmela':'carmella.co.il','carmella':'carmella.co.il', 3301 'צ××ת ספר××':'booknet.co.il','tzomet sfarim':'booknet.co.il','booknet':'booknet.co.il', 3302 'atterley':'atterleyroad.com','atterley road':'atterleyroad.com', 3303 'mandm direct':'mandmdirect.com','mandm':'mandmdirect.com','mand m direct':'mandmdirect.com', 3304}; // _STORE_DOMAINS_LEGACY_PLACEHOLDER end â NOT used at runtime 3305// ââ Auto-merge server-side domain map (non-blocking, additive) ââââââââââââââââ 3306// Fetches /api/store-domains and adds any stores not already in STORE_DOMAINS. 3307// Any store added to the DB is automatically recognized for favicons without 3308// changing this file. 3309fetch('/api/store-domains').then(r => r.json()).then(serverMap => { 3310 for (const [domain, name] of Object.entries(serverMap)) { 3311 const key = name.toLowerCase(); 3312 if (!STORE_DOMAINS[key]) STORE_DOMAINS[key] = domain; 3313 if (!STORE_DOMAINS[name]) STORE_DOMAINS[name] = domain; 3314 } 3315}).catch(() => {/* non-critical â STORE_DOMAINS already has all known stores */}); 3316 3317// ââ Globe fingerprint â detect Google's "no favicon" placeholder ââââââââââââââ 3318let _globeFingerprint = null; 3319function _loadGlobeFingerprint() { 3320 const img = new Image(); 3321 img.crossOrigin = 'anonymous'; 3322 img.onload = () => { 3323 try { 3324 const c = document.createElement('canvas'); 3325 c.width = img.width; c.height = img.height; 3326 c.getContext('2d').drawImage(img, 0, 0); 3327 _globeFingerprint = c.getContext('2d').getImageData(0, 0, img.width, img.height).data; 3328 } catch(e) {} 3329 }; 3330 img.src = 'https://www.google.com/s2/favicons?domain=thisdoesnotexist99999xyz.com&sz=64'; 3331} 3332function _isGlobe(img) { 3333 if (!_globeFingerprint) return false; 3334 try { 3335 const c = document.createElement('canvas'); 3336 c.width = img.naturalWidth; c.height = img.naturalHeight; 3337 c.getContext('2d').drawImage(img, 0, 0); 3338 const data = c.getContext('2d').getImageData(0, 0, img.naturalWidth, img.naturalHeight).data; 3339 if (data.length !== _globeFingerprint.length) return false; 3340 return data.every((v, i) => v === _globeFingerprint[i]); 3341 } catch(e) { return false; } 3342} 3343 3344function _storeInitial(store, size) { 3345 const words = (store || '').trim().split(/\s+/).filter(Boolean); 3346 let letters; 3347 if (words.length >= 2) {
3348 letters = words.slice(0, 2) 3349 .map(w => (w.replace(/[^a-zA-Z×-ת]/g, '')[0] || w[0] || '').toUpperCase()) 3350 .join(''); 3351 } else { 3352 const clean = ((words[0] || '').replace(/[^a-zA-Z×-ת]/g, '') || (store || '??')); 3353 letters = clean.slice(0, 2).toUpperCase(); 3354 } 3355 const colors = ['#00e87a','#63b3ed','#ffe45e','#b794f4','#fc8181','#f6ad55','#68d391','#76e4f7']; 3356 let hash = 0; 3357 for (let i = 0; i < store.length; i++) hash = store.charCodeAt(i) + ((hash << 5) - hash); 3358 const color = colors[Math.abs(hash) % colors.length]; 3359 // size param â fixed px square (modal); no size â 100% fill (cards) 3360 const dim = size ? `width:${size}px;height:${size}px;` : `width:100%;height:100%;`; 3361 return `<div style="${dim}display:flex;align-items:center;justify-content:center;background:${color}22;font-size:13px;font-weight:800;color:${color};font-family:'Syne',system-ui,sans-serif;line-height:1">${letters}</div>`; 3362} 3363 3364function _faviconErr(img, urls, store) { 3365 // urls is a pipe-separated string of remaining fallback URLs 3366 const list = (typeof urls === 'string' ? urls : '').split('|').filter(
3366Boolean); 3367 if (list.length === 0) { 3368 // Favicon chain exhausted â try stored logo before initials 3369 const storedLogo = STORE_LOGOS[(store || '').toLowerCase()]; 3370 if (storedLogo) { 3371 img.onerror = () => { img.style.display = 'none'; img.insertAdjacentHTML('afterend', _storeInitial(store)); img.remove(); }; 3372 img.src = storedLogo; 3373 return; 3374 } 3375 img.style.display = 'none'; 3376 img.insertAdjacentHTML('afterend', _storeInitial(store)); 3377 img.remove(); 3378 return; 3379 } 3380 const next = list[0]; 3381 const rest = list.slice(1).join('|'); 3382 img.onload = null; 3383 img.onerror = function() { _faviconErr(img, rest, store); }; 3384 img.src = next; 3385} 3386 3387function _faviconUrls(domain) { 3388 return [ 3389 `/api/favicon?domain=${encodeURIComponent(domain)}`, 3390 ]; 3391} 3392 3393function storeToFavicon(store, sourceUrl) { 3394 const key = (store || '').toLowerCase().trim(); 3395 let domain = null; 3396 3397 // 1. Exact match in map 3398 if (STORE_DOMAINS[key]) domain = STORE_DOMAINS[key]; 3399 3400 // 2. No partial match â exact only (partial causes "golf" to hit "golf kids") 3401 3402 // 3. Extract domain from coupon source URL (skip coupon aggregator domains) 3403 if (!domain && sourceUrl) { 3404 try { 3405 const raw = sourceUrl.replace(/^\[.*?\]\|/, '').replace(/^Web:\s*/, '').trim(); 3406 const h = new URL(raw).hostname.replace(/^www\./, ''); 3407 const skip = ['secretcoupon','israelicoupons','promocode','couponet','couponfollow', 3408 'israelcoupon','couponwcode','bradsdeals','slickdeals','retailmenot']; 3409 if (h && !skip.some(s => h.includes(s))) domain = h; 3410 } catch(e) {} 3411 } 3412 3413 // 4. No domain known â return null (do not guess) 3414 return domain ? _faviconUrls(domain) : null; 3415} 3416 3417// Returns the target store homepage URL from the store name only. 3418// Source URL is intentionally ignored â it's where we found the deal, not where to buy. 3419function _storeHomeUrl(store) { 3420 const key = (store || '').toLowerCase().trim(); 3421 3422 // 1. Exact match in STORE_DOMAINS 3423 if (STORE_DOMAINS[key]) { 3424 const d = STORE_DOMAINS[key]; 3425 return `https://${d.startsWith('www.') ? '' : 'www.'}${d}/`; 3426 } 3427 3428 // No partial match â "golf" must not resolve via "golf kids" or any other store 3429 return ''; // no verified URL in DB for this store â do not guess 3430} 3431 3432function formatDiscount(raw) { 3433 if (!raw) return '×× ××'; 3434 const d = raw.trim(); 3435 // Already has a Hebrew/English discount descriptor â show as-is 3436 if (/×× ××|××× ×|shipping|discount|off\b/i.test(d)) return d; 3437 // Percentage or shekel amount â append ×× ×× 3438 if (d.includes('%') || d.includes('âª')) return d + ' ×× ××'; 3439 return d; 3440} 3441 3442function parseDiscount(discount) { 3443 if (!discount) return 0; 3444 const s = discount.toString(); 3445 const pct = s.match(/(\d+(?:\.\d+)?)\s*%/); 3446 if (pct) return parseFloat(pct[1]); 3447 const shek = s.match(/âª\s*(\d+)|(\d+)\s*âª/); 3448 if (shek) return parseFloat(shek[1] || shek[2]) * 0.15; 3449 const heShek = s.match(/(\d+)\s*(?:שק×|ש×|ש"×)/); 3450 if (heShek) return parseFloat(heShek[1]) * 0.15; 3451 return 0; 3452} 3453 3454function getVoteState(id) { return localStorage.getItem(`vote_${id}`) || ''; } 3455 3456// ââ Device ID â one random ID per browser, persists forever âââââââââââââââââ 3457function _getDeviceId() { 3458 let id = localStorage.getItem('_couponai_device_id'); 3459 if (!id) { 3460 id = 'dev_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36); 3461 localStorage.setItem('_couponai_device_id', id); 3462 } 3463 return id; 3464} 3465 3466// ââ Browser fingerprint collector ââââââââââââââââââââââââââââââââââââââââââââ 3467function _collectEnv() { 3468 const nav = navigator; 3469 const conn = nav.connection || nav.mozConnection || nav.webkitConnection || {}; 3470 return { 3471 lang: nav.language || nav.userLanguage || '', 3472 langs: (nav.languages || []).join(','), 3473 tz: Intl.DateTimeFormat().resolvedOptions().timeZone || '', 3474 mem: nav.deviceMemory || null, 3475 cpu: nav.hardwareConcurrency || null, 3476 conn: conn.effectiveType || conn.type || null, 3477 rtt: conn.rtt || null, 3478 platform: nav.platform || '', 3479 vendor: nav.vendor || '', 3480 cookies: nav.cookieEnabled, 3481 online: nav.onLine, 3482 pwa: window.matchMedia('(display-mode: standalone)').matches, 3483 dnt: nav.doNotTrack === '1', 3484 }; 3485} 3486 3487function _collectScreen() { 3488 return { 3489 w: screen.width, 3490 h: screen.height, 3491 vw: window.innerWidth, 3492 vh: window.innerHeight, 3493 dpr: window.devicePixelRatio || 1, 3494 touch: navigator.maxTouchPoints || 0, 3495 depth: screen.colorDepth || null, 3496 orient: (screen.orientation || {}).type || null, 3497 }; 3498} 3499 3500function _collectUTM() { 3501 const p = new URLSearchParams(location.search); 3502 const utm = {}; 3503 for (const k of ['source','medium','campaign','content','term']) { 3504 const v = p.get('utm_' + k); 3505 if (v) utm[k] = v; 3506 } 3507 // absorb click IDs 3508 for (const [key, field] of [['fbclid','fb_click'],['gclid','g_click'],['ttclid','tt_click'],['msclkid','ms_click']]) { 3509 const v = p.get(key); 3510 if (v) utm[field] = v; 3511 } 3512 return Object.keys(utm).length ? utm : null; 3513} 3514 3515async function _canvasFingerprint() { 3516 try { 3517 const c = document.createElement('canvas'); 3518 c.width = 200; c.height = 50; 3519 const ctx = c.getContext('2d'); 3520 ctx.textBaseline = 'top';
3521 ctx.font = '14px Arial'; 3522 ctx.fillStyle = '#f60'; 3523 ctx.fillRect(125, 1, 62, 20); 3524 ctx.fillStyle = '#069'; 3525 ctx.fillText('CouponAIð', 2, 15); 3526 ctx.fillStyle = 'rgba(102,204,0,0.7)'; 3527 ctx.fillText('CouponAIð', 4, 17); 3528 const data = c.toDataURL(); 3529 let hash = 0; 3530 for (let i = 0; i < data.length; i++) hash = ((hash << 5) - hash) + data.charCodeAt(i) | 0; 3531 return hash.toString(36); 3532 } catch(e) { return null; } 3533} 3534 3535async function _webglFingerprint() { 3536 try { 3537 const c = document.createElement('canvas'); 3538 const gl = c.getContext('webgl') || c.getContext('experimental-webgl'); 3539 if (!gl) return null; 3540 const dbg = gl.getExtension('WEBGL_debug_renderer_info'); 3541 return { 3542 renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null, 3543 vendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : null, 3544 }; 3545 } catch(e) { return null; } 3546} 3547 3548let _fpCache = null; 3549async function _collectFingerprint() { 3550 if (_fpCache) return _fpCache; 3551 const [canvas, webgl] = await Promise.all([_canvasFingerprint(), _webglFingerprint()]); 3552 _fpCache = { canvas, webgl }; 3553 return _fpCache; 3554} 3555 3556// Build the full device context payload, cached after first call 3557let _deviceContextCache = null; 3558async function _getDeviceContext() { 3559 if (_deviceContextCache) return _deviceContextCache; 3560 const [fp] = await Promise.all([_collectFingerprint()]); 3561 // Visit counter 3562 const visits = parseInt(localStorage.getItem('_cai_visits') || '0') + 1; 3563 localStorage.setItem('_cai_visits', visits); 3564 const firstVisit = localStorage.getItem('_cai_first_visit') || Date.now().toString(); 3565 localStorage.setItem('_cai_first_visit', firstVisit); 3566 3567 _deviceContextCache = { 3568 ua: navigator.userAgent, 3569 ref: document.referrer || null, 3570 utm: _collectUTM(), 3571 url: location.href, 3572 env: _collectEnv(), 3573 screen: _collectScreen(), 3574 fp, 3575 visit_num: visits, 3576 first_visit: parseInt(firstVisit), 3577 }; 3578 return _deviceContextCache; 3579} 3580 3581// ââ Client-side analytics (localStorage) ââââââââââââââââââââââââââââââââââââ 3582const AK = 'couponai_analytics'; 3583function _aGet() { try { return JSON.parse(localStorage.getItem(AK)||'{}'); } catch(e) { return {}; } } 3584function _aSet(d) { try { localStorage.setItem(AK, JSON.stringify(d)); } catch(e) {} } 3585function _currentUsername() { 3586 return sessionStorage.getItem('couponai_admin_user') || ''; 3587} 3588function _tagWithUser(data) { 3589 // Attach _u (username) to event data when an admin/operator is logged in 3590 const u = _currentUsername(); 3591 if (!u) return data || ''; 3592 if (data && typeof data === 'object') return { ...data, _u: u }; 3593 return { _v: data || '', _u: u }; 3594} 3595function trackEvent(type, data) { 3596 const d = _aGet(); 3597 if (!d.events) d.events = []; 3598 d.events.push({ type, data: _tagWithUser(data), ts: Date.now() }); 3599 if (d.events.length > 5000) d.events = d.events.slice(-5000); 3600 _aSet(d); 3601} 3602function trackSearch(q) { 3603 const d = _aGet(); 3604 if (!d.searches) d.searches = []; 3605 d.searches.push({ q, ts: Date.now(), u: _currentUsername() || undefined }); 3606 if (d.searches.length > 2000) d.searches = d.searches.slice(-2000); 3607 _aSet(d); 3608} 3609// ââ Server-side analytics persistence âââââââââââââââââââââââââââââââââââââââ 3610async function _syncAnalyticsToServer(beacon) { 3611 const d = _aGet(); 3612 if (!d.events && !d.searches && !d.sessions) return; 3613 const ctx = await _getDeviceContext().catch(() => ({})); 3614 const payload = { ...d, did: _getDeviceId(), ctx }; 3615 try { 3616 if (beacon && navigator.sendBeacon) { 3617 // sendBeacon is fire-and-forget, safe on tab close 3618 navigator.sendBeacon('/api/analytics', JSON.stringify(payload)); 3619 } else { 3620 await fetch('/api/analytics', { 3621 method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload) 3622 }); 3623 // Server just acks the insert now â it no longer echoes the full dataset back. 3624 // Local state (events/searches/sessions) is untouched; admin analytics views 3625 // pull the merged cross-device dataset on demand via _loadAnalyticsFromServer(). 3626 } 3627 } catch(e) {} 3628} 3629 3630async function _loadAnalyticsFromServer() { 3631 try { 3632 const resp = await fetch('/api/analytics'); 3633 if (!resp.ok) return; 3634 const server = await resp.json(); 3635 if (!server || (!server.events && !server.searches)) return; 3636 // Merge server data into localStorage 3637 const local = _aGet(); 3638 const merged = {}; 3639 for (const key of ['events','searches','sessions']) { 3640 const s = server[key] || []; 3641 const l = local[key] || []; 3642 const seen = {}; 3643 for (const item of [...l, ...s]) { 3644 // Sessions use {start, end, deviceId} â no ts/type/q, so key by start time 3645 const dk = key === 'sessions' 3646 ? `s-${item.start || item.ts || Math.random()}` 3647 : `${item.ts||''}-${item.type||''}-${item.q||''}`; 3648 seen[dk] = item; 3649 } 3650 merged[key] = Object.values(seen).sort((a,b) =>
3650 ((a.ts||a.start||0)-(b.ts||b.start||0))); 3651 } 3652 _aSet(merged); 3653 } catch(e) {} 3654} 3655 3656// ââ Coupon event sender âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3657async function _sendCouponEvent(type, store, code, couponId) { 3658 try { 3659 const ctx = await _getDeviceContext().catch(() => ({})); 3660 fetch('/api/coupon-event', { 3661 method: 'POST', 3662 headers: {'Content-Type':'application/json'}, 3663 body: JSON.stringify({ type, store: store||'', code: code||'', coupon_id: couponId||null, 3664 did: _getDeviceId(), ua: ctx.ua, ref: ctx.ref }), 3665 keepalive: true, 3666 }); 3667 } catch(e) {} 3668} 3669 3670// ââ Search event sender âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3671async function _sendSearchEvent(q, results, elapsed, mode) { 3672 try { 3673 const ctx = await _getDeviceContext().catch(() => ({})); 3674 fetch('/api/search-event', { 3675 method: 'POST', 3676 headers: {'Content-Type':'application/json'}, 3677 body: JSON.stringify({ q, results, elapsed, mode: mode||'fast', 3678 did: _getDeviceId(), ua: ctx.ua, ref: ctx.ref, utm: ctx.utm }), 3679 keepalive: true, 3680 }); 3681 } catch(e) {} 3682} 3683 3684// ââ JS error tracker âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3685(function() { 3686 function _sendError(msg, src, line, col, stack) { 3687 try { 3688 fetch('/api/js-error', { 3689 method: 'POST', 3690 headers: {'Content-Type':'application/json'}, 3691 body: JSON.stringify({ message: msg, source: src, lineno: line, colno: col, 3692 stack: stack||null, ua: navigator.userAgent, 3693 url: location.href, did: _getDeviceId() }), 3694 keepalive: true, 3695 }); 3696 } catch(e) {} 3697 } 3698 window.addEventListener('error', e => { 3699 _sendError(e.message, e.filename, e.lineno, e.colno, e.error && e.error.stack); 3700 }); 3701 window.addEventListener('unhandledrejection', e => { 3702 const msg = (e.reason && e.reason.message) ? e.reason.message :
3702String(e.reason); 3703 const stack = (e.reason && e.reason.stack) || null; 3704 _sendError('UnhandledRejection: ' + msg, location.href, null, null, stack); 3705 }); 3706})(); 3707 3708// ââ Record session start ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3709(function() { 3710 // Load server analytics then render dashboard panels â admin-only data, so only 3711 // fetch it for authed admin sessions. Public visitors never see these panels, and 3712 // admin views already re-fetch on demand (showPage('admin'), switchAdminView()). 3713 if (_isAdminAuthed()) { 3714 _loadAnalyticsFromServer().then(() => { 3715 try { _renderPopSearchBar(); } catch(e) {} 3716 try { _renderSlowBar(); } catch(e) {} 3717 try { loadAnalytics(); } catch(e) {} 3718 try { loadSpeedAnalytics(); } catch(e) {} 3719 try { loadPopupAnalytics(); } catch(e) {} 3720 }); 3721 } 3722 3723 const d = _aGet(); 3724 if (!d.sessions) d.sessions = []; 3725 d.sessions.push({ start: Date.now(), end: null, deviceId: _getDeviceId() }); 3726 if (d.sessions.length > 2000) d.sessions = d.sessions.slice(-2000); 3727 _aSet(d); 3728 3729 // Save to server on tab close (beacon = no wait for response) 3730 window.addEventListener('beforeunload', () => { 3731 const d2 = _aGet(); 3732 if (d2.sessions && d2.sessions.length) { d2.sessions[d2.sessions.length-1].end = Date.now(); _aSet(d2); } 3733 _syncAnalyticsToServer(true); 3734 }); 3735 3736 // Auto-save every 5 minutes 3737 setInterval(() => _syncAnalyticsToServer(false), 5 * 60 * 1000); 3738 3739 // Save after every search (called from aiSearch) 3740 window._analyticsSearchDone = () => _syncAnalyticsToServer(false); 3741})(); 3742 3743function cardAge(createdAt) { 3744 if (!createdAt) return { label: '', isOld: false }; 3745 const days = Math.floor((Date.now() - new Date(createdAt)) / 86400000); 3746 if (days < 1) return { label: '××××', isOld: false }; 3747 if (days < 7) return { label: `××¤× × ${days} ××××`, isOld: false }; 3748 if (days < 30) return { label: `××¤× × ${Math.floor(days/7)} ש×××¢×ת`, isOld: false }; 3749 if (days < 60) return { label: `××¤× × ×××ש`, isOld: false }; 3750 return { label: `××¤× × ${Math.floor(days/30)} ×××ש××`, isOld: true }; 3751} 3752 3753function couponCard(c) { 3754 const id = c.id || ''; 3755 const faviconUrls = storeToFavicon(c.store, c.source) || []; 3756 const favicon = faviconUrls[0] || null; 3757 // Encode as pipe-separated string â avoids double-quote conflicts in HTML attributes 3758 const faviconFallbacksStr = faviconUrls.slice(1).join('|'); 3759 const discount = formatDiscount(c.discount); 3760 // Detect newsletter / first-purchase deals 3761 const _notesLower = (c.notes || '').toLowerCase(); 3762 const isNewsletter = _notesLower.includes('× ×××××ר') || _notesLower.includes('ר×××©× ×¨×ש×× ×') || _notesLower.includes('newsletter') || _notesLower.includes('first order') || _notesLower.includes('first purchase'); 3763 const newsletterBadge = isNewsletter ? `<div class="cc-newsletter-badge">âï¸ ××§×××ת ××ש×× ×××× â ר×ש×× ×× ×××××ר</div>` : ''; 3764 // Strip the standardized prefix from notes since the badge already shows it 3765 const displayNotes = isNewsletter 3766 ? (c.notes || '').replace(/×× ×× ×ר×××©× ×¨×ש×× ×\s*[ââ-]\s*××רש ר×ש×× ×× ×××××ר\s*/i, '').trim() 3767 : (c.notes || ''); 3768 const notesHtml = displayNotes ? `<div class="cc-notes">${displayNotes}</div>` : ''; 3769 const voteState = id ? getVoteState(id) : ''; 3770 const upDisabled = voteState === 'up' ? 'disabled class="vote-btn vote-up voted"' : 'class="vote-btn vote-up"'; 3771 const downDisabled = voteState === 'down' ? 'disabled class="vote-btn vote-down voted"' : 'class="vote-btn vote-down"'; 3772 const safeCode = (c.code || '').replace(/'/g, "\\'"); 3773 const safeStore = (c.store || '').replace(/'/g, "\\'"); 3774 // Strip discovery-type prefix "[primary]|" / "[secondary]|" / "[auto]|" then "Web: " 3775 const safeSrc = (c.src || '').replace(/^\[(?:primary|secondary|auto)\]\|/,'').replace(/^Web:\s*/i,'').trim().replace(/'/g, "\\'"); 3776 // Real store homepage â always points to the store, never the aggregator source 3777 const _knownHome = _storeHomeUrl(c.store).replace(/'/g, "\\'"); 3778 const storeHome = _knownHome || ''; 3779 // Aggregator source URL â used as fallback link on the ש×××× button 3780 const _aggUrl = (_admCleanSrc(c.src || '').url || '').replace(/'/g, "\\'"); 3781 const expired = isExpired(c); 3782 const age = cardAge(c.createdAt); 3783 // Published date from source site â shown as secondary line below the age badge 3784 const pubLabel = c.publishedAt 3785 ? (() => { 3786 const d = new Date(c.publishedAt); 3787 if (isNaN(d)) return ''; 3788 const fmt = d.toLocaleDateString('he-IL', {day:'2-digit', month:'2-digit', year:'2-digit'}); 3789 return d > new Date() ? `×ת××§×£ ×¢× ${fmt}` : `פ××¨×¡× ${fmt}`; 3790 })() 3791 : ''; 3792 // On expired cards: show age badge in normal gray (not orange) â expired badge already signals it 3793 const ageBadge = (age.label || pubLabel) 3794 ? `<div class="cc-age">${age.label}${age.label && pubLabel ? '<br>' : ''}${pubLabel ? `<span style="opacity:0.7">${pubLabel}</span>` : ''}</div>` 3795 : ''; 3796 const expClass = expired ? 'exp-strike' : ''; 3797 const expPill = expired ? `<span class="exp-badge-orange" style="margin-right:auto;font-size:10px;padding:2px 8px">â° ××ת×× ×©×¤× ×ª××§×£</span>` : ''; 3798 return ` 3799 <div class="coupon-card ${(c.votes || 0) > 10 ? 'featured' : ''} ${expClass}" data-id="${id}" data-created="${c.createdAt||''}" data-pub="${c.publishedAt||''}" data-pub-label="${pubLabel||''}" data-age-label="${age.label||''}" data-notes="${(displayNotes||'').replace(/"/g,'"')}" data-discount-raw="${(c.discount||'').replace(/"/g,'"')}" data-source="${(c.source||'').replace(/"/g,'"')}" data-votes="${c.votes||0}" data-newsletter="${isNewsletter?'1':''}"> 3800 ${ageBadge} 3801 <a class="cc-store-row" href="/store/${_storeSlug(c.store)}" style="text-decoration:none;color:inherit"> 3802 <div class="cc-logo"> 3803 ${favicon 3804 ? `<img src="${favicon}" alt="${safeStore}" onerror="_faviconErr(this,'${faviconFallbacksStr}','${safeStore}')"/>` 3805 : _storeInitial(c.store)} 3806 </div> 3807 <div class="cc-store">${_storeDisplayCard(c.store)}</div> 3808 </a> 3809 ${newsletterBadge} 3810 ${notesHtml} 3811 <div class="cc-discount">${discount}</div> 3812 ${(c.code || '').toUpperCase() === 'NO_CODE' 3813 ? storeHome 3814 ? `<a class="no-code-btn" href="/go?store=${encodeURIComponent(c.store)}&url=${encodeURIComponent(storeHome)}" target="_blank" rel="noopener noreferrer" onclick="trackEvent('show_site','${c.id||''}')" title="××¦× ××צע ××תר">ðï¸ ××¦× ××צע ××תר</a>` 3815 : `` 3816 : `<div class="cc-code-row"> 3817 <button class="cc-copy" id="ccp-${id}" onclick="ccReveal(this.closest('.cc-code-row').querySelector('.cc-code'),'${safeCo
3817de}','${safeStore}','${safeSrc}','${id}','${storeHome}')" title="×עתק ×§××">${COPY_ICON}</button> 3818 <div class="cc-code" onclick="ccReveal(this,'${safeCode}','${safeStore}','${safeSrc}','${id}','${storeHome}')" style="cursor:pointer"> 3819 <div class="cc-code-masked"> 3820 <span class="cc-code-peek"> 3821 <span class="cc-code-dots">â¢â¢â¢â¢â¢â¢</span> 3822 <span class="cc-code-end">${(c.code||'').slice(-2)}</span> 3823 </span> 3824 <span class="cc-code-eye"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span> 3825 </div> 3826 <div class="cc-code-revealed" id="ccr-${id}">${c.code}</div> 3827 </div> 3828 </div>` 3829 } 3830 ${expPill ? `<div class="cc-footer">${expPill}</div>` : ''} 3831 </div>`; 3832} 3833 3834function isExpired(c) { return (c.votes || 0) <= -10; } 3835 3836async function vote(id, dir, btn) { 3837 if (!id || btn.disabled) return; 3838 const key = `vote_${id}`; 3839 const existing = getVoteState(id); 3840 // Same direction â ignore 3841 if ((existing === 'up' && dir > 0) || (existing === 'down' && dir < 0)) return; 3842 3843 const endpoint = dir > 0 ? `/api/coupons/${id}/vote` : `/api/coupons/${id}/downvote`; 3844 const res = await fetch(endpoint, { method: 'POST' }).catch(() => null); 3845 if (!res || !res.ok) return; 3846 3847 localStorage.setItem(key, dir > 0 ? 'up' : 'down'); 3848 3849 const card = btn.closest('.coupon-card'); 3850 const upBtn = card.querySelector('.vote-up'); 3851 const downBtn = card.querySelector('.vote-down'); 3852 const upSpan = upBtn ? upBtn.querySelector('span') : null; 3853 const downSpan = downBtn ? downBtn.querySelector('span') : null; 3854 3855 if (dir > 0) { 3856 // ââ Clicked ð ââââââââââââââââââââââââââââââââââ 3857 trackEvent('thumb_up', id); 3858 // If switching from a downvote, cancel it on the red counter too 3859 if (existing === 'down' && downSpan) 3860 downSpan.textContent = Math.max(0, parseInt(downSpan.textContent || '0') - 1); 3861 // Add to green counter 3862 if (upSpan) upSpan.textContent = parseInt(upSpan.textContent || '0') + 1; 3863 // Lock green, unlock red 3864 upBtn.disabled = true; upBtn.classList.add('voted'); 3865 downBtn.disabled = false; downBtn.classList.remove('voted'); 3866 } else { 3867 // ââ Clicked ð ââââââââââââââââââââââââââââââââââ 3868 trackEvent('thumb_down', id); 3869 // If switching from an upvote, cancel it on the green counter too 3870 if (existing === 'up' && upSpan) 3871 upSpan.textContent = Math.max(0, parseInt(upSpan.textContent || '0') - 1); 3872 // Add to red counter 3873 if (downSpan) downSpan.textContent = parseInt(downSpan.textContent || '0') + 1; 3874 // Lock red, unlock green 3875 downBtn.disabled = true; downBtn.classList.add('voted'); 3876 upBtn.disabled = false; upBtn.classList.remove('voted'); 3877 } 3878} 3879 3880function showSections(searchMode) { 3881 inSearch = searchMode; 3882 // Sections always stay visible â search results appear above them 3883} 3884 3885function clearSearch() { 3886 document.getElementById('pub-search').value = ''; 3887 pubSearch = ''; 3888 const banner = document.getElementById('ai-banner'); 3889 if (banner) { banner.className = 'ai-banner'; banner.innerHTML = ''; banner.style.borderRadius = ''; } 3890 _stopMiniGame(); 3891 const rw = document.getElementById('search-results-wrap'); 3892 if (rw) rw.style.display = 'none'; 3893 const rg = document.getElementById('search-results-grid'); 3894 if (rg) rg.innerHTML = ''; 3895 showSections(false); // restore main grids 3896} 3897 3898 3899// ââ Userbot toggle âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 3900let _userbotEnabled = null; 3901 3902async function loadUserbotStatus() { 3903 try { 3904 const r = await fetch('/api/admin/userbot/status'); 3905 if (!r.ok) return; 3906 const d = await r.json(); 3907 _userbotEnabled = d.enabled; 3908 _applyUserbotUI(d); 3909 } catch(e) {} 3910} 3911 3912function _applyUserbotUI(d) { 3913 const enabled = d.enabled; 3914 const status = d.status || 'unknown'; 3915 const stats = d.stats || {}; 3916 const dot = document.getElementById('userbot-dot'); 3917 const track = document.getElementById('userbot-track'); 3918 const thumb = document.getElementById('userbot-thumb'); 3919 const label = document.getElementById('userbot-toggle-label'); 3920 const subtitle = document.getElementById('userbot-subtitle'); 3921 const statsEl = document.getElementById('userbot-stats'); 3922 const chk = document.getElementById('userbot-toggle'); 3923 if (!dot) return; 3924 3925 // Dot color 3926 if (enabled && status === 'running') { 3927 dot.style.background = '#00e87a'; 3928 dot.style.boxShadow = '0 0 6px #00e87a'; 3929 } else if (status === 'paused') { 3930 dot.style.background = '#f59e0b'; 3931 dot.style.boxShadow = '0 0 6px #f59e0b'; 3932 } else { 3933 dot.style.background = '#ef4444'; 3934 dot.style.boxShadow = 'none'; 3935 } 3936 3937 // Toggle switch 3938 chk.checked = enabled; 3939 track.style.background = enabled ? 'var(--accent)' : 'var(--gray4)'; 3940 thumb.style.transform = enabled ? 'translateX(-20px)' : 'translateX(0)'; 3941 label.textContent = enabled ? 'פע××' : '××××'; 3942 label.style.color = enabled ? 'var(--accent)' : 'var(--gray5)'; 3943 3944 // Subtitle 3945 const statusMap = { running: 'ð¢ ××××× ×××××¢×ת', paused: 'ð¡ ××ש××', unknown: enabled ? 'ð¢ ××××ר' : '⪠×× ×¤×¢××' }; 3946 subtitle.textContent = statusMap[status] || status; 3947 3948 // Stats 3949 if (stats.coupons_saved !== undefined) { 3950 const lastMsg = stats.last_message_at 3951 ? new Date(stats.last_message_at * 1000).toLocaleTimeString('he-IL') 3952 : 'â'; 3953 const nowSec = Math.floor(Date.now() / 1000); 3954 const heartbeat = stats.heartbeat_at; 3955 const aliveAgo = heartbeat ? (nowSec - heartbeat) : null; 3956 const aliveStr = aliveAgo === null 3957 ? '<span style="color:var(--gray5)">××× ××××¢</span>' 3958 : aliveAgo < 90 3959 ? `<span style="color:var(--accent)">
3959×× (××¤× × ${aliveAgo}ש׳)</span>` 3960 : aliveAgo < 300 3961 ? `<span style="color:#ffe45e">××¤× × ${Math.round(aliveAgo/60)} ×ק׳</span>` 3962 : `<span style="color:#ff6b6b">â ï¸ ×× ××××× ${Math.round(aliveAgo/60)} ×ק׳</span>`; 3963 statsEl.innerHTML = `${stats.coupons_saved} ×§×פ×× ×× × ×©××¨× â¢ ××××¢× ××ר×× ×: ${lastMsg} ⢠ת××××: ${aliveStr}`; 3964 } else { 3965 statsEl.textContent = ''; 3966 } 3967} 3968 3969async function toggleUserbot() { 3970 const newVal = !_userbotEnabled; 3971 _userbotEnabled = newVal; 3972 // Optimistic UI update 3973 _applyUserbotUI({ enabled: newVal, status: newVal ? 'running' : 'paused', stats: {} }); 3974 try { 3975 const r = await fetch('/api/admin/userbot/toggle', { 3976 method: 'POST', 3977 headers: { 'Content-Type': 'application/json' }, 3978 body: JSON.stringify({ enabled: newVal }), 3979 }); 3980 if (!r.ok) throw new Error(); 3981 // Reload actual status after 1s 3982 setTimeout(loadUserbotStatus, 1000); 3983 } catch(e) { 3984 // Revert on error 3985 _userbotEnabled = !newVal; 3986 loadUserbotStatus(); 3987 } 3988} 3989 3990// null = not loaded yet (switch stays dimmed/disabled) â never guess a value before the server confirms one. 3991let _jobBEnabled = null; 3992let _jobBToggleBusy = false; 3993 3994function _applyJobBToggleUI(enabled) { 3995 _jobBEnabled = enabled; 3996 const wrap = document.getElementById('job-b-toggle-wrap'); 3997 const track = document.getElementById('job-b-toggle-track'); 3998 const thumb = document.getElementById('job-b-toggle-thumb'); 3999 const label = document.getElementById('job-b-toggle-label'); 4000 const chk = document.getElementById('job-b-toggle'); 4001 if (!track) return; 4002 // Known value (true/false) â enable the switch. null â keep it dimmed/unclickable. 4003 wrap.style.opacity = enabled === null ? '0.4' : (_jobBToggleBusy ? '0.6' : '1'); 4004 wrap.style.pointerEvents = (enabled === null || _jobBToggleBusy) ? 'none' : 'auto'; 4005 if (enabled === null) { label.textContent = '×××¢×â¦'; return; } 4006 chk.checked = enabled; 4007 track.style.background = enabled ? 'var(--accent)' : 'var(--gray4)'; 4008 thumb.style.transform = enabled ? 'translateX(-20px)' : 'translateX(0)'; 4009 label.textContent = enabled ? 'פע××' : '××××'; 4010 label.style.color = enabled ? 'var(--accent)' : 'var(--gray5)'; 4011} 4012 4013async function toggleJobB() { 4014 if (_jobBToggleBusy || _jobBEnabled === null) return; // ignore clicks until state is known / prior click settled 4015 const newVal = !_jobBEnabled; 4016 _jobBToggleBusy = true; 4017 _applyJobBToggleUI(newVal); // optimistic, but now locked against double-clicks 4018 try { 4019 const r = await fetch('/api/admin/job-b/toggle', { 4020 method: 'POST', 4021 headers: { 'Content-Type': 'application/json' }, 4022 body: JSON.stringify({ enabled: newVal }), 4023 }); 4024 if (!r.ok) throw new Error(); 4025 const confirmed = await r.json(); // trust the server's echoed value, not our guess 4026 _jobBToggleBusy = false; 4027 _applyJobBToggleUI(!!confirmed.enabled); 4028 showToast(confirmed.enabled ? 'ð¢ Job B ×××¤×¢× â ×ר××¥ ××××× ×-02:00' : 'ð´ Job B ××ש×ת â ×× ×ר××¥ ×××××'); 4029 } catch(e) { 4030 _jobBToggleBusy = false; 4031 _applyJobBToggleUI(!newVal); // revert to prior known state on error 4032 showToast('â ש×××× ×ש×× ×× ×¡×××ס Job B â × ×¡× ×©××'); 4033 } 4034} 4035 4036// ââ Admin table state ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4037const ADM = { filter:'', sort:'newest', cat:'', disc:'', geo:'all', page:0, perPage:20 }; 4038const WEB = { page: 0, perPage: 20 }; 4039const LOG = { page: 0, perPage: 50 }; 4040const NL = { page: 0, perPage: 20 }; 4041const CAT_HE = {Fashion:'×××¤× ×',Tech:'××× ××××××',Food:'××××',Travel:'× ×¡××¢×ת', 4042 Vacation:'××פש×',Home:'××ת',Beauty:'××פ×',Health:'×ר×××ת', 4043 Kids:'×××××',Sports:'ספ×ר×',Other:'××ר', 4044 Leisure:'×¤× ××',Finance:'פ×× × ×¡×'}; 4045 4046const DTYPE_LABEL = { 4047 primary: { text: '××פ×ש ×שת×ש', bg: 'rgba(0,232,122,0.15)', color: 'var(--accent)', icon: 'ð' }, 4048 secondary: { text: '××פ×ש ××©× ×', bg: 'rgba(255,228,94,0.15)', color: 'var(--accent2)', icon: 'ð' }, 4049 auto: { text: '×××××××', bg: 'rgba(100,100,100,0.2)', color: 'var(--gray6)', icon: 'ð¤' }, 4050 scan: { text: '××פ×ש ××××§×', bg: 'rgba(94,180,255,0.15)', color: '#5eb4ff', icon: 'ð' }, 4051}; 4052 4053function _admCleanSrc(src) { 4054 if (!src) return { url: '', type: '' }; 4055 const m = src.match(/^\[(primary|secondary|auto)\]\|/); 4056 let type = m ? m[1] : ''; 4057 const url = src 4058 .replace(/^\[(?:primary|secondary|auto)\]\|/, '') 4059 .replace(/^Web:\s*/i, '').trim(); 4060 // Web-scraped coupons (no [tag] prefix, but has a URL) â 'scan' 4061 if (!type && (src.match(/^\/?\s*[Ww]eb:\s*https?:\/\//) || (!m && url.startsWith('http')))) { 4062 type = 'scan'; 4063 } 4064 return { url, type }; 4065} 4066 4067// Hebrew â English store name aliases for search matching 4068const STORE_ALIASES = { 4069 'קס×ר×':['castro','castro.com','castro fashion'], 4070 'castro':['קס×ר×'],'castro fashion':['קס×ר×'], 4071 '×שר××ר×':['isracard','israelicard'],'isracard':['×שר××ר×'],'israelicard':['×שר××ר×'], 4072 '××ר×':['zara'],'zara':['××ר×'], 4073 '×××ץ׳ ×× × ××':['h&m','hm'],'h&m':['×××ץ׳ ×× × ××'],'hm':['×××ץ׳ ×× × ××'], 4074 '× ×××§×':['nike'],'nike':['× ×××§×'], 4075 '××××ס':['adidas'],'adidas':['××××ס'], 4076 'ש××':['shein'],'ש×××':['shein'],'shein':['ש××','ש×××'], 4077 '×ר××× ×':['terminalx','terminal x'],'×ר××× × ××קס':['terminalx','terminal x'],'terminalx':['×ר××× ×','×ר××× × ××קס'], 4078 '×××':['bug'],'bug':['×××'], 4079 '×§×× ×ס פ×':['ksp'],'ksp':['×§×× ×ס פ×'], 4080 '××××£':['golf'],'golf':['××××£'], 4081 'פ×קס':['fox'],'fox':['פ×קס'], 4082 '×¨× ××ר':['renuar'],'renuar':['×¨× ××ר'], 4083 '××××§×':['adika'],'adika':['××××§×'], 4084 'ש×פרס×':['shufersal'],'shufersal':['ש×פרס×'], 4085 'ר×× ×××':['rami levy','rami-levy'],'rami levy':['ר×× ×××'], 4086 '×¢×××קספרס':['aliexpress'],'aliexpress':['×¢×××קספרס'], 4087 '××××××':['amazon'],'amazon':['××××××'], 4088 '×××§×× ×':['booking'],'booking':['×××§×× ×'], 4089 '×××××':['ebay'],'ebay':['×××××'], 4090}; 4091 4092function _searchAliases(q) { 4093 const direct = STORE_ALIASES[q] || []; 4094 // also try partial key match 4095 const partial = []; 4096 for (const [k,v] of Object.entries(STORE_ALIASES)) { 4097 if (k !== q && k.includes(q)) partial.push(...v); 4098 } 4099 return [...direct, ...partial]; 4100} 4101 4102// Shared pagination helper â build page-number list with ellipsis 4103function _pageNums(cur, tot) { 4104 if (tot <= 1) return []; 4105 const c = cur + 1; // 1-indexed 4106 const out = []; 4107 out.push(1); 4108 const lo = Math.max(2, c - 2); 4109 const hi = Math.min(tot - 1, c + 2); 4110 if (lo > 2) out.push('â¦'); 4111 for (let i = lo; i <= hi; i++) out.push(i); 4112 if (hi < tot - 1) out.push('â¦'); 4113 if (tot > 1) out.push(tot); 4114 return out; 4115} 4116 4117async function renderAdminTable() { 4118 // Build query â always fetch from server so new coupons are always visible 4119 const params = new URLSearchParams(); 4120 if (ADM.filter) params.set('search', ADM.filter); 4121 if (ADM.cat) params.set('category', ADM.cat); 4122 if (ADM.disc) params.set('source_type', ADM.disc); 4123 if (ADM.sort === 'newest' || ADM.sort === 'oldest') params.set('sort', ADM.sort); 4124 params.set('limit', ADM.perPage); 4125 params.set('offset', ADM.page * ADM.perPage); 4126 4127 let slice = [], total = 0; 4128 try { 4129 // Fetch page data + exact total count in parallel 4130 const countParams = new URLSearchParams(); 4131 if (ADM.filter) countParams.set('search', ADM.filter); 4132 if (ADM.cat) countParams.set('category', ADM.cat); 4133 if (ADM.disc) countParams.set('source_type', ADM.disc); 4134 const [res, countRes] = await Promise.all([ 4135 fetch(`/api/coupons?${params}`),
4136 fetch(`/api/coupons/count?${countParams}`), 4137 ]); 4138 const data = await res.json(); 4139 const countData = await countRes.json(); 4140 total = countData.count ?? data.length; 4141 const normalize = c => ({ 4142 id: c.id, store: c.store, code: c.code, 4143 discount: c.discount || '×× ××', notes: c.notes || '', 4144 cat: (c.categories && c.categories[0]) || 'Other', src: c.source || '', 4145 status: c.status || 'unknown', votes: c.votes || 0, 4146 createdAt: c.created_at || null, publishedAt: c.published_at || null, 4147 }); 4148 slice = data.map(normalize); 4149 if (ADM.geo === 'il') slice = slice.filter(c => isIsraeli(c.store, c.src)); 4150 if (ADM.geo === 'intl') slice = slice.filter(c => !isIsraeli(c.store, c.src)); 4151 } catch(e) { slice = []; total = 0; } 4152 4153 const pages = Math.max(1, Math.ceil(total / ADM.perPage)); 4154 4155 const html = slice.map((c, idx) => { 4156 const { url: srcUrl, type: srcType } = _admCleanSrc(c.src); 4157 const _tgNameMatch = c.src.match(/^Telegram:\s*(.+)/i); 4158 const srcCell = srcUrl.startsWith('https://t.me/') 4159 ? `<a href="${srcUrl}" target="_blank" style="color:#29b6f6;text-decoration:none;word-break:break-all" title="${srcUrl}"><svg width="10" height="10" viewBox="0 0 24 24" fill="#29b6f6" style="vertical-align:-1px;margin-left:3px"><path d="M11.944 0A12 12 0 1 0 24 12 12 12 0 0 0 11.944 0zm5.992 8.287-2.021 9.525c-.148.658-.55.818-1.113.508l-3-2.21-1.448 1.393c-.16.16-.295.295-.605.295l.213-3.052 5.56-5.023c.242-.214-.054-.333-.373-.12l-6.871 4.326-2.962-.924c-.644-.204-.657-.644.136-.953l11.574-4.462c.537-.194 1.006.131.91.697z"/></svg> ${srcUrl.replace('https://t.me/','@')}</a>` 4160 : srcUrl.startsWith('http') 4161 ? `<a href="${srcUrl}" target="_blank" style="color:var(--accent);text-decoration:none;word-break:break-all" title="${srcUrl}">${new URL(srcUrl).hostname.replace('www.','')}</a>` 4162 : _tgNameMatch 4163 ? `<span style="color:#29b6f6;word-break:break-all"><svg width="10" height="10" viewBox="0 0 24 24" fill="#29b6f6" style="vertical-align:-1px;margin-left:3px"><path d="M11.944 0A12 12 0 1 0 24 12 12 12 0 0 0 11.944 0zm5.992 8.287-2.021 9.525c-.148.658-.55.818-1.113.508l-3-2.21-1.448 1.393c-.16.16-.295.295-.605.295l.213-3.052 5.56-5.023c.242-.214-.054-.333-.373-.12l-6.871 4.326-2.962-.924c-.644-.204-.657-.644.136-.953l11.574-4.462c.537-.194 1.006.131.91.697z"/></svg> ${_tgNameMatch[1].trim()}</span>` 4164 : `<span style="color:var(--gray5)">${srcUrl||'â'}</span>`; 4165 const dtInfo = DTYPE_LABEL[srcType]; 4166 const isTelegram = !srcType && (c.src.startsWith('Telegram:') || c.src.includes('t.me/')); 4167 const dtCell = dtInfo 4168 ? `<span style="font-size:11px;padding:2px 7px;border-radius:20px;white-space:nowrap;background:${dtInfo.bg};color:${dtInfo.color}">${dtInfo.icon} ${dtInfo.text}</span>` 4169 : isTelegram 4170 ? `<span style="font-size:11px;padding:2px 7px;border-radius:20px;white-space:nowrap;background:rgba(41,182,246,0.15);color:#29b6f6">
4170<svg width="11" height="11" viewBox="0 0 24 24" fill="#29b6f6" style="vertical-align:-1px;margin-left:3px"><path d="M11.944 0A12 12 0 1 0 24 12 12 12 0 0 0 11.944 0zm5.992 8.287-2.021 9.525c-.148.658-.55.818-1.113.508l-3-2.21-1.448 1.393c-.16.16-.295.295-.605.295l.213-3.052 5.56-5.023c.242-.214-.054-.333-.373-.12l-6.871 4.326-2.962-.924c-.644-.204-.657-.644.136-.953l11.574-4.462c.537-.194 1.006.131.91.697z"/></svg> ×××ר×</span>` 4171 : `<span style="color:var(--gray5);font-size:11px">â</span>`; 4172 const safeCode = (c.code||'').replace(/'/g,"\\'"); 4173 const notesCell = c.notes ? `<span title="${c.notes.replace(/"/g,'"')}" style="cursor:default;color:var(--gray6);font-size:12px">${c.notes.length>40?c.notes.slice(0,40)+'â¦':c.notes}</span>` : `<span style="color:var(--gray4)">â</span>`; 4174 const rowNum = ADM.page * ADM.perPage + idx + 1; 4175 // Derive a useful link for the store: use the source page URL if available, else /go 4176 const _srcRaw = (c.source||''); 4177 const _srcUrl = _srcRaw.includes('|') ? _srcRaw.split('|').slice(1).join('|') : ''; 4178 const _storeHref = _srcUrl && _srcUrl.startsWith('http') ? _srcUrl : `/go?store=${encodeURIComponent(c.store)}`; 4179 // Favicon for admin table row 4180 const _admFavUrls = storeToFavicon(c.store, c.source) || []; 4181 const _admFavUrl = _admFavUrls[0] || ''; 4182 const _admFavFb = _admFavUrls.slice(1).join('|'); 4183 const _admFavHtml = _admFavUrl 4184 ? `<img src="${_admFavUrl}" onerror="_faviconErr(this,'${_admFavFb}','${(c.store||'').replace(/'/g,"\\'")}')" style="width:16px;height:16px;border-radius:3px;object-fit:contain;vertical-align:middle;flex-shrink:0">` 4185 : `<span style="display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:3px;background:var(--gray3);font-size:8px;font-weight:700;color:var(--white);flex-shrink:0">${(c.store||'?').replace(/[^a-zA-Z×-ת]/g,'')[0]||'?'}</span>`; 4186 return `<tr> 4187 <td style="color:var(--gray5);font-size:11px;text-align:center">${rowNum}</td> 4188 <td style="text-align:center;padding:0 4px">${_admFavHtml}</td> 4189 <td class="td-store"><a href="${_storeHref}" target="_blank" style="color:var(--white);text-decoration:none;border-bottom:1px dotted var(--gray5)" onmouseover="this.style.color='var(--accent)';this.style.borderBottomColor='var(--accent)'" onmouseout="this.style.color='var(--white)';this.style.borderBottomColor='var(--gray5)'">${STORE_HE[c.store] || c.store}</a></td> 4190 <td>${(c.code||'').toUpperCase()==='NO_CODE' ? `<span style="color:var(--gray5);font-size:13px">-</span>` : `<span class="td-code" style="font-size:13px;letter-spacing:1px">${c.code}</span>`}</td> 4191 <td>${c.discount||'â'}</td> 4192 <td class="col-notes">${notesCell}</td> 4193 <td>${CAT_HE[c.cat]||c.cat||'â'}</td> 4194 <td class="col-source">${srcCell}</td> 4195 <td class="col-source">${dtCell}</td> 4196 <td style="font-size:11px;color:var(--gray5);white-space:nowrap">${c.createdAt ? new Date(c.createdAt).toLocaleDateString('he-IL',{day:'2-digit',month:'2-digit',year:'2-digit'}) : 'â'}</td> 4197 <td style="font-size:11px;color:var(--gray5);white-space:nowrap">${c.publishedAt ? new Date(c.publishedAt).toLocaleDateString('he-IL',{day:'2-digit',month:'2-digit',year:'2-digit'}) : 'â'}</td> 4198 <td><div class="td-actions"> 4199 <button class="td-btn" onclick="copyCode('${safeCode}','${(c.store||'').replace(/'/g,"\\'")}','')">×עתק</button> 4200 <button class="td-btn danger" onclick="adminDelete('${c.id}',this,'${(c.store||'').replace(/'/g,"\\'")}','${(c.src||'').replace(/'/g,"\\'")}')">×××§</button> 4201 </div></td> 4202 </tr>`; 4203 }).join('') || `<tr><td colspan="12" style="text-align:center;color:var(--gray5);padding:24px">××× ×ª×צ××ת</td></tr>`; 4204 4205 ['admin-tbody','coupons-tbody'].forEach(tid => { 4206 const el = document.getElementById(tid); 4207 if (el) el.innerHTML = html; 4208 }); 4209 4210 const nums = _pageNums(ADM.page, pages); 4211 const numBtns = nums.map(n => n === 'â¦' 4212 ? `<span style="color:var(--gray5);padding:0 4px;font-size:13px">â¦</span>` 4213 : `<button class="td-btn${n-1===ADM.page?' page-active':''}" onclick="admGoPage(${n-1})" style="min-width:32px">${n}</button>` 4214 ).join(''); 4215 4216 const pagHtml = ` 4217 <div style="font-size:11px;color:var(--gray5);text-align:center;padding:8px 0 2px">${total} ×§×פ×× ××</div> 4218 <div style="display:flex;align-items:center;justify-content:center;gap:4px;padding:8px 18px 14px;border-top:1px solid var(--gray3);flex-wrap:wrap"> 4219 <button class="td-btn" onclick="admPage(-1)" ${ADM.page===0?'disabled':''} style="padding:4px 10px">â¹ ××§×××</button> 4220 ${numBtns} 4221 <button class="td-btn" onclick="admPage(1)" ${ADM.page>=pages-1?'disabled':''} style="padding:4px 10px">××× âº</button> 4222 <span style="display:flex;align-items:center;gap:6px;margin-right:8px;border-right:1px solid var(--gray3);padding-right:12px">
4223 <span style="font-size:12px;color:var(--gray5)">×¢×××</span> 4224 <input type="number" min="1" max="${pages}" value="${ADM.page+1}" 4225 style="width:52px;background:var(--gray3);border:1px solid var(--gray4);border-radius:6px;padding:3px 6px;font-size:12px;color:var(--white);text-align:center;outline:none;font-family:'DM Sans',sans-serif" 4226 onkeydown="if(event.key==='Enter'){const p=Math.max(1,Math.min(${pages},+this.value||1));admGoPage(p-1)}" 4227 onblur="const p=Math.max(1,Math.min(${pages},+this.value||1));admGoPage(p-1)" 4228 /> 4229 <span style="font-size:12px;color:var(--gray5)">×ת×× ${pages}</span> 4230 </span> 4231 </div>`; 4232 ['admin-pagination','coupons-pagination'].forEach(pid => { 4233 const el = document.getElementById(pid); 4234 if (el) el.innerHTML = pagHtml; 4235 }); 4236} 4237 4238function admPage(dir) { ADM.page = Math.max(0, ADM.page + dir); renderAdminTable(); } 4239function admGoPage(n) { ADM.page = n; renderAdminTable(); } 4240function filterAdmin(v){ ADM.filter = v; ADM.page = 0; renderAdminTable(); } 4241function admSort(sel) { ADM.sort = sel.value; ADM.page = 0; renderAdminTable(); } 4242function admCat(sel) { ADM.cat = sel.value; ADM.page = 0; renderAdminTable(); } 4243function admDisc(sel) { ADM.disc = sel.value; ADM.page = 0; renderAdminTable(); } 4244function admGeo(sel) { ADM.geo = sel.value; ADM.page = 0; renderAdminTable(); } 4245 4246async function adminDelete(id, btn, storeName, storeSrc) { 4247 if (!id) return; 4248 const row = btn.closest('tr'); 4249 4250 // Look up domain from stores map (populated from DB) 4251 const storeDomain = STORE_DOMAINS[(storeName || '').toLowerCase().trim()] || ''; 4252 4253 const choice = await _showDeleteModal(storeName || '', storeDomain); 4254 if (!choice) return; 4255 4256 btn.disabled = true; 4257 4258 if (choice.action === 'one') { 4259 const res = await fetch(`/api/admin/coupons/${id}`, { method: 'DELETE' }).catch(() => null); 4260 if (res && res.ok) { 4261 if (row) { row.style.transition='opacity 0.3s'; row.style.opacity='0'; setTimeout(()=>row.remove(),300); } 4262 const idx = COUPONS.findIndex(c => c.id === id); 4263 if (idx > -1) COUPONS.splice(idx, 1); 4264 const idx2 = ALL_COUPONS.findIndex(c => c.id === id); 4265 if (idx2 > -1) ALL_COUPONS.splice(idx2, 1); 4266 if (choice.block && choice.domain) { 4267 await fetch('/api/admin/blocked-sites', { 4268 method: 'POST', headers: {'Content-Type':'application/json'}, 4269 body: JSON.stringify({ name: storeName, domain: choice.domain, reason: '× ×××§ ×××××××' }) 4270 }).catch(() => null); 4271 showToast('ð« ××תר × ×ס×'); 4272 } 4273 renderAdminTable(); 4274 } else { btn.disabled = false; } 4275 4276 } else { 4277 const deleteStore = choice.action === 'store'; 4278 const res = await fetch('/api/admin/store-and-coupons', { 4279 method: 'DELETE', 4280 headers: {'Content-Type':'application/json'}, 4281 body: JSON.stringify({ store: storeName, delete_store: deleteStore }) 4282 }).catch(() => null); 4283 if (res && res.ok) { 4284 const d = await res.json(); 4285 let msg = `ð × ×××§× ${d.coupons_deleted} ×§×פ×× ××${deleteStore && d.stores_deleted ? ' + ××× ×ת' : ''}`; 4286 if (choice.block && choice.domain) { 4287 const bRes = await fetch('/api/admin/blocked-sites', { 4288 method: 'POST', 4289 headers: {'Content-Type':'application/json'}, 4290 body: JSON.stringify({ name: storeName, domain: choice.domain, reason: '× ×××§ ×××××××' }) 4291 }).catch(() => null); 4292 if (bRes && bRes.ok) msg += ' + × ×ס×'; 4293 } 4294 showToast(msg); 4295 renderAdminTable(); 4296 } else { btn.disabled = false; } 4297 } 4298} 4299 4300function _showDeleteModal(storeName, storeDomain) { 4301 return new Promise(resolve => { 4302 const overlay = document.createElement('div'); 4303 overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;display:flex;align-items:center;justify-content:center'; 4304 overlay.innerHTML = ` 4305 <div id="_del_modal" style="background:#1a1a1a;border:1px solid #333;border-radius:14px;padding:28px 32px;min-width:320px;max-width:440px;text-align:right;direction:rtl;font-family:'DM Sans',sans-serif"> 4306 <div style="font-size:16px;font-weight:600;margin-bottom:6px;color:#fff">×××קת ×§×פ××</div> 4307 <div style="font-size:13px;color:#888;margin-bottom:18px">×× ×ת: <span style="color:#fff">${storeName || 'â'}</span></div> 4308 <div style="display:flex;flex-direction:column;gap:8px"> 4309 <label style="display:flex;align-items:center;gap:10px;cursor:pointer;padding:10px 14px;border:1px solid #444;border-radius:8px;background:#2a2a2a"> 4310 <input type="radio" name="_del_action" value="one" checked style="accent-color:var(--accent);cursor:pointer" />
4311 <span style="font-size:13px;color:#fff">ð ×××§ ×§×פ×× ×× ××××</span> 4312 </label> 4313 <label style="display:flex;align-items:center;gap:10px;cursor:pointer;padding:10px 14px;border:1px solid #444;border-radius:8px;background:#2a2a2a"> 4314 <input type="radio" name="_del_action" value="coupons" style="accent-color:var(--accent);cursor:pointer" /> 4315 <span style="font-size:13px;color:#fff">ðð ×××§ ×ת ×× ××§×פ×× ×× ×©× ××× ×ת</span> 4316 </label> 4317 <label style="display:flex;align-items:center;gap:10px;cursor:pointer;padding:10px 14px;border:1px solid #8b2222;border-radius:8px;background:#3a1a1a"> 4318 <input type="radio" name="_del_action" value="store" style="accent-color:#e05555;cursor:pointer" /> 4319 <span style="font-size:13px;color:#ff6b6b">ð£ ×××§ ×ת ×× ××§×פ×× ×× + ××× ×ת ×-stores</span> 4320 </label> 4321 </div> 4322 <div style="margin-top:16px;padding-top:14px;border-top:1px solid #2a2a2a"> 4323 <label style="display:flex;align-items:center;gap:10px;cursor:pointer;font-size:13px;color:#aaa"> 4324 <input type="checkbox" id="_del_block_cb" style="width:16px;height:16px;accent-color:#e05555;cursor:pointer" /> 4325 <span>ð« ××סף ×× ×רש××ת ××ס×××</span> 4326 </label> 4327 <div id="_del_block_domain" style="display:none;margin-top:10px"> 4328 <input id="_del_block_input" type="text" value="${storeDomain || ''}" placeholder="domain.com" style="background:#2a2a2a;border:1px solid #444;border-radius:8px;padding:7px 12px;font-size:13px;color:#fff;outline:none;font-family:'DM Sans',sans-serif;width:100%;box-sizing:border-box;direction:ltr;text-align:left" /> 4329 </div> 4330 </div> 4331 <div style="display:flex;gap:10px;justify-content:flex-end;margin-top:20px"> 4332 <button id="_del_cancel" style="background:transparent;border:1px solid #444;border-radius:8px;padding:8px 18px;color:#aaa;cursor:pointer;font-size:13px;font-family:'DM Sans',sans-serif">×××××</button> 4333 <button id="_del_ok" style="background:var(--accent);border:none;border-radius:8px;padding:8px 22px;color:#000;cursor:pointer;font-size:13px;font-weight:600;font-family:'DM Sans',sans-serif">××ש×ר</button> 4334 </div> 4335 </div>`; 4336 4337 document.body.appendChild(overlay); 4338 4339 const cb = overlay.querySelector('#_del_block_cb'); 4340 const domainWrap = overlay.querySelector('#_del_block_domain'); 4341 cb.addEventListener('change', () => { domainWrap.style.display = cb.checked ? 'block' : 'none'; }); 4342 4343 function _resolve() { 4344 const action = overlay.querySelector('input[name="_del_action"]:checked')?.value || 'one'; 4345 const block = cb.checked; 4346 const domain = overlay.querySelector('#_del_block_input')?.value?.trim() || ''; 4347 document.body.removeChild(overlay); 4348 resolve({ action, block, domain }); 4349 } 4350 4351 overlay.querySelector('#_del_ok').addEventListener('click', e => { e.stopPropagation(); _resolve(); }); 4352 overlay.querySelector('#_del_cancel').addEventListener('click', e => { 4353 e.stopPropagation(); 4354 document.body.removeChild(overlay); 4355 resolve(null); 4356 }); 4357 }); 4358} 4359 4360function filterPub() { 4361 pubSearch = document.getElementById('pub-search').value; 4362 // Do NOT clear search results on input â results persist until next AI search starts 4363} 4364 4365// ââ Admin auth âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4366function _isAdminAuthed() { 4367 return sessionStorage.getItem(_ADMIN_KEY) === '1'; 4368} 4369function _getUserRole() { 4370 return sessionStorage.getItem('couponai_user_role') || 'operator'; 4371} 4372function _isPowerUser() { 4373 // admin or operator see full technical details 4374 return _isAdminAuthed() && (_getUserRole() === 'admin' || _getUserRole() === 'operator'); 4375} 4376 4377function _showLoginModal() { 4378 document.getElementById('login-overlay').style.display = 'flex'; 4379 setTimeout(() => document.getElementById('login-username').focus(), 50); 4380} 4381 4382function _hideLoginModal() { 4383 document.getElementById('login-overlay').style.display = 'none'; 4384 document.getElementById('login-username').value = ''; 4385 document.getElementById('login-password').value = ''; 4386 document.getElementById('login-error').style.display = 'none'; 4387} 4388 4389async function submitLogin(e) { 4390 if (e) e.preventDefault(); 4391 const username = (document.getElementById('login-username').value || '').trim(); 4392 const pass = document.getElementById('login-password').value; 4393 const err = document.getElementById('login-error'); 4394 err.style.display = 'none'; 4395 try { 4396 const res = await fetch('/api/admin/login', { 4397 method: 'POST', 4398 headers: {'Content-Type':'application/json'}, 4399 body: JSON.stringify({username, password: pass, did: _getDeviceId()}) 4400 }); 4401 if (res.ok) { 4402 const data = await res.json(); 4403 sessionStorage.setItem(_ADMIN_KEY, '1'); 4404 sessionStorage.setItem('couponai_admin_user', data.username || username); 4405 sessionStorage.setItem('couponai_user_role', data.role || 'operator'); 4406 document.body.classList.add('is-staff'); 4407 // Tag a login event immediately so deviceâuser mapping exists in DB 4408 trackEvent('login', '×× ××¡× ×× ××××'); 4409 await _syncAnalyticsToServer(false).catch(() => {}); 4410 _hideLoginModal(); 4411 _doShowPage('admin'); 4412 } else { 4413 err.style.display = 'block'; 4414 document.getElementById('login-password').value = ''; 4415 document.getElementById('login-password').focus(); 4416 } 4417 } catch(e) { 4418 err.style.display = 'block'; 4419 } 4420} 4421 4422function adminLogout() { 4423 sessionStorage.removeItem(_ADMIN_KEY);
4424 sessionStorage.removeItem('couponai_admin_user'); 4425 sessionStorage.removeItem('couponai_user_role'); 4426 document.body.classList.remove('is-staff'); 4427 showPage('public'); 4428} 4429 4430// Restore is-staff on page reload if session is still active 4431if (sessionStorage.getItem(_ADMIN_KEY) === '1') { 4432 document.body.classList.add('is-staff'); 4433} 4434 4435// ââ User management ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4436function showToast(msg, duration) { 4437 const t = document.getElementById('toast'); 4438 if (!t) return; 4439 t.textContent = msg; 4440 t.classList.add('show'); 4441 clearTimeout(t._hideTimer); 4442 const ms = duration || (msg.includes('â') || msg.includes('â ï¸') ? 10000 : 4000); 4443 t._hideTimer = setTimeout(() => t.classList.remove('show'), ms); 4444} 4445 4446async function _loadUsers() { 4447 const tbody = document.getElementById('users-tbody'); 4448 if (!tbody) return; 4449 tbody.innerHTML = '<tr><td colspan="7" style="padding:20px;text-align:center;color:var(--gray5)">×××¢×...</td></tr>'; 4450 const res = await fetch('/api/admin/users', { cache: 'no-store' }); 4451 const users = await res.json(); 4452 if (!users.length) { 4453 tbody.innerHTML = '<tr><td colspan="7" style="padding:20px;text-align:center;color:var(--gray5)">××× ×שת×ש××</td></tr>'; 4454 return; 4455 } 4456 const _ROLE_BADGE = { 4457 admin: `<span style="font-size:11px;padding:2px 9px;border-radius:20px;background:rgba(252,129,129,0.15);color:#fc8181;font-weight:600">ð ×××××</span>`, 4458 operator: `<span style="font-size:11px;padding:2px 9px;border-radius:20px;background:rgba(255,228,94,0.15);color:var(--accent2);font-weight:600">ð§ ××פר××ר</span>`, 4459 user: `<span style="font-size:11px;padding:2px 9px;border-radius:20px;background:rgba(100,100,100,0.2);color:var(--gray6);font-weight:600">ð¤ ×שת×ש</span>`, 4460 }; 4461 const myRole = _getUserRole(); 4462 tbody.innerHTML = users.map((u, i) => { 4463 const role = u.role || 'operator'; 4464 const badge = _ROLE_BADGE[role] || _ROLE_BADGE.operator; 4465 const deleteBtn = myRole === 'admin' 4466 ? `<button class="td-btn" onclick="_deleteUser('${u.id}','${u.username}')" style="color:#ff6b6b;border-color:rgba(255,107,107,0.3)">×××§</button>` 4467 : `<button class="td-btn" disabled title="רק ××××× ×××× ×××××§ ×שת×ש××" style="opacity:0.3;cursor:not-allowed">×××§</button>`; 4468 const lastLogin = u.last_login_at 4469 ? new Date(u.last_login_at).toLocaleString('he-IL', {day:'2-digit',month:'2-digit',year:'2-digit',hour:'2-digit',minute:'2-digit'}) 4470 : 'â'; 4471 const lastIp = u.last_login_ip || 'â'; 4472 return `<tr style="border-bottom:1px solid var(--gray3)"> 4473 <td style="padding:12px 16px;color:var(--gray5);font-size:13px">${i+1}</td> 4474 <td style="padding:12px 16px;font-size:14px;font-weight:500">${u.username}</td> 4475 <td style="padding:12px 16px">${badge}</td> 4476 <td style="padding:12px 16px;font-size:12px;color:var(--gray5)">${new Date(u.created_at).toLocaleDateString('he-IL')}</td> 4477 <td style="padding:12px 16px;font-size:12px;color:var(--gray6)">${lastLogin}</td> 4478 <td style="padding:12px 16px;font-size:11px;color:var(--gray5);font-family:monospace">${lastIp}</td> 4479 <td style="padding:12px 16px;display:flex;gap:6px"> 4480 <button class="td-btn" onclick="_openEditUserModal('${u.id}','${u.username}','${role}')">ער××</button> 4481 ${deleteBtn} 4482 </td> 4483 </tr>`; 4484 }).join(''); 4485} 4486 4487async function _openAddUserModal() { 4488 const username = prompt('×©× ×שת×ש ××ש:'); 4489 if (!username) return; 4490 const password = prompt('ס×ס×× (××× ×³ 6 ת××××):'); 4491 if (!password) return; 4492 const roleRaw = prompt('תפק×× (admin / operator / user):', 'operator'); 4493 const role = ['admin','operator','user'].includes((roleRaw||'').trim().toLowerCase()) ? roleRaw.trim().toLowerCase() : 'operator'; 4494 try { 4495 const r = await fetch('/api/admin/users', { 4496 method: 'POST', 4497 headers: {'Content-Type':'application/json'}, 4498 body: JSON.stringify({username: username.trim(), password, role}) 4499 }); 4500 if (r.ok) { 4501 showToast('×שת×ש × ×סף â'); 4502 await _loadUsers(); 4503 } else { 4504 const d = await r.json().catch(() => ({})); 4505 showToast('ש××××: ' + (d.detail || '× ×¡× ×©××')); 4506 } 4507 } catch(e) { 4508 showToast('ש×××ת רשת â × ×¡× ×©××'); 4509 } 4510} 4511 4512function _openEditUserModal(id, username, role) { 4513 document.getElementById('eu-id').value = id; 4514 document.getElementById('eu-username').value = username; 4515 document.getElementById('eu-role').value = role || 'operator'; 4516 document.getElementById('eu-password').value = ''; 4517 document.getElementById('eu-error').style.display = 'none'; 4518 document.getElementById('edit-user-overlay').style.display = 'flex'; 4519 setTimeout(() => document.getElementById('eu-username').focus(), 50); 4520} 4521 4522function _closeEditUserModal() { 4523 document.getElementById('edit-user-overlay').style.display = 'none'; 4524} 4525 4526async function _submitEditUser() { 4527 const id = document.getElementById('eu-id').value; 4528 const username = document.getElementById('eu-username').value.trim(); 4529 const password = document.getElementById('eu-password').value; 4530 const role = document.getElementById('eu-role').value; 4531 const err = document.getElementById('eu-error'); 4532 if (!username) { err.textContent = '×©× ×שת×ש ×× ×××× ××××ת ר××§'; err.style.display='block'; return; } 4533 const body = { username, role }; 4534 if (password) body.password = password; 4535 const r = await fetch(`/api/admin/users/${id}`, { 4536 method: 'PATCH', 4537 headers: {'Content-Type':'application/json'}, 4538 body: JSON.stringify(body) 4539 }); 4540 if (r.ok) { 4541 _closeEditUserModal(); 4542 showToast('×שת×ש ×¢×××× â'); 4543 _loadUsers(); 4544 } else { 4545 const d = await r.json(); 4546 err.textContent = d.detail || 'ש×××× ××¢××××'; 4547 err.style.display = 'block'; 4548 } 4549} 4550 4551async function _deleteUser(id, username) { 4552 if (!confirm(`×××××§ ×ת ××שת×ש "${username}"?`)) return; 4553 const r = await fetch(`/api/admin/users/${id}`, { 4554 method: 'DELETE', 4555 headers: { 'X-Caller-Role': _getUserRole() } 4556 }); 4557 if (r.ok) { showToast('×שת×ש × ×××§'); await _loadUsers(); } 4558 else { 4559 const d = await r.json().catch(() => ({})); 4560 showToast('ש××××: ' + (d.detail || '××× ×רש××')); 4561 } 4562} 4563 4564function showPage(id){ 4565 if (id === 'admin' && !_isAdminAuthed()) { 4566 _showLoginModal(); 4567 return; 4568 } 4569 _doShowPage(id); 4570} 4571 4572function _doShowPage(id){ 4573 document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); 4574 document.getElementById('page-'+id).classList.add('active'); 4575 document.querySelectorAll('.nav-link').forEach((b,i)=>b.classList.toggle('active',(id==='public'&&i===0)||(id==='admin'&&i===1))); 4576 // Re-render charts + refresh all data when admin becomes visible 4577 if (id === 'admin') { 4578 requestAnimationFrame(async () => { 4579 // 1. Refresh coupon metrics (active/total/stores) from server
4580 try { await loadStats(); } catch(e) {} 4581 // 2. Pull latest analytics events from server then re-render dashboard panels 4582 try { await _loadAnalyticsFromServer(); } catch(e) {} 4583 try { _renderPopSearchBar(); } catch(e) {} 4584 try { _renderSlowBar(); } catch(e) {} 4585 try { loadAnalytics(); } catch(e) {} 4586 try { loadSpeedAnalytics(); } catch(e) {} 4587 try { loadPopupAnalytics(); } catch(e) {} 4588 // 3. Refresh source panel + coupon table 4589 try { _fetchSourceData(); } catch(e) {} 4590 try { renderAdminTable(); } catch(e) {} 4591 try { _refreshOrphanCount(); } catch(e) {} 4592 }); 4593 } 4594} 4595// Apply is-mobile class â uses ?pw= param in preview mode, innerWidth on real devices 4596(function() { 4597 function _applyMobileClass() { 4598 var w = window._previewWidth || window.innerWidth; 4599 if (w <= 640) { 4600 document.body.classList.add('is-mobile'); 4601 } else { 4602 document.body.classList.remove('is-mobile'); 4603 } 4604 } 4605 _applyMobileClass(); 4606 window.addEventListener('resize', _applyMobileClass); 4607})(); 4608 4609const _VIEW_TITLES = { dashboard:'×ש××ר×', coupons:'×× ××§×פ×× ××', sources:'×××ר×', 'coupon-sites':'××ª×¨× ×§×פ×× ×× ××××', 'automation-scans':'סר××§×ת', web:'×תר×× ×©× ×¡×¨×§×', instagram:'××× ×¤×××× ×¡×¨××', newsletters:'× ×××××ר××', affiliate:'ש×תפ×××ת', 'blocked-sites':'×תר×× ×ס××××', stores:'×× ×××ת', pending:'×××××§×', 'no-url':'××× ×תר ××¢×', users:'×שת×ש××', settings:'×××ר×ת', logs:'×××××', 'hide-mechanism':'×× ×× ×× ×סתר×', expired:'×¤× ×ª××§×£', 'search-anim':'×× ××צ××ת ××פ×ש', mole:'×ש××§××', minigames:'×× ××צ××ת ××¢×× ×', debug:'Pipeline Debugger', 'null-domain':'××× ××××××', 'orphan-coupons':'×§×פ×× ×× ××× ××××××', 'analytics-usage':'×× ××××קס ש×××ש', 'analytics-speed':'××× × ××פ×ש', 'analytics-popup':'פ×פ×פ ×§×פ×× ××', 'analytics-extension':'ת×סף Chrome', 'analytics-buttons':'תפר×× ××פת×ר××', 'store-pages-demo':'××× ××¤× ×× ×××ת', 'home-demo':'×××§×פ ×¢××× ××ת', 'contact-messages':'××××¢×ת צ×ר קשר', 'user-profiles':'פר×פ××× ×שת×ש××', 'search-intel':'× ×ת×× ××פ×ש××', 'demand-gaps':'×××§×ש ××× ×§×פ×× ××', 'traffic-sources':'××§×ר×ת ×ª× ××¢×', 'geo-map':'×××××רפ××', 'device-stats':'××ש×ר×× ××פ××¤× ××', 'js-errors':'ש××××ת JS' }; 4610 4611let _activeAdminView = 'dashboard'; 4612 4613// ââ Trusted coupon sites (××ª×¨× ×§×פ×× ×× ××××) ââââââââââââââââââââââââââââââââ 4614const INPUT_PRICE_PER_TOKEN = 0.80 / 1_000_000; 4615const OUTPUT_PRICE_PER_TOKEN = 4.00 / 1_000_000; 4616 4617function _calcSiteCost(tokIn, tokOut) { 4618 return (tokIn * INPUT_PRICE_PER_TOKEN) + (tokOut * OUTPUT_PRICE_PER_TOKEN); 4619} 4620 4621function _fmtCost(dollars) { 4622 if (dollars === 0) return 'â'; 4623 if (dollars < 0.01) return '<$0.01'; 4624 return '$' + dollars.toFixed(2); 4625} 4626 4627function _fmtTokens(n) { 4628 if (!n) return 'â'; 4629 return n >= 1000 ? (n/1000).toFixed(1) + 'K' : String(n); 4630} 4631 4632async function loadCouponSites() { 4633 const tbody = document.getElementById('coupon-sites-tbody'); 4634 try { 4635 const r = await fetch('/api/admin/trusted-coupon-sites'); 4636 const data = await r.json(); 4637 document.getElementById('src-coupon-sites-count').textContent = data.length; 4638 if (!data.length) { 4639 tbody.innerHTML = '<tr><td colspan="6" style="color:var(--gray5);text-align:center;padding:20px">××× ×תר××</td></tr>'; 4640 return; 4641 } 4642 tbody.innerHTML = data.map((s, i) => { 4643 const method = s.extraction_method || 'bs'; 4644 const isBS = method === 'bs'; 4645 const methodBadge = isBS 4646 ? `<span style="background:#1a3a1a;color:#68d391;border:1px solid #2d6a4f;border-radius:5px;padding:2px 8px;font-size:11px;font-family:monospace;cursor:pointer" onclick="toggleExtractionMethod('${s.id}','ai')" title="×××¥ ××¢××ר ×-AI">BS</span>` 4647 : `<span style="background:#3a1a2a;color:#fc8181;border:1px solid #7b2d3f;border-radius:5px;padding:2px 8px;font-size:11px;font-family:monospace;cursor:pointer" onclick="toggleExtractionMethod('${s.id}','bs')" title="×××¥ ××¢××ר ×-BS">AI</span>`;
4648 return ` 4649 <tr> 4650 <td style="color:var(--gray5)">${i+1}</td> 4651 <td><b>${s.name}</b></td> 4652 <td><a href="${s.url}" target="_blank" style="color:var(--accent);font-size:12px">${s.url}</a></td> 4653 <td style="font-family:monospace;font-size:11px;color:var(--gray5)">${s.store_url_pattern || 'â'}</td> 4654 <td>${methodBadge}</td> 4655 <td><span style="color:${s.active ? 'var(--accent)' : 'var(--gray5)'}; font-size:12px">${s.active ? 'â פע××' : '⸠××××'}</span></td> 4656 <td> 4657 <button class="btn-sm btn-ghost" onclick="toggleTrustedSite('${s.id}', ${!s.active})">${s.active ? '×××' : '×פע×'}</button> 4658 <button class="btn-sm btn-ghost" style="color:#fc8181" onclick="deleteTrustedSite('${s.id}')">×××§</button> 4659 </td> 4660 </tr>`; 4661 }).join(''); 4662 } catch(e) { 4663 tbody.innerHTML = '<tr><td colspan="6" style="color:#fc8181;text-align:center;padding:20px">ש×××× ×××¢×× ×</td></tr>'; 4664 } 4665} 4666 4667function updateJobBEstCost() { 4668 const sites = 25; 4669 const cap = parseInt(document.getElementById('cfg-per-site-cap')?.value || 15); 4670 const maxT = parseInt(document.getElementById('cfg-max-tokens')?.value || 1500); 4671 // Estimate: avg ~800 input + maxT/2 output tokens per call 4672 const estIn = sites * cap * 800; 4673 const estOut = sites * cap * (maxT / 2); 4674 const estCost = (estIn * 0.80 / 1_000_000) + (estOut * 4.00 / 1_000_000); 4675 const el = document.getElementById('job-b-est-cost'); 4676 if (el) el.textContent = `×¢××ת ×קס׳ ×ש×ערת: $${estCost.toFixed(2)}`; 4677} 4678 4679async function saveJobBConfig() { 4680 const btn = document.getElementById('job-b-config-save-btn'); 4681 const statusEl = document.getElementById('job-b-config-status'); 4682 btn.disabled = true; 4683 btn.textContent = 'ש××ר...'; 4684 try { 4685 const body = { 4686 max_tokens: parseInt(document.getElementById('cfg-max-tokens').value), 4687 max_urls_per_site: parseInt(document.getElementById('cfg-max-urls').value), 4688 il_coupon_max_pages: parseInt(document.getElementById('cfg-il-coupon-max').value), 4689 per_site_call_cap: parseInt(document.getElementById('cfg-per-site-cap').value), 4690 max_cost_usd: parseFloat(document.getElementById('cfg-max-cost').value), 4691 }; 4692 const r = await fetch('/api/admin/job-b-config', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)}); 4693 const res = await r.json(); 4694 statusEl.style.display = 'block'; 4695 statusEl.style.color = 'var(--accent)'; 4696 statusEl.textContent = 'â ××××ר×ת × ×©××¨× â ××××× ×ר××¦× ×××× (02:00)'; 4697 } catch(e) { 4698 statusEl.style.display = 'block'; 4699 statusEl.style.color = '#fc8181'; 4700 statusEl.textContent = 'â ש×××× ×ש××ר×'; 4701 } 4702 btn.disabled = false; 4703 btn.textContent = 'ש××ר'; 4704} 4705 4706async function addTrustedSitePrompt() { 4707 const name = prompt('×©× ××תר:'); 4708 if (!name) return; 4709 const url = prompt('×ת××ת (URL):'); 4710 if (!url) return; 4711 const pattern = prompt('ת×× ×ת URL ××× ×ת (××ש×: /coupons/{slug}/) â ×פשר ××ש××ר ר××§:') || ''; 4712 try { 4713 await fetch('/api/admin/trusted-coupon-sites', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({name, url, store_url_pattern: pattern || null, active: true})}); 4714 showToast('â ××תר × ×סף'); 4715 loadCouponSites(); 4716 } catch(e) { showToast('â ש×××× ×××ספ×'); } 4717} 4718 4719async function toggleTrustedSite(id, active) { 4720 await fetch(`/api/admin/trusted-coupon-sites/${id}`, {method:'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify({active})}); 4721 loadCouponSites(); 4722} 4723 4724async function toggleExtractionMethod(id, method) { 4725 await fetch(`/api/admin/trusted-coupon-sites/${id}`, {method:'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify({extraction_method: method})}); 4726 loadCouponSites(); 4727} 4728 4729async function deleteTrustedSite(id) { 4730 if (!confirm('×××××§ ×תר ××?')) return; 4731 await fetch(`/api/admin/trusted-coupon-sites/${id}`, {method:'DELETE'}); 4732 showToast('ð ××תר × ×××§'); 4733 loadCouponSites(); 4734} 4735 4736// ââ Automation scans view ââââââââââââââââââââââââââââââââââââââââââââââââââââ 4737function _renderJobLog(prefix, log) { 4738 if (!log) return; 4739 const el = document.getElementById(`${prefix}-log`); 4740 if (el) el.style.display = 'block';
4741 const ts = log.run_at ? new Date(log.run_at).toLocaleString('he-IL') : 'â'; 4742 const fin = log.finished_at ? ` (ס×××: ${new Date(log.finished_at).toLocaleString('he-IL')})` : ''; 4743 const statusColor = log.status === 'success' ? 'var(--accent)' : log.status === 'error' ? '#fc8181' : '#f6c90e'; 4744 const statusLabel = log.status === 'success' ? 'â ×צ×××' : log.status === 'error' ? 'â ש××××' : 'Ⳡרץ...'; 4745 const timeEl = document.getElementById(`${prefix}-log-time`); 4746 const statusEl = document.getElementById(`${prefix}-log-status`); 4747 if (timeEl) timeEl.textContent = ts + fin; 4748 if (statusEl) { statusEl.textContent = statusLabel; statusEl.style.color = statusColor; } 4749 4750 if (prefix === 'job-b') { 4751 const s = document.getElementById('job-b-log-sites'); 4752 const c = document.getElementById('job-b-log-coupons'); 4753 if (s) s.textContent = log.sites_scanned ?? 'â'; 4754 if (c) c.textContent = log.coupons_found ?? 'â'; 4755 } else { 4756 const s = document.getElementById('job-c-log-stores'); 4757 const c = document.getElementById('job-c-log-coupons'); 4758 if (s) s.textContent = `${log.stores_scanned ?? 'â'} / ${log.stores_total ?? 'â'}`; 4759 if (c) c.textContent = log.coupons_found ?? 'â'; 4760 } 4761 4762 const errors = log.errors || []; 4763 const errBox = document.getElementById(`${prefix}-log-errors`); 4764 const errList = document.getElementById(`${prefix}-log-errors-list`); 4765 if (errBox && errList) { 4766 if (errors.length) { 4767 errBox.style.display = 'block'; 4768 errList.innerHTML = errors.map(e => `<li>${e}</li>`).join(''); 4769 } else { 4770 errBox.style.display = 'none'; 4771 } 4772 } 4773} 4774 4775function _showSiteErrors(siteName, errors) { 4776 let modal = document.getElementById('_site-errors-modal'); 4777 if (!modal) { 4778 modal = document.createElement('div'); 4779 modal.id = '_site-errors-modal'; 4780 modal.setAttribute('style','position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:9999;display:flex;align-items:center;justify-content:center'); 4781 modal.onclick = e => { if (e.target === modal) modal.remove(); }; 4782 document.body.appendChild(modal); 4783 } 4784 modal.innerHTML = ''; 4785 const box = document.createElement('div'); 4786 box.setAttribute('style','background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:24px;max-width:680px;width:90%;max-height:80vh;overflow-y:auto'); 4787 const title = document.createElement('div'); 4788 title.setAttribute('style','font-size:15px;font-weight:700;color:#fc8181;margin-bottom:16px'); 4789 title.textContent = `â ש××××ת â ${siteName} (${errors.length})`; 4790 box.appendChild(title); 4791 errors.forEach((err, i) => { 4792 const row = document.createElement('div'); 4793 row.setAttribute('style','font-size:11px;color:#ccc;padding:6px 8px;border-bottom:1px solid #222;font-family:monospace;word-break:break-all;direction:ltr;text-align:left'); 4794 row.textContent = `${i+1}. ${err}`; 4795 box.appendChild(row); 4796 }); 4797 const close = document.createElement('button'); 4798 close.setAttribute('style','margin-top:16px;padding:6px 18px;background:#333;border:none;border-radius:6px;color:#fff;cursor:pointer;font-size:12px'); 4799 close.textContent = 'ס××ר'; 4800 close.onclick = () => modal.remove(); 4801 box.appendChild(close); 4802 modal.appendChild(box); 4803} 4804 4805async function loadAutomationScans() { 4806 // Fire both requests in parallel â the toggle shouldn't sit in its "loading" state 4807 // waiting on the job-logs fetch, which is unrelated and can be slower. 4808 const jobLogsPromise = fetch('/api/admin/job-logs').then(r => r.json()).catch(() => null); 4809 const jobBConfigPromise = fetch('/api/admin/job-b-config').then(r => r.json()).catch(() => null); 4810 4811 const logs = await jobLogsPromise; 4812 if (logs) { 4813 _renderJobLog('job-b', logs.job_b); 4814 _renderJobLog('job-c', logs.job_c); 4815 } 4816 4817 const cfg = await jobBConfigPromise; 4818 if (cfg) { 4819 if (cfg.max_tokens != null) document.getElementById('cfg-max-tokens').value = cfg.max_tokens; 4820 if (cfg.max_urls_per_site != null) document.getElementById('cfg-max-urls').value = cfg.max_urls_per_site; 4821 if (cfg.il_coupon_max_pages != null) document.getElementById('cfg-il-coupon-max').value = cfg.il_coupon_max_pages;
4822 if (cfg.per_site_call_cap != null) document.getElementById('cfg-per-site-cap').value = cfg.per_site_call_cap; 4823 if (cfg.max_cost_usd != null) document.getElementById('cfg-max-cost').value = cfg.max_cost_usd; 4824 _applyJobBToggleUI(cfg.enabled !== false); 4825 updateJobBEstCost(); 4826 } else { 4827 // Fetch failed â leave it visibly "loading" rather than silently stuck on a guess. 4828 _applyJobBToggleUI(null); 4829 } 4830 4831 // Job B: load trusted coupon sites 4832 try { 4833 const r = await fetch('/api/admin/trusted-coupon-sites'); 4834 const sites = await r.json(); 4835 const tbody = document.getElementById('job-b-tbody'); 4836 document.getElementById('job-b-status').textContent = `${sites.length} ×תר××`; 4837 // Show last-run per-site details if available, otherwise show site list 4838 const lastRun = await fetch('/api/admin/job-logs').then(r=>r.json()).catch(()=>({})); 4839 const siteDetails = (lastRun.job_b?.site_details || []); 4840 4841 if (siteDetails.length) { 4842 document.getElementById('job-b-status').textContent = `${sites.length} ×תר××`; 4843 tbody.innerHTML = ''; 4844 siteDetails.forEach((s, i) => { 4845 const errList = Array.isArray(s.errors) ? s.errors : []; 4846 const tr = document.createElement('tr'); 4847 const td0 = document.createElement('td'); 4848 td0.setAttribute('style','color:var(--gray5)'); td0.textContent = i + 1; 4849 const td1 = document.createElement('td'); 4850 const b = document.createElement('b'); b.textContent = s.name || ''; 4851 const sm = document.createElement('span'); 4852 sm.setAttribute('style','display:block;font-size:10px;color:var(--gray5)'); 4853 sm.textContent = s.domain || ''; 4854 td1.appendChild(b); td1.appendChild(sm); 4855 const td2 = document.createElement('td'); 4856 const badge = document.createElement('span'); 4857 badge.setAttribute('style','font-size:11px;color:' + (s.method==='sitemap' ? 'var(--accent)' : s.method==='pagination' ? '#63b3ed' : 'var(--gray5)')); 4858 badge.textContent = s.method === 'sitemap' ? 'sitemap' : s.method === 'pagination' ? 'pagination' : 'none'; 4859 td2.appendChild(badge); 4860 const td3 = document.createElement('td'); 4861 td3.setAttribute('style','text-align:center;color:var(--gray5)'); 4862 td3.textContent = s.pages_scraped ?? 0; 4863 const td4 = document.createElement('td'); 4864 td4.setAttribute('style','text-align:center'); 4865 const bNew = document.createElement('b'); 4866 bNew.setAttribute('style','color:' + ((s.new_found > 0) ? '#ffe45e' : 'var(--gray5)')); 4867 bNew.textContent = (s.new_found > 0 ? '+' : '') + (s.new_found ?? 0); 4868 td4.appendChild(bNew); 4869 const td5 = document.createElement('td'); 4870 td5.setAttribute('style','text-align:center;color:var(--gray5)'); 4871 td5.textContent = s.skipped ?? 0; 4872 const td6 = document.createElement('td'); 4873 td6.setAttribute('style','text-align:center'); 4874 const errSpan = document.createElement('span'); 4875 if (errList.length) { 4876 errSpan.setAttribute('style','font-size:11px;color:#fc8181;cursor:pointer;text-decoration:underline dotted'); 4877 errSpan.textContent = 'â ' + errList.length; 4878 errSpan.onclick = () => _showSiteErrors(s.name, errList); 4879 } else { 4880 errSpan.setAttribute('style','font-size:11px;color:var(--gray5)'); 4881 errSpan.textContent = 'â'; 4882 } 4883 td6.appendChild(errSpan); 4884 const td7 = document.createElement('td'); 4885 td7.setAttribute('style','font-size:11px;color:' + (s.completed ? 'var(--accent)' : '#f6c90e')); 4886 td7.textContent = s.completed ? 'â ' : 'â³'; 4887 // Tokens in/out 4888 const td8 = document.createElement('td'); 4889 td8.setAttribute('style','text-align:center;font-size:11px;color:#f6ad55'); 4890 const tokIn = s.tokens_in || 0; const tokOut = s.tokens_out || 0; 4891 td8.textContent = (tokIn || tokOut) ? `${_fmtTokens(tokIn)} / ${_fmtTokens(tokOut)}` : 'â'; 4892 // Cost 4893 const td9 = document.createElement('td'); 4894 const siteCost = _calcSiteCost(tokIn, tokOut); 4895 const costColor = siteCost > 1 ? '#fc8181' : siteCost > 0.3 ? '#f6ad55' : siteCost > 0 ? '#68d391' : 'var(--gray5)'; 4896 td9.setAttribute('style',`text-align:center;font-weight:600;color:${costColor}`); 4897 td9.textContent = _fmtCost(siteCost); 4898 tr.append(td0, td1, td2, td3, td4, td5, td6, td8, td9, td7); 4899 tbody.appendChild(tr); 4900 }); 4901 // Total cost badge 4902 const totalCost = siteDetails.reduce((sum, s) => sum + _calcSiteCost(s.tokens_in || 0, s.tokens_out || 0), 0); 4903 const costEl = document.getElementById('job-b-log-cost'); 4904 if (costEl) { 4905 const costColor = totalCost > 10 ? '#fc8181' : totalCost > 5 ? '#f6ad55' : '#68d391'; 4906 costEl.textContent = totalCost > 0 ? _fmtCost(totalCost) : 'â'; 4907 costEl.style.color = totalCost > 0 ? costColor : 'var(--gray5)'; 4908 } 4909 } else { 4910 document.getElementById('job-b-status').textContent = `${sites.length} ×תר××`; 4911 tbody.innerHTML = '';
4912 (Array.isArray(sites) ? sites : []).forEach((s, i) => { 4913 const tr = document.createElement('tr'); 4914 const td0 = document.createElement('td'); td0.setAttribute('style','color:var(--gray5)'); td0.textContent = i + 1; 4915 const td1 = document.createElement('td'); td1.colSpan = 2; 4916 const b = document.createElement('b'); b.textContent = s.name || ''; 4917 const sm = document.createElement('span'); sm.setAttribute('style','display:block;font-size:10px;color:var(--gray5)'); sm.textContent = s.url || ''; 4918 td1.appendChild(b); td1.appendChild(sm); 4919 const td2 = document.createElement('td'); td2.colSpan = 6; td2.setAttribute('style','color:var(--gray5);font-size:12px'); td2.textContent = '××ת×× ×ר××¦× ×ר×ש×× × (02:00)'; 4920 const td3 = document.createElement('td'); const sp = document.createElement('span'); sp.setAttribute('style','color:#f6c90e;font-size:12px'); sp.textContent = 'â³ ××ת××'; td3.appendChild(sp); 4921 tr.append(td0, td1, td2, td3); 4922 tbody.appendChild(tr); 4923 }); 4924 } 4925 } catch(e) { 4926 console.error('[Job B]', e); 4927 document.getElementById('job-b-tbody').innerHTML = `<tr><td colspan="10" style="color:#fc8181;text-align:center;padding:12px">ש××××: ${e.message || e}</td></tr>`; 4928 } 4929 4930 // Job B history 4931 try { 4932 const jbHist = await fetch('/api/admin/job-b-history').then(r => r.json()); 4933 const histEl = document.getElementById('job-b-history'); 4934 const history = (jbHist.history || []).slice().reverse(); 4935 if (histEl) { 4936 if (!history.length) { 4937 histEl.textContent = '××× ××ס××ר×× ×¢×××× â ת××¨×©× ×××ר ×ר××¦× ×ר×ש×× × ××©×¢× 02:00'; 4938 } else { 4939 const IN_PRICE = 0.80 / 1_000_000, OUT_PRICE = 4.00 / 1_000_000; 4940 histEl.innerHTML = `<table style="width:100%;border-collapse:collapse"> 4941 <thead><tr> 4942 <th style="text-align:right;padding:6px 10px;color:var(--gray5);font-weight:500;font-size:11px;border-bottom:1px solid var(--gray3)">ת×ר××</th> 4943 <th style="text-align:center;padding:6px 10px;color:var(--gray5);font-weight:500;font-size:11px;border-bottom:1px solid var(--gray3)">×תר××</th> 4944 <th style="text-align:center;padding:6px 10px;color:var(--accent);font-weight:500;font-size:11px;border-bottom:1px solid var(--gray3)">×§×פ×× ××</th> 4945 <th style="text-align:center;padding:6px 10px;color:#fc8181;font-weight:500;font-size:11px;border-bottom:1px solid var(--gray3)">×¢××ת $</th> 4946 <th style="text-align:center;padding:6px 10px;color:var(--gray5);font-weight:500;font-size:11px;border-bottom:1px solid var(--gray3)">ס×××ס</th> 4947 </tr></thead> 4948 <tbody>${history.map(h => { 4949 const cost = ((h.tokens_in || 0) * IN_PRICE + (h.tokens_out || 0) * OUT_PRICE).toFixed(3); 4950 const statusColor = h.status === 'success' ? 'var(--accent)' : h.status === 'error' ? '#fc8181' : '#f6c90e'; 4951 const statusLabel = h.status === 'success' ? 'â ' : h.status === 'error' ? 'â' : 'â³'; 4952 return `<tr> 4953 <td style="padding:6px 10px;font-size:12px;color:var(--white)">${h.date}</td> 4954 <td style="padding:6px 10px;font-size:12px;text-align:center;color:var(--gray6)">${h.sites_scanned}</td> 4955 <td style="padding:6px 10px;font-size:12px;text-align:center;color:${h.coupons_found > 0 ? '#ffe45e' : 'var(--gray5)'}">${h.coupons_found > 0 ? '+' : ''}${h.coupons_found}</td> 4956 <td style="padding:6px 10px;font-size:12px;text-align:center;color:#fc8181">$${cost}</td> 4957 <td style="padding:6px 10px;text-align:center;color:${statusColor}">${statusLabel}</td> 4958 </tr>`; 4959 }).join('')}</tbody> 4960 </table>`; 4961 } 4962 } 4963 } catch(e) { /* non-critical */ } 4964 4965 // Job C: load store sweep stats 4966 try { 4967 const r = await fetch('/api/admin/sweep-status'); 4968 const d = await r.json(); 4969 document.getElementById('job-c-total').textContent = d.total ?? 'â'; 4970 document.getElementById('job-c-scanned-today').textContent = d.scanned_today ?? 'â'; 4971 document.getElementById('job-c-pending').textContent = d.pending ?? 'â'; 4972 document.getElementById('job-c-next').textContent = d.next_store ?? 'â'; 4973 document.getElementById('job-c-status').textContent = `${d.total ?? 0} ×× ×××ת`; 4974 const tbody = document.getElementById('job-c-tbody'); 4975 const stores = d.stores || []; 4976 if (!stores.length) { 4977 tbody.innerHTML = '<tr><td colspan="7" style="color:var(--gray5);text-align:center;padding:12px">××× × ×ª×× ××</td></tr>'; 4978 } else { 4979 tbody.innerHTML = stores.map((s, i) => { 4980 const scanned = s.last_scanned_at ? new Date(s.last_scanned_at).toLocaleStr
4980ing('he-IL') : 'â'; 4981 const isToday = s.last_scanned_at && new Date(s.last_scanned_at).toDateString() === new Date().toDateString(); 4982 const total = s.coupon_count != null ? `<b style="color:var(--accent)">${s.coupon_count}</b>` : 'â'; 4983 const before = s.before != null ? s.before : 'â'; 4984 const newF = s.new_found != null 4985 ? `<b style="color:${s.new_found > 0 ? '#ffe45e' : 'var(--gray5)'}">${s.new_found > 0 ? '+' : ''}${s.new_found}</b>` 4986 : 'â'; 4987 return `<tr> 4988 <td style="color:var(--gray5)">${i+1}</td> 4989 <td>${s.name}</td> 4990 <td style="font-size:12px;color:var(--gray5)">${scanned}</td> 4991 <td style="text-align:center">${total}</td> 4992 <td style="text-align:center;color:var(--gray5)">${before}</td> 4993 <td style="text-align:center">${newF}</td> 4994 <td><span style="color:${isToday ? 'var(--accent)' : '#f6c90e'};font-size:12px">${isToday ? 'â × ×¡×¨×§ ××××' : 'â³ ××ת××'}</span></td> 4995 </tr>`; 4996 }).join(''); 4997 } 4998 4999 // Daily history 5000 const histEl = document.getElementById('job-c-history'); 5001 const history = (d.history || []).slice().reverse(); // newest first 5002 if (histEl) { 5003 if (!history.length) { 5004 histEl.textContent = '××× ××ס××ר×× ×¢×××× â ת××¨×©× ×××ר ×ר××¦× ×ר×ש×× × ××©×¢× 02:00'; 5005 } else { 5006 histEl.innerHTML = `<table style="width:100%;border-collapse:collapse"> 5007 <thead><tr> 5008 <th style="text-align:right;padding:6px 10px;color:var(--gray5);font-weight:500;font-size:11px;border-bottom:1px solid var(--gray3)">ת×ר××</th> 5009 <th style="text-align:center;padding:6px 10px;color:var(--gray5);font-weight:500;font-size:11px;border-bottom:1px solid var(--gray3)">×× ×××ת ×©× ×¡×¨×§×</th> 5010 <th style="text-align:center;padding:6px 10px;color:var(--accent);font-weight:500;font-size:11px;border-bottom:1px solid var(--gray3)">×§×פ×× ×× ××ש××</th> 5011 <th style="text-align:center;padding:6px 10px;color:var(--gray5);font-weight:500;font-size:11px;border-bottom:1px solid var(--gray3)">ס×××ס</th> 5012 </tr></thead> 5013 <tbody>${history.map(h => { 5014 const statusColor = h.status === 'success' ? 'var(--accent)' : h.status === 'error' ? '#fc8181' : '#f6c90e'; 5015 const statusLabel = h.status === 'success' ? 'â ' : h.status === 'error' ? 'â' : 'â³'; 5016 return `<tr> 5017 <td style="padding:6px 10px;font-size:12px;color:var(--white)">${h.date}</td> 5018 <td style="padding:6px 10px;font-size:12px;text-align:center;color:var(--gray6)">${h.stores_scanned}</td> 5019 <td style="padding:6px 10px;font-size:12px;text-align:center;color:${h.coupons_found > 0 ? '#ffe45e' : 'var(--gray5)'}">${h.coupons_found > 0 ? '+' : ''}${h.coupons_found}</td> 5020 <td style="padding:6px 10px;text-align:center;color:${statusColor}">${statusLabel}</td> 5021 </tr>`; 5022 }).join('')}</tbody> 5023 </table>`; 5024 } 5025 } 5026 } catch(e) { 5027 console.error('[Job C]', e); 5028 document.getElementById('job-c-tbody').innerHTML = `<tr><td colspan="7" style="color:#fc8181;text-align:center;padding:12px">ש××××: ${e.message || e}</td></tr>`; 5029 } 5030} 5031 5032// ââ Analytics panel DOM mover âââââââââââââââââââââââââââââââââââââââââââââââââ 5033// Panels live in dashboard slot divs. When a sidebar analytics item is clicked 5034// the panel is physically moved to the dedicated view div (and back on leave). 5035const _ANALYTICS_PANEL_MAP = [ 5036 { view:'analytics-usage', panel:'analytics-panel', slot:'analytics-panel-slot' }, 5037 { view:'analytics-speed', panel:'speed-panel', slot:'speed-panel-slot' }, 5038 { view:'analytics-popup', panel:'popup-panel', slot:'popup-panel-slot' }, 5039 { view:'analytics-extension', panel:'ext-panel', slot:'ext-panel-slot' }, 5040]; 5041function _returnAnalyticsToDash() { 5042 _ANALYTICS_PANEL_MAP.forEach(({panel, slot}) => { 5043 const p=document.getElementById(panel), s=document.getElementById(slot); 5044 if (p && s && p.parentElement !== s) s.appendChild(p); 5045 }); 5046} 5047 5048function setAdminView(btn,view){ 5049 document.querySelectorAll('.sidebar-item').forEach(b=>b.classList.remove('on')); 5050 btn.classList.add('on'); 5051 5052 const ALL_VIEWS = ['dashboard','coupons','sources','coupon-sites','automation-scans','web','instagram','newsletters','affiliate','blocked-sites','trusted-international','
5052stores','store-categories','pending','no-url','users','settings','logs','hide-mechanism','expired','search-anim','mole','minigames','debug','null-domain','orphan-coupons','analytics-usage','analytics-speed','analytics-popup','analytics-extension','analytics-buttons','store-pages-demo','home-demo','contact-messages','no-origin','coupon-preview','user-profiles','search-intel','demand-gaps','traffic-sources','geo-map','device-stats','js-errors']; 5053 5054 // Return any displaced analytics panels back to dashboard slots 5055 _returnAnalyticsToDash(); 5056 5057 ALL_VIEWS.forEach(v=>{ 5058 const el=document.getElementById('view-'+v); 5059 if(el)el.style.display=v===view?'block':'none'; 5060 }); 5061 _activeAdminView = view; 5062 5063 // For analytics views: move the panel node into the dedicated view div 5064 const aEntry = _ANALYTICS_PANEL_MAP.find(e=>e.view===view); 5065 if (aEntry) { 5066 const p = document.getElementById(aEntry.panel); 5067 const viewEl = document.getElementById('view-'+view); 5068 if (p && viewEl) viewEl.appendChild(p); 5069 _loadAnalyticsFromServer().then(() => { 5070 if (view==='analytics-usage') { try{loadAnalytics();}catch(e){} } 5071 if (view==='analytics-speed') { try{loadSpeedAnalytics();}catch(e){} } 5072 if (view==='analytics-popup') { try{loadPopupAnalytics();}catch(e){} } 5073 if (view==='analytics-extension') { try{loadExtensionAnalytics();}catch(e){} } 5074 }); 5075 } 5076 if (view==='user-profiles') { try{loadUserProfiles();}catch(e){console.error(e)} } 5077 if (view==='search-intel') { try{loadSearchIntel();}catch(e){console.error(e)} } 5078 if (view==='demand-gaps') { try{loadDemandGaps();}catch(e){console.error(e)} } 5079 if (view==='traffic-sources') { try{loadTrafficSources();}catch(e){console.error(e)} } 5080 if (view==='geo-map') { try{loadGeoMap();}catch(e){console.error(e)} } 5081 if (view==='device-stats') { try{loadDeviceStats();}catch(e){console.error(e)} } 5082 if (view==='js-errors') { try{loadJsErrors();}catch(e){console.error(e)} } 5083 if (view==='analytics-buttons') { try{loadButtonClickAnalytics();}catch(e){} } 5084 5085 if(view==='logs') loadLogs(); 5086 if(view==='sources') { initSourcesView(); loadUserbotStatus(); } 5087 if(view==='coupon-sites') loadCouponSites(); 5088 if(view==='automation-scans') loadAutomationScans(); 5089 if(view==='web') initWebView(); 5090 if(view==='instagram') initInstagramView(); 5091 if(view==='users') _loadUsers();
5092 if(view==='pending') loadPendingView(); 5093 if(view==='newsletters') loadNewsletterStores(); 5094 if(view==='affiliate') _affRender(); 5095 if(view==='no-url') loadNoUrlCoupons(); 5096 if(view==='expired') renderExpiredDemo(); 5097 if(view==='mole') wamStart(); 5098 if(view==='search-anim') _mgInitTips(); 5099 if(view==='minigames') demoGameStart(); 5100 if(view==='debug') initDebugView(); 5101 if(view==='stores') loadStoresView(); 5102 if(view==='null-domain') loadNullDomainStores(); 5103 if(view==='no-origin') loadNoOriginStores(); 5104 if(view==='coupon-preview') loadCouponPreview(); 5105 if(view==='orphan-coupons') loadOrphanCoupons(); 5106 if(view==='blocked-sites') loadBlockedSites(); 5107 if(view==='trusted-international') loadTrustedInternational(); 5108 // Update mobile title and close drawer 5109 const titleEl = document.getElementById('mob-view-title'); 5110 if (titleEl) titleEl.textContent = _VIEW_TITLES[view] || view; 5111 closeMobMenu(); 5112} 5113 5114// ââ Hide Mechanism view JS ââââââââââââââââââââââââââââââââââââââââââââââââââââ 5115// 1 â Slide Banner: green strip lifts up to reveal code 5116function hmRevealStrip(el, code) { 5117 if (el.dataset.r) { navigator.clipboard?.writeText(code).catch(()=>{}); showToast('â ××עתק!'); return; } 5118 el.dataset.r = '1'; 5119 const strip = el.querySelector('.hm-strip'); 5120 strip.style.transform = 'translateY(-110%)'; 5121 strip.style.opacity = '0'; 5122 strip.style.pointerEvents = 'none'; 5123 navigator.clipboard?.writeText(code).catch(()=>{}); 5124 setTimeout(() => showToast('â ××עתק!'), 200); 5125} 5126 5127// 2 â Blur Focus: heavy blur â hover partially focuses â click fully reveals 5128function hmRevealBlurFocus(el, code) { 5129 if (el.dataset.r) { navigator.clipboard?.writeText(code).catch(()=>{}); showToast('â ××עתק!'); return; } 5130 el.dataset.r = '1'; 5131 const blurEl = el.querySelector('.hm-blur-code'); 5132 el.classList.remove('hm-blur-wrap2'); // stop hover rule 5133 blurEl.style.filter = 'blur(0)'; 5134 el.style.borderColor = 'rgba(0,232,122,.5)'; 5135 el.style.cursor = 'pointer'; 5136 navigator.clipboard?.writeText(code).catch(()=>{}); 5137 showToast('â ××עתק!'); 5138} 5139 5140// 3 â Split Doors: two panels slide apart like elevator doors 5141function hmRevealDoors(el, code) { 5142 if (el.dataset.r) { navigator.clipboard?.writeText(code).catch(()=>{}); showToast('â ××עתק!'); return; } 5143 el.dataset.r = '1'; 5144 el.querySelector('.hm-door-l').style.transform = 'translateX(-101%)'; 5145 el.querySelector('.hm-door-r').style.transform = 'translateX(101%)'; 5146 navigator.clipboard?.writeText(code).catch(()=>{}); 5147 setTimeout(() => showToast('â ××עתק!'), 220); 5148} 5149 5150// 4 â Night Vision: scanline overlay fades out 5151function hmRevealScan(el, code) { 5152 if (el.dataset.r) { navigator.clipboard?.writeText(code).catch(()=>{}); showToast('â ××עתק!'); return; } 5153 el.dataset.r = '1'; 5154 const ov = el.querySelector('.hm-scan-overlay'); 5155 ov.style.opacity = '0'; 5156 navigator.clipboard?.writeText(code).catch(()=>{}); 5157 setTimeout(() => showToast('â ××עתק!'), 180); 5158} 5159 5160// 5 â Stamp Lift: diagonal stamp rotates and flies off 5161function hmRevealStamp(el, code) { 5162 if (el.dataset.r) { navigator.clipboard?.writeText(code).catch(()=>{}); showToast('â ××עתק!'); return; } 5163 el.dataset.r = '1'; 5164 const stamp = el.querySelector('.hm-stamp'); 5165 stamp.style.animation = 'hm-stamp-out .5s cubic-bezier(.4,0,.2,1) forwards'; 5166 el.querySelector('.hm-code-box').style.borderColor = 'rgba(0,232,122,.4)'; 5167 navigator.clipboard?.writeText(code).catch(()=>{}); 5168 setTimeout(() => showToast('â ××עתק!'), 300); 5169} 5170 5171// 6 â Eye Toggle: password-style eye icon reveals code 5172function hmRevealEye(el, code) { 5173 if (el.dataset.r) { navigator.clipboard?.writeText(code).catch(()=>{}); showToast('â ××עתק!'); return; } 5174 el.dataset.r = '1'; 5175 el.querySelector('.hm-eye-hidden').style.display = 'none'; 5176 const rev = el.querySelector('.hm-eye-revealed'); 5177 rev.style.display = 'block'; 5178 el.classList.remove('cc-highlight'); void el.offsetWidth; el.classList.add('cc-highlight'); 5179 const eye = el.querySelector('.hm-eye-btn'); 5180 eye.style.color = 'var(--accent)'; 5181 // swap to eye-off icon
5182 eye.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>'; 5183 navigator.clipboard?.writeText(code).catch(()=>{}); 5184 showToast('â ××עתק!'); 5185} 5186 5187// Button-style demos â reveal on click with full-field pop 5188function hmRevealBtn(el, code) { 5189 if (el.dataset.r) { navigator.clipboard?.writeText(code).catch(()=>{}); showToast('â ××עתק!'); return; } 5190 el.dataset.r = '1'; 5191 // Solid Fill: switch to dark bg + green border so text is readable 5192 if (el.classList.contains('hm-btn-solid')) { 5193 el.style.background = 'var(--gray2)'; 5194 el.style.border = '1px solid rgba(0,232,122,0.5)'; 5195 el.style.filter = ''; 5196 } 5197 const masked = el.querySelector('.hm-btn-masked'); 5198 masked.innerHTML = `<span style="color:#00e87a;font-weight:800;font-size:16px;letter-spacing:2px;font-family:monospace">${code}</span>`; 5199 const eye = el.querySelector('.hm-btn-eye'); 5200 if (eye) eye.remove(); 5201 // Full-field green pop 5202 el.classList.remove('cc-highlight'); void el.offsetWidth; el.classList.add('cc-highlight'); 5203 navigator.clipboard?.writeText(code).catch(()=>{}); 5204 showToast('â ××עתק!'); 5205} 5206 5207// Shine effects â play on click, reset then re-apply class 5208// BOX_CLASSES: applied to the .cc-code container (wrapper-level) 5209// Everything else goes on the <span> text node 5210function hmShinePlay(n) { 5211 const box = document.getElementById('hs'+n); 5212 const txt = document.getElementById('hs'+n+'-t'); 5213 if (!box || !txt) return; 5214 // Reset 5215 txt.className = ''; 5216 box.className = 'cc-code'; 5217 void txt.offsetWidth; // force reflow 5218 // Box-level classes (wrapper animation) 5219 const BOX_CLASSES = { 3:'hm-shine-3-wrap', 10:'hm-shine-10-wrap' }; 5220 if (BOX_CLASSES[n]) box.classList.add(BOX_CLASSES[n]); 5221 else txt.classList.add('hm-shine-'+n); 5222} 5223 5224// ââ Minigames view JS âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 5225let _mgTipIdx = 0, _mgTipTimer = null, _mgLogTimer = null, _mgLogStart = null; 5226const _MG_TIPS = [ 5227 { icon:'ð³', text:'×ר×××¡× ×שר×× ×ס××××× × ××ª× ×× <strong>×§×ש××§ × ×סף</strong> ×¢× ×§× ××× ×¢× ×§×× ×§×פ×× â ת××× ×©××× ×××××§!' }, 5228 { icon:'ð', text:'×ש×ר ××צר×× ××¢××× ×××-×××××× â ×ר×× ×× ×××ת ש××××ת <strong>×§×× ×× ××</strong> ××××"× ××× ××××ר ×ת ××§× ×××.' }, 5229 { icon:'ð±', text:'×פ××קצ××ת ×©× ×× ×××ת ×שר××××ת ××× <strong>KSP ×-Bug</strong> ×צ××¢×ת ×פע××× ××צע×× ×××¢×××× ××פ××קצ××.' }, 5230 { icon:'â°', text:'<strong>ש××©× ×צ×ר×××</strong> ××× ×××× ××× ××× ××צ×× ×§×פ×× ×× ×שר×××× â ר×× ××× ×××ת ×פרס××ת ×ס××£ ×ש×××¢.' }, 5231 { icon:'ð', text:'××¢×§× ×××¨× ×¢×¨××¦× ××××¨× ×©× ××× ×¤×××× ×¡×¨×× ×שר×××× ×× ×× ×פע××× ×§×פ×× ×× <strong>×× ×פ×רס×××</strong>.' }, 5232]; 5233function _mgInitTips() { 5234 _mgTipIdx = 0; 5235 if (_mgTipTimer) clearInterval(_mgTipTimer); 5236 _mgTipTimer = setInterval(() => { _mgTipIdx = (_mgTipIdx + 1) % _MG_TIPS.length; _mgShowTip(_mgTipIdx); }, 4000); 5237 _mgShowTip(0); 5238 // log timer 5239 if (_mgLogTimer) clearInterval(_mgLogTimer); 5240 _mgLogStart = Date.now(); 5241 _mgLogTimer = setInterval(() => { 5242 const el = document.getElementById('mg-log-timer'); 5243 if (el) el.textContent = ((Date.now() - _mgLogStart) / 1000).toFixed(1) + 's'; 5244 }, 100); 5245} 5246function _mgShowTip(i) { 5247 const icon = document.getElementById('mg-tip-icon'); 5248 const text = document.getElementById('mg-tip-text'); 5249 const bar = document.getElementById('mg-tip-bar'); 5250 if (!icon || !text) return; 5251 icon.textContent = _MG_TIPS[i].icon; 5252 text.innerHTML = _MG_TIPS[i].text; 5253 for (let j = 0; j < 5; j++) { 5254 const d = document.getElementById('mg-td' + j); 5255 if (d) d.style.background = j === i ? 'var(--accent)' : 'var(--gray3)'; 5256 }
5257 if (bar) { bar.style.animation = 'none'; requestAnimationFrame(() => { bar.style.animation = 'mg-progress 4s linear forwards'; }); } 5258} 5259function mgReplayLog() { 5260 const lines = document.querySelectorAll('#mg-log1 .mg-log-line'); 5261 lines.forEach(l => { l.style.opacity = 0; l.style.animation = 'none'; }); 5262 _mgLogStart = Date.now(); 5263 requestAnimationFrame(() => { lines.forEach(l => { l.style.animation = ''; }); }); 5264} 5265 5266// ââ Demo game (real mini-game preview) âââââââââââââââââââââââââââââââââââââââ 5267let _demoTipInterval = null, _demoTipIdx = -1; 5268function demoGameStart() { 5269 const skelGrid = document.getElementById('demo-skel-grid'); 5270 if (skelGrid) skelGrid.innerHTML = Array(6).fill(0).map(_skelCard).join(''); 5271 _demoTipIdx = _randomTipIdx(); 5272 _demoShowTip(_demoTipIdx); 5273 clearInterval(_demoTipInterval); 5274 _demoTipInterval = setInterval(() => { _demoTipIdx = _randomTipIdx(); _demoShowTip(_demoTipIdx); }, 16000); 5275} 5276function demoGameStop() { 5277 clearInterval(_demoTipInterval); _demoTipInterval = null; 5278 const g = document.getElementById('demo-skel-grid'); 5279 if (g) g.innerHTML = ''; 5280 const icon = document.getElementById('demo-tip-icon'); 5281 const text = document.getElementById('demo-tip-text'); 5282 const bar = document.getElementById('demo-tip-bar'); 5283 if (icon) icon.textContent = 'ð¡'; 5284 if (text) text.textContent = '×××¥ ××¤×¢× ××× ×ר××ת ×ת ××ש××§'; 5285 if (bar) { bar.style.transition = 'none'; bar.style.width = '0%'; } 5286} 5287function _demoShowTip(i) { 5288 const icon = document.getElementById('demo-tip-icon'); 5289 const text = document.getElementById('demo-tip-text'); 5290 const bar = document.getElementById('demo-tip-bar'); 5291 if (!icon || !text) return; 5292 icon.textContent = _TIPS[i].icon; 5293 text.textContent = _TIPS[i].text; 5294 if (bar) { 5295 bar.style.transition = 'none'; bar.style.width = '0%'; 5296 requestAnimationFrame(() => requestAnimationFrame(() => { 5297 bar.style.transition = 'width 16s linear'; bar.style.width = '100%'; 5298 })); 5299 } 5300} 5301 5302// ââ Whack-a-Mole game ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 5303let _wamInterval = null; 5304const _WAM_EMOJIS = ['ð¤','ð¸','ð·ï¸','ð','ð°','ðï¸','ð«','ð']; 5305const _WAM_COLS = 4, _WAM_ROWS = 2, _WAM_TOTAL = _WAM_COLS * _WAM_ROWS; 5306let _wamScore = 0, _wamActive = -1, _wamCells = []; 5307 5308function wamStart() { 5309 wamStop(); 5310 _wamScore = 0; 5311 const scoreEl = document.getElementById('wam-view-score'); 5312 const grid = document.getElementById('wam-view-grid'); 5313 if (!grid) return; 5314 if (scoreEl) scoreEl.textContent = '0 × ×§×××ת'; 5315 grid.innerHTML = ''; 5316 _wamCells = Array.from({ length: _WAM_TOTAL }, (_, i) => { 5317 const btn = document.createElement('button'); 5318 btn.style.cssText = 'font-size:32px;background:var(--gray3);border:2px solid var(--gray4);border-radius:12px;padding:10px;cursor:pointer;transition:transform 0.1s;width:70px;height:70px;display:flex;align-items:center;justify-content:center;'; 5319 btn.textContent = 'ð³ï¸'; 5320 btn.onclick = () => { 5321 if (_wamActive === i) { 5322 _wamScore++; 5323 if (scoreEl) scoreEl.textContent = _wamScore + ' × ×§×××ת'; 5324 btn.style.transform = 'scale(1.3)'; 5325 setTimeout(() => { btn.style.transform = ''; }, 150); 5326 _wamActive = -1; 5327 _wamCells.forEach(c => { c.textContent = 'ð³ï¸'; c.style.borderColor = 'var(--gray4)'; }); 5328 } 5329 }; 5330 grid.appendChild(btn); 5331 return btn; 5332 }); 5333 _wamInterval = setInterval(() => { 5334 if (_wamActive >= 0) { _wamCells[_wamActive].textContent = 'ð³ï¸'; _wamCells[_wamActive].style.borderColor = 'var(--gray4)'; } 5335 _wamActive = Math.floor(Math.random() * _WAM_TOTAL); 5336 _wamCells[_wamActive].textContent = _WAM_EMOJIS[Math.floor(Math.random() * _WAM_EMOJIS.length)]; 5337 _wamCells[_wamActive].style.borderColor = 'var(--accent)'; 5338 }, 900); 5339} 5340 5341function wamStop() { 5342 if (_wamInterval) { clearInterval(_wamInterval); _wamInterval = null; } 5343 _wamActive = -1;
5344 _wamCells.forEach(c => { c.textContent = 'ð³ï¸'; c.style.borderColor = 'var(--gray4)'; }); 5345} 5346 5347function openMobMenu() { 5348 document.getElementById('admin-sidebar').classList.add('mob-open'); 5349 document.getElementById('mob-backdrop').classList.add('show'); 5350 document.body.style.overflow = 'hidden'; 5351} 5352 5353function closeMobMenu() { 5354 document.getElementById('admin-sidebar').classList.remove('mob-open'); 5355 document.getElementById('mob-backdrop').classList.remove('show'); 5356 document.body.style.overflow = ''; 5357} 5358 5359// ââ Feature 1: Pending Review view âââââââââââââââââââââââââââââââââââââââââââ 5360 5361let _pendingGeo = 'all'; 5362let _pendingAllData = []; 5363 5364function _setPendingGeo(geo, btn) { 5365 _pendingGeo = geo; 5366 ['all','il','int'].forEach(g => { 5367 const el = document.getElementById(`pending-geo-${g}`); 5368 if (el) { el.style.background = g===geo ? 'var(--accent)' : ''; el.style.color = g===geo ? '#000' : ''; el.classList.toggle('btn-ghost', g!==geo); } 5369 }); 5370 _renderPendingTable(); 5371} 5372 5373function _isPendingIL(domain) { 5374 return domain.endsWith('.co.il') || domain.endsWith('.il') || 5375 ['yemama','10bis','shufersal','ksp','castro','golf','bug','jemix','couponet','icoupons', 5376 'secretcoupon','israelicoupons','israelcoupon','groo','buyme','influ','aliclub','pepper', 5377 'dealtas','dealhunter','lastprice','mishloha','gett','isrotel','ivory','zer4u'].some(k => domain.includes(k)); 5378} 5379 5380function _getPendingSourceType(domain) { 5381 const d = domain.toLowerCase(); 5382 if (d.includes('t.me') || d.includes('telegram') || d.includes('tg.')) 5383 return { icon:'ð£', label:'×××ר×', color:'#7289da', bg:'rgba(114,137,218,0.12)' }; 5384 if (['coupon','deal','promo','discount','code','offer','×§×פ××','××צע','×ס×××','saving'].some(k => d.includes(k))) 5385 return { icon:'ð·ï¸', label:'×תר ×§×פ×× ××', color:'var(--accent2)', bg:'rgba(255,228,94,0.1)' }; 5386 if (d.endsWith('.co.il') || d.endsWith('.il') || _isPendingIL(d)) 5387 return { icon:'ð®ð±', label:'×תר ×שר×××', color:'var(--accent)', bg:'rgba(0,232,122,0.08)' }; 5388 if (['blog','news','article','post','review','magazine'].some(k => d.includes(k))) 5389 return { icon:'ð°', label:'×××× / ××××', color:'var(--gray6)', bg:'rgba(150,150,150,0.1)' }; 5390 if (['facebook','instagram','twitter','reddit','tiktok','youtube','pinterest','whatsapp'].some(k => d.includes(k))) 5391 return { icon:'ð±', label:'רשת ××רת×ת', color:'#a78bfa', bg:'rgba(167,139,250,0.1)' }; 5392 if (['amazon','ebay','aliexpress','rakuten','walmart','target'].some(k => d.includes(k))) 5393 return { icon:'ð', label:'×× ×ת ××× ×××××ת', color:'#f97316', bg:'rgba(249,115,22,0.1)' }; 5394 return { icon:'â', label:'×× ×××ר', color:'var(--gray4)', bg:'rgba(100,100,100,0.08)' }; 5395} 5396 5397function _renderPendingTable() { 5398 const tbody = document.getElementById('pending-tbody'); 5399 const titleEl = document.getElementById('pending-table-title'); 5400 if (!tbody) return; 5401 5402 const filtered = _pendingAllData.filter(d => { 5403 if (_pendingGeo === 'il') return _isPendingIL(d.domain); 5404 if (_pendingGeo === 'int') return !_isPendingIL(d.domain); 5405 return true; 5406 }); 5407 5408 const total = _pendingAllData.reduce((s, d) => s + d.count, 0); 5409 const filtTotal = filtered.reduce((s, d) => s + d.count, 0); 5410 if (titleEl) titleEl.textContent = `${filtered.length} ×××××× ×× Â· ${filtTotal} ×§×פ×× ×× ×××××§×${_pendingGeo!=='all' ? ` (×ת×× ${_pendingAllData.length} / ${total})` : ''}`; 5411 5412 if (!filtered.length) { 5413 tbody.innerHTML = `<tr><td colspan="7" style="color:var(--gray5);text-align:center;padding:24px">××× ×§×פ×× ×× ×××××§× ð</td></tr>`; 5414 return; 5415 } 5416 5417 tbody.innerHTML = filtered.map((d, idx) => { 5418 const safeDomain = d.domain.replace(/'/g, "\\'"); 5419 const href = d.url || `https://${d.domain}`; 5420 const codesHtml = (d.sample_codes || []).map(c => 5421 `<span style="font-family:monospace;font-size:11px;background:var(--gray3);border:1px dashed var(--gray4);border-radius:4px;padding:2px 7px;letter-spacing:1px;color:var(--gray6);direction:ltr;display:inline-block;margin:1px">${c}</span>` 5422 ).join(' '); 5423 const storesHtml = (d.stores || []).slice(0,4).join(', '); 5424 const ilFlag = _isPendingIL(d.domain) ? 'ð®ð± ' : 'ð '; 5425 const srcType = _getPendingSourceType(d.domain); 5426 return `<tr> 5427 <td style="color:var(--gray5);font-size:11px;text-align:center">${idx + 1}</td> 5428 <td class="td-store"><a href="${href}" target="_blank" style="color:var(--accent);text-decoration:none">${ilFlag}${d.domain}</a></td> 5429 <td style="text-align:center"><span style="display:inline-flex;align-items:center;gap:4px;background:${srcType.bg};color:${srcType.color};border-radius:20px;padding:2px 9px;font-size:11px;white-space:nowrap">${srcType.icon} ${srcType.label}</span></td> 5430 <td style="text-align:center;font-weight:600;color:var(--white)">${d.count}</td> 5431 <td style="font-size:12px;color:var(--gray6)">${storesHtml || 'â'}</td> 5432 <td>${codesHtml || 'â'}</td> 5433 <td><div class="td-actions"> 5434 <button class="td-btn" style="color:#00e87a" onclick="approvePendingSource('${safeDomain}',this)">â ×שר</button> 5435 <button class="td-btn danger" onclick="rejectPendingSource('${safeDomain}',this)">ð ×××</button> 5436 </div></td> 5437 </tr>`; 5438 }).join(''); 5439} 5440 5441async function loadPendingView() {
5442 const tbody = document.getElementById('pending-tbody'); 5443 const titleEl = document.getElementById('pending-table-title'); 5444 if (!tbody) return; 5445 tbody.innerHTML = `<tr><td colspan="7" style="color:var(--gray5);text-align:center;padding:24px">ð ס×רק ××§×ר×ת ××ש××...</td></tr>`; 5446 // Auto-backfill silently â reclassify any unknowns from unapproved domains 5447 try { await fetch('/api/admin/backfill-pending', { method: 'POST' }); } catch(_) {} 5448 tbody.innerHTML = `<tr><td colspan="7" style="color:var(--gray5);text-align:center;padding:24px">×××¢×...</td></tr>`; 5449 try { 5450 const res = await fetch('/api/admin/pending-sources'); 5451 _pendingAllData = await res.json(); 5452 // Update sidebar badge with total 5453 const badge = document.getElementById('src-pending-count'); 5454 const total = _pendingAllData.reduce((s, d) => s + d.count, 0); 5455 if (badge) badge.textContent = total || '0'; 5456 _renderPendingTable(); 5457 } catch(e) { 5458 tbody.innerHTML = `<tr><td colspan="7" style="color:#ff6060;text-align:center;padding:24px">ש×××× ×××¢×× ×ª × ×ª×× ××</td></tr>`; 5459 } 5460} 5461 5462async function approvePendingSource(domain, btn) { 5463 btn.disabled = true; 5464 btn.textContent = '...'; 5465 try { 5466 const res = await fetch('/api/admin/approve-source', { 5467 method: 'POST', 5468 headers: {'Content-Type':'application/json'}, 5469 body: JSON.stringify({domain}) 5470 }); 5471 const d = await res.json(); 5472 showToast(`â ×××©×¨× ${d.updated} ×§×פ×× ×× ×-${domain}`); 5473 loadPendingView(); 5474 } catch(e) { 5475 showToast('â ש×××× ×××ש×ר ××§×ר'); 5476 btn.disabled = false; 5477 btn.textContent = 'â ×שר'; 5478 } 5479} 5480 5481async function rejectPendingSource(domain, btn) { 5482 if (!confirm(`×××××§ ×ת ×× ××§×פ×× ×× ×-${domain}?`)) return; 5483 btn.disabled = true; 5484 btn.textContent = '...'; 5485 try { 5486 const res = await fetch('/api/admin/reject-source', { 5487 method: 'POST', 5488 headers: {'Content-Type':'application/json'}, 5489 body: JSON.stringify({domain}) 5490 }); 5491 const d = await res.json(); 5492 showToast(`ð × ×××§× ${d.deleted} ×§×פ×× ×× ×-${domain}`); 5493 loadPendingView(); 5494 } catch(e) { 5495 showToast('â ש×××× ×××××ת ××§×ר'); 5496 btn.disabled = false; 5497 btn.textContent = 'ð ×××'; 5498 } 5499} 5500 5501// ââ No-URL coupons view ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 5502 5503let _noUrlData = []; 5504let _noUrlPage = 0; 5505const _noUrlPerPg = 25; 5506let _noUrlGeo = 'all'; // 'all' | 'il' | 'intl' 5507let _noUrlExpanded = new Set(); // set of store names that are expanded 5508 5509// Detect IL vs INTL from source URLs of the store's coupons 5510function _noUrlIsIL(r) { 5511 for (const c of (r.coupons || [])) { 5512 const src = c.source || ''; 5513 const url = src.includes('|') ? src.split('|').slice(1).join('|') : src; 5514 try { 5515 const host = url.startsWith('http') ? new URL(url).hostname : url; 5516 if (/\.co\.il|\.org\.il|\.net\.il|\.ac\.il|\.gov\.il/.test(host)) return true; 5517 } catch(e) {} 5518 if (/\.co\.il|\.org\.il|\.net\.il/.test(src)) return true; 5519 } 5520 return false; 5521} 5522 5523function _noUrlFilteredData() { 5524 if (_noUrlGeo === 'il') return _noUrlData.filter(r => _noUrlIsIL(r)); 5525 if (_noUrlGeo === 'intl') return _noUrlData.filter(r => !_noUrlIsIL(r)); 5526 return _noUrlData; 5527} 5528 5529// Extract the first real http(s) URL from any source format. 5530// Handles: [secondary]|url, /Web: url, Telegram:name|url, bare url, etc. 5531function _noUrlParseUrl(src) { 5532 if (!src) return ''; 5533 // Telegram â t.me link 5534 const tg = src.match(/https?:\/\/t\.me\/[^\s\]|]+/); 5535 if (tg) return tg[0]; 5536 // Any other https?:// URL in the string 5537 const m = src.match(/https?:\/\/[^\s\]|,]+/); 5538 return m ? m[0].replace(/[)\]>'"]+$/, '') : ''; 5539} 5540 5541// Aggregator domains â source URLs from these are NOT the target store 5542const _AGGREGATOR_HOSTS = new Set([ 5543 'dealspotr.com','hotdeals.com','moneysaveme.com','promocode.co.il', 5544 'coupert.com','couponfollow.com','rabato.com','israelcoupon.co.il', 5545 'tiendeo.co.il','retailmenot.com','groupon.com','slickdeals.net', 5546 'coupons.com','vouchercodes.co.uk','picodi.com','zap.co.il', 5547]); 5548 5549// Known aggregator patterns: extract store slug from URL path â suggest real domain 5550const _AGGREGATOR_PATTERNS = [ 5551 { re: /dealspotr\.com\/(?:#!\/)?@?([^/?#\s]+)/i, suffix: '.com' }, 5552 { re: /hotdeals\.com\/(?:coupons|stores?)\/([^/?#\s]+)/i, suffix: '.com' }, 5553 { re: /moneysaveme\.com\/store\/([^/?#\s]+)/i, suffix: '.com' }, 5554 { re: /promocode\.co\.il\/(?:stores?\/)?([^/?#\s]+)/i, suffix: '.co.il' }, 5555 { re: /coupert\.com\/(?:[a-z]{2}\/)?([^/?#\s]+)/i, suffix: '.com' }, 5556 { re: /couponfollow\.com\/([^/?#\s]+)-coupons/i, suffix: '.com' }, 5557 { re: /rabato\.com\/(?:il\/)?([^/?#\s]+)/i, suffix: '.com' }, 5558 { re: /israelcoupon\.co\.il\/(?:coupon|store)\/([^/?#\s]+)/i, suffix: '.co.il' }, 5559]; 5560 5561function _noUrlGuessDomain(coupons) { 5562 // 1. If source URLs are direct store pages (not aggregators), use the most common host 5563 const directCount = {}; 5564 for (const c of (coupons || [])) { 5565 const url = _noUrlParseUrl(c.source || ''); 5566 if (!url) continue; 5567 try { 5568 const host = new URL(url).hostname.replace(/^www\./, ''); 5569 if (host && !_AGGREGATOR_HOSTS.has(host) && !host.includes('t.me') && host.includes('.')) 5570 directCount[host] = (directCount[host] || 0) + 1; 5571 } catch(e) {} 5572 } 5573 const top = Object.entries(directCount).sort((a,b) => b[1]-a[1])[0]; 5574 if (top) return top[0]; // â direct source domain wins (e.g. jemix.co.il for Pandezzz) 5575 5576 // 2. Extract store slug from known aggregator URL patterns 5577 for (const c of (coupons || [])) { 5578 const url = _noUrlParseUrl(c.source || ''); 5579 for (const { re, suffix } of _AGGREGATOR_PATTERNS) { 5580 const m = url.match(re); 5581 if (m && m[1]) { 5582 const slug = m[1].replace(/-coupons?$/i,'').replace(/-/g,'').toLowerCase(); 5583 if (slug.length > 2) return slug + suffix; 5584 } 5585 } 5586 } 5587 return ''; 5588} 5589 5590function _noUrlSrcDisplay(src) { 5591 if (!src) return '<span style="color:var(--gray4)">â</span>'; 5592 const url = _noUrlParseUrl(src); 5593 if (!url) { 5594 if (/^Telegram:/i.test(src)) 5595 return `<span style="color:#29b6f6;font-size:11px">ð¨ ${src.replace(/^Telegram:/i,'').split('|')[0].trim()}</span>`;
5596 return `<span style="color:var(--gray5);font-size:11px">${src.slice(0,60)}</span>`; 5597 } 5598 const isTg = url.includes('t.me/'); 5599 let domain = url; 5600 try { domain = new URL(url).hostname.replace(/^www\./, ''); } catch(e) {} 5601 return `<a href="${url}" target="_blank" title="${url}" style="color:${isTg?'#29b6f6':'var(--accent)'};font-size:11px;white-space:nowrap">${isTg?'ð¨ ':''}${domain}</a>`; 5602} 5603 5604async function loadNullDomainStores() { 5605 const tbody = document.getElementById('nulldomain-tbody'); 5606 if (!tbody) return; 5607 tbody.innerHTML = `<tr><td colspan="9" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr>`; 5608 const selAll = document.getElementById('nd-select-all'); 5609 const delBtn = document.getElementById('nd-delete-btn'); 5610 if (selAll) selAll.checked = false; 5611 if (delBtn) delBtn.style.display = 'none'; 5612 try { 5613 const res = await fetch('/api/admin/stores/null-domain'); 5614 const data = await res.json(); 5615 const badge = document.getElementById('src-nulldomain-count'); 5616 if (badge) badge.textContent = data.length || '0'; 5617 if (!data.length) { 5618 tbody.innerHTML = `<tr><td colspan="9" style="text-align:center;color:var(--accent);padding:28px">â ×× ××× ×××ת ×××××ת ××××××</td></tr>`; 5619 return; 5620 } 5621 tbody.innerHTML = data.map((s, i) => { 5622 const aliases = (s.aliases || []).join(', ') || 'â'; 5623 const nameVal = s.name.replace(/"/g,'"'); 5624 const heVal = (s.store_he || '').replace(/"/g,'"'); 5625 return `<tr data-store-id="${s.id}"> 5626 <td style="text-align:center"> 5627 <input type="checkbox" class="nd-cb" data-id="${s.id}" onchange="_ndCbChanged()" 5628 style="cursor:pointer;width:15px;height:15px;accent-color:var(--accent)"> 5629 </td> 5630 <td style="color:var(--gray5);font-size:12px">${i + 1}</td> 5631 <td> 5632 <input id="nd-name-${s.id}" value="${nameVal}" placeholder="×©× ××× ×××ת" dir="ltr" 5633 style="width:100%;background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:5px 8px;font-size:12px;color:var(--white);outline:none"> 5634 </td> 5635 <td> 5636 <input id="nd-he-${s.id}" value="${heVal}" placeholder="×©× ××¢×ר×ת" dir="rtl" 5637 style="width:100%;background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:5px 8px;font-size:12px;color:var(--white);outline:none"> 5638 </td> 5639 <td style="text-align:center;color:var(--gray5)">${s.coupon_count || 0}</td> 5640 <td style="font-size:12px;color:var(--gray5);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${aliases}">${aliases}</td> 5641 <td style="font-size:11px;color:var(--gray5);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(s.sample_source||'').replace(/"/g,'"')}">${s.sample_source ? _ndFormatSource(s.sample_source) : 'â'}</td> 5642 <td> 5643 <input id="nd-domain-${s.id}" type="text" placeholder="castro.com" dir="ltr" 5644 style="width:100%;background:var(--gray2);border:1px solid #7c2d12;border-radius:6px;padding:5px 8px;font-size:12px;color:var(--white);outline:none"> 5645 </td> 5646 <td style="display:flex;gap:5px;align-items:center"> 5647 <button onclick="_ndSave('${s.id}')" 5648 style="padding:5px 10px;background:var(--accent);color:#000;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap"> 5649 ש××ר 5650 </button> 5651 <button onclick="_ndShowCoupons('${s.id}','${s.name.replace(/'/g,"\\'")}')" 5652 style="padding:5px 8px;background:var(--gray2);color:var(--gray6);border:1px solid var(--gray3);border-radius:6px;font-size:12px;cursor:pointer"> 5653 ð 5654 </button> 5655 </td> 5656 </tr>`; 5657 }).join(''); 5658 } catch(e) { 5659 tbody.innerHTML = `<tr><td colspan="9" style="text-align:center;color:#fc8181;padding:24px">ש×××× ×××¢×× ×</td></tr>`; 5660 } 5661} 5662 5663function _ndFormatSource(src) { 5664 if (!src) return 'â'; 5665 if (src.startsWith('Newsletter:')) return 'ð§ ' + src.slice(11); 5666 if (src.includes('t.me/')) { 5667 const m = src.match(/t\.me\/([^/\s?]+)/); 5668 return 'ð¢ ' + (m ? m[1] : 'Telegram'); 5669 } 5670 try { return 'ð ' + new URL(src).hostname; } catch(_) {} 5671 return src.length > 40 ? src.slice(0, 40) + 'â¦' : src; 5672} 5673 5674function _ndCbChanged() { 5675 const cbs = document.querySelectorAll('.nd-cb'); 5676 const checked = document.querySelectorAll('.nd-cb:checked'); 5677 const selAll = document.getElementById('nd-select-all'); 5678 const delBtn = document.getElementById('nd-delete-btn'); 5679 if (selAll) selAll.checked = cbs.length > 0 && checked.length === cbs.length; 5680 if (delBtn) { 5681 delBtn.style.display = checked.length > 0 ? '' : 'none'; 5682 delBtn.textContent = `ð ×××§ × ××ר×× (${checked.length})`; 5683 } 5684} 5685 5686function _ndToggleAll(checked) { 5687 document.querySelectorAll('.nd-cb').forEach(cb => cb.checked = checked); 5688 _ndCbChanged(); 5689} 5690 5691async function _ndDeleteSelected() { 5692 const checked = document.querySelectorAll('.nd-cb:checked'); 5693 if (!checked.length) return; 5694 const ids = Array.from(checked).map(cb => cb.dataset.id); 5695 if (!confirm(`×××××§ ${ids.length} ×× ×××ת ××× ××§×פ×× ×× ×©×××?`)) return; 5696 const delBtn = document.getElementById('nd-delete-btn'); 5697 if (delBtn) delBtn.disabled = true; 5698 try { 5699 const r = await fetch('/api/admin/stores/bulk-delete', { 5700 method: 'DELETE', 5701 headers: {'Content-Type':'application/json'}, 5702 body: JSON.stringify({ store_ids: ids }) 5703 }); 5704 const d = await r.json(); 5705 showToast(`ð × ×××§× ${d.stores_deleted} ×× ×××ת ×-${d.coupons_deleted} ×§×פ×× ××`); 5706 loadNullDomainStores(); 5707 } catch(e) { 5708 alert('ש×××× ×××××§×'); 5709 if (delBtn) delBtn.disabled = false;
5710 } 5711} 5712 5713let _storesData = []; 5714let _storesSortKey = 'name', _storesSortDir = 1, _storesPage = 0, _storesOriginFilterVal = 'all'; 5715const _storesPerPage = 50; 5716 5717async function loadStoresView() { 5718 const tbody = document.getElementById('stores-tbody'); 5719 if (!tbody) return; 5720 tbody.innerHTML = `<tr><td colspan="6" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr>`; 5721 try { 5722 const res = await fetch('/api/admin/stores/all'); 5723 _storesData = await res.json(); 5724 const badge = document.getElementById('src-stores-count'); 5725 if (badge) badge.textContent = _storesData.length; 5726 _storesPage = 0; 5727 _storesRender(); 5728 } catch(e) { 5729 tbody.innerHTML = `<tr><td colspan="6" style="text-align:center;color:#fc8181;padding:24px">ש×××× ×××¢×× ×</td></tr>`; 5730 } 5731} 5732 5733function _storesFilter() { 5734 _storesPage = 0; 5735 _storesRender(); 5736} 5737 5738function _storesSetSortKey(key) { 5739 _storesSortKey = key; _storesSortDir = 1; _storesPage = 0; 5740 const dirBtn = document.getElementById('stores-sort-dir-btn'); 5741 if (dirBtn) dirBtn.textContent = 'â'; 5742 _storesRender(); 5743} 5744 5745function _storesToggleDir() { 5746 _storesSortDir *= -1; _storesPage = 0; 5747 const dirBtn = document.getElementById('stores-sort-dir-btn'); 5748 if (dirBtn) dirBtn.textContent = _storesSortDir === 1 ? 'â' : 'â'; 5749 _storesRender(); 5750} 5751 5752function storesGoPage(p) { _storesPage = p; _storesRender(); } 5753 5754function _storesOriginFilter(val) { 5755 _storesOriginFilterVal = val; 5756 _storesPage = 0; 5757 ['all','il','int','null'].forEach(k => { 5758 const btn = document.getElementById('stores-origin-' + k); 5759 if (btn) { btn.style.background = k===val ? 'var(--accent)' : ''; btn.style.color = k===val ? '#000' : ''; } 5760 }); 5761 _storesRender(); 5762} 5763 5764function _storesRender() { 5765 const q = (document.getElementById('stores-search')?.value || '').toLowerCase(); 5766 let rows = _storesData.slice(); 5767 if (q) rows = rows.filter(s => 5768 s.name.toLowerCase().includes(q) || 5769 (s.store_he || '').includes(q) || 5770 (s.domain || '').toLowerCase().includes(q) || 5771 (s.aliases || []).join(' ').toLowerCase().includes(q) 5772 ); 5773 if (_storesOriginFilterVal === 'il') rows = rows.filter(s => s.origin === 'il'); 5774 if (_storesOriginFilterVal === 'int') rows = rows.filter(s => s.origin === 'int'); 5775 if (_storesOriginFilterVal === 'null') rows = rows.filter(s => !s.origin); 5776 5777 rows.sort((a, b) => { 5778 let va, vb; 5779 if (_storesSortKey === 'name') { va = a.name.toLowerCase(); vb = b.name.toLowerCase(); } 5780 else if (_storesSortKey === 'coupons') { va = a.coupon_count || 0; vb = b.coupon_count || 0; } 5781 else if (_storesSortKey === 'domain') { va = (a.domain || '').toLowerCase(); vb = (b.domain || '').toLowerCase(); } 5782 if (va < vb) return -1 * _storesSortDir; 5783 if (va > vb) return 1 * _storesSortDir; 5784 return 0; 5785 }); 5786 5787 const totalPages = Math.max(1, Math.ceil(rows.length / _storesPerPage)); 5788 if (_storesPage >= totalPages) _storesPage = totalPages - 1; 5789 const slice = rows.slice(_storesPage * _storesPerPage, (_storesPage + 1) * _storesPerPage); 5790 5791 // pagination 5792 const pgDiv = document.getElementById('stores-pagination'); 5793 if (pgDiv) { 5794 if (totalPages <= 1) { pgDiv.innerHTML = ''; } else { 5795 const nums = _pageNums(_storesPage, totalPages); 5796 const numBtns = nums.map(n => n === 'â¦' 5797 ? `<span style="color:var(--gray5);padding:0 4px;font-size:13px">â¦</span>` 5798 : `<button class="td-btn${n-1===_storesPage?' page-active':''}" onclick="storesGoPage(${n-1})" style="min-width:32px">${n}</button>` 5799 ).join(''); 5800 pgDiv.innerHTML = ` 5801 <div style="display:flex;align-items:center;justify-content:center;gap:4px;padding:8px 18px 14px;border-top:1px solid var(--gray3);flex-wrap:wrap"> 5802 <button class="td-btn" onclick="storesGoPage(${_storesPage-1})" ${_storesPage===0?'disabled':''} style="padding:4px 10px">â¹ ××§×××</button> 5803 ${numBtns} 5804 <button class="td-btn" onclick="storesGoPage(${_storesPage+1})" ${_storesPage>=totalPages-1?'disabled':''} style="padding:4px 10px">××× âº</button> 5805 </div>`; 5806 } 5807 } 5808 5809 _renderStoresTable(slice); 5810} 5811 5812function _renderStoresTable(rows) { 5813 const tbody = document.getElementById('stores-tbody'); 5814 if (!tbody) return; 5815 if (!rows.length) { 5816 tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:var(--gray5);padding:24px">×× × ×צ×× ×× ×××ת</td></tr>`; 5817 return; 5818 } 5819 tbody.innerHTML = rows.map((s, i) => { 5820 const aliases = (s.aliases || []).join(', '); 5821 const domainVal = s.domain || ''; 5822 const hasDomain = !!domainVal; 5823 const logoUrl = s.logo_url || ''; 5824 const logoCell = logoUrl 5825 ? `<span style="color:#00e87a;font-size:13px;font-weight:700">â</span>` 5826 : `<span style="color:var(--gray5);font-size:13px">â</span>`;
5827 const storeHeVal = (s.store_he || '').replace(/"/g,'"'); 5828 return `<tr data-sid="${s.id}"> 5829 <td style="color:var(--gray5);font-size:12px">${_storesPage * _storesPerPage + i + 1}</td> 5830 <td> 5831 <input id="store-name-${s.id}" value="${s.name.replace(/"/g,'"')}" data-orig="${s.name.replace(/"/g,'"')}" 5832 style="width:100%;background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:5px 8px;font-size:12px;color:var(--white);direction:ltr"> 5833 </td> 5834 <td> 5835 <input id="store-he-${s.id}" value="${storeHeVal}" placeholder="×©× ××¢×ר×ת" 5836 style="width:100%;background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:5px 8px;font-size:12px;color:var(--white);direction:rtl"> 5837 </td> 5838 <td style="text-align:center;color:var(--gray5)">${s.coupon_count || 0}</td> 5839 <td> 5840 <input id="store-domain-${s.id}" value="${domainVal}" placeholder="domain.com" 5841 style="width:100%;background:var(--gray2);border:1px solid ${hasDomain ? 'var(--gray3)' : '#7c2d12'};border-radius:6px;padding:5px 8px;font-size:12px;color:var(--white);direction:ltr"> 5842 </td> 5843 <td> 5844 <input id="store-aliases-${s.id}" value="${aliases}" placeholder="alias1, alias2" 5845 style="width:100%;background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:5px 8px;font-size:12px;color:var(--white);direction:ltr"> 5846 </td> 5847 <td style="text-align:center">${logoCell}</td> 5848 <td style="text-align:center"> 5849 <select onchange="_storeSetOrigin('${s.id}',this.value)" 5850 style="background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:3px 5px;font-size:11px;color:var(--white);cursor:pointer;outline:none"> 5851 <option value="" ${!s.origin?'selected':''}>â</option> 5852 <option value="il" ${s.origin==='il'?'selected':''}>ð®ð± IL</option> 5853 <option value="int" ${s.origin==='int'?'selected':''}>ð INT</option> 5854 </select> 5855 </td> 5856 <td> 5857 <div style="display:flex;gap:6px"> 5858 <button class="btn-sm" onclick="_storeSave('${s.id}')" 5859 style="padding:5px 10px;font-size:11px;background:var(--accent);color:#000">ש××ר</button> 5860 <button class="btn-sm" onclick="_storeDelete('${s.id}','${s.name.replace(/'/g,"\\'")}',${ s.coupon_count||0},'${(s.domain||'').replace(/'/g,"\\'")}')" 5861 style="padding:5px 10px;font-size:11px;background:#fc818122;color:#fc8181;border:1px solid #fc818144">×××§</button> 5862 </div> 5863 </td> 5864 </tr>`; 5865 }).join(''); 5866} 5867 5868async function _storeSave(storeId) { 5869 const nameInput = document.getElementById('store-name-' + storeId); 5870 const storeHeInput = document.getElementById('store-he-' + storeId); 5871 const domainInput = document.getElementById('store-domain-' + storeId); 5872 const aliasesInput = document.getElementById('store-aliases-' + storeId); 5873 if (!domainInput) return; 5874 const newName = nameInput ? nameInput.value.trim() : null; 5875 const origName = nameInput ? nameInput.dataset.orig : null; 5876 const storeHe = storeHeInput ? storeHeInput.value.trim() : null; 5877 const domain = domainInput.value.trim().replace(/^https?:\/\//i,'').replace(/^www\d*\./i,'').split('/')[0].toLowerCase(); 5878 const aliases = aliasesInput ? aliasesInput.value.split(',').map(a=>a.trim()).filter(Boolean) : undefined; 5879 const btn = domainInput.closest('tr').querySelector('button'); 5880 if (btn) { btn.disabled = true; btn.textContent = '...'; } 5881 // Language validation 5882 const _hasHebrew = /[×-תװ-×´]/.test(newName || ''); 5883 const _heHasHebrew = /[×-תװ-×´]/.test(storeHe || ''); 5884 const _heHasLatin = /[a-zA-Z]/.test(storeHe || ''); 5885 if (_hasHebrew) { showToast('â ï¸ ×©× ×× ×ת ×××× ××××ת ××× ×××ת ××××'); if (btn) { btn.disabled=false; btn.textContent='ש××ר'; } return; } 5886 // Allow pure brand acronyms (H&M, BSTN) â only block mixing Hebrew + Latin 5887 if (_heHasLatin && _heHasHebrew) { showToast('â ï¸ ×©× ×¢××¨× ×× ×××× ×ער×× ×¢×ר×ת ××× ×××ת'); if (btn) { btn.disabled=false; btn.textContent='ש××ר'; } return; } 5888 try { 5889 // Rename if name changed 5890 if (newName && newName !== origName) { 5891 const rr = await fetch(`/api/admin/stores/${storeId}/rename`, { 5892 method: 'POST', 5893 headers: {'Content-Type':'application/json'}, 5894 body: JSON.stringify({ name: newName }) 5895 }); 5896 if (!rr.ok) { 5897 const errText = await rr.text().catch(()=>''); 5898 showToast(rr.status === 409 ? 'â ï¸ ×©× ××ר ×§×××' : rr.status === 400 ? 'â ï¸ ' + (errText || 'ש×××× ×ש×× ×× ×©×') : 'ש×××× ×ש×× ×× ×©×'); 5899 return; 5900 } 5901 if (nameInput) nameInput.dataset.orig = newName; 5902 } 5903 // Update domain + aliases + store_he 5904 const body = {}; 5905 if (domain) body.domain = domain; else body.domain = null; 5906 if (aliases !== undefined) body.aliases = aliases; 5907 body.store_he = storeHe || null; 5908 const r = await fetch(`/api/admin/stores/${storeId}`, { 5909 method: 'PATCH', 5910 headers: {'Content-Type':'application/json'}, 5911 body: JSON.stringify(body) 5912 }); 5913 if (r.ok) { 5914 showToast('â × ×©×ר'); 5915 if (domainInput) domainInput.style.borderColor = domain ? 'var(--accent)' : '#7c2d12'; 5916 } else { 5917 const errText = await r.text().catch(()=>'');
5918 showToast(r.status === 409 ? 'â ï¸ ×××××× ××ר ש××× ××× ×ת ××רת' : r.status === 400 ? 'â ï¸ ' + (errText || 'ש××××') : 'ש×××× ×ש××ר×'); 5919 } 5920 } catch(e) { 5921 showToast('ש×××× ×ש××ר×'); 5922 } finally { 5923 if (btn) { btn.disabled = false; btn.textContent = 'ש××ר'; } 5924 } 5925} 5926 5927 5928function _showResultModal(icon, title, body) { 5929 document.getElementById('result-modal-icon').textContent = icon; 5930 document.getElementById('result-modal-title').textContent = title; 5931 document.getElementById('result-modal-body').textContent = body; 5932 document.getElementById('result-modal').style.display = 'flex'; 5933} 5934function _closeResultModal() { 5935 document.getElementById('result-modal').style.display = 'none'; 5936} 5937 5938async function _storeDelete(storeId, storeName, couponCount, storeDomain) { 5939 const choice = await _showStoreDeleteModal(storeName, couponCount, storeDomain || ''); 5940 if (!choice) return; 5941 const row = document.querySelector(`tr[data-sid="${storeId}"]`); 5942 if (row) row.style.opacity = '0.4'; 5943 try { 5944 if (choice.action === 'merge') { 5945 const r = await fetch(`/api/admin/stores/${storeId}/merge-into`, { 5946 method: 'POST', 5947 headers: {'Content-Type':'application/json'}, 5948 body: JSON.stringify({ target_domain: choice.targetDomain }) 5949 }); 5950 const d = await r.json(); 5951 if (!r.ok) { 5952 if (row) row.style.opacity = ''; 5953 _showResultModal('â', 'ש×××× ××××××', d.detail || JSON.stringify(d)); 5954 return; 5955 } 5956 _showResultModal('â ', `"${storeName}" ××××× ×× "${d.dst}"`, 5957 `${d.coupons_moved} ×§×פ×× ×× ×××¢×ר×${d.coupons_dropped ? `, ${d.coupons_dropped} ×פ×××× × ×××§×` : ''}.`); 5958 if (row) row.remove(); 5959 _storesData = _storesData.filter(s => s.id !== storeId); 5960 } else { 5961 const r = await fetch('/api/admin/stores/bulk-delete', { 5962 method: 'DELETE', 5963 headers: {'Content-Type':'application/json'}, 5964 body: JSON.stringify({ store_ids: [storeId] }) 5965 }); 5966 const d = await r.json(); 5967 if (!r.ok) { 5968 if (row) row.style.opacity = ''; 5969 _showResultModal('â', 'ש×××× ×××××§×', d.detail || JSON.stringify(d)); 5970 return; 5971 } 5972 const warn = couponCount > 0 && d.coupons_deleted === 0 5973 ? `\n\nâ ï¸ ×©×× ××: ××× ×ת × ×××§× ×× ${couponCount} ×§×פ×× ×× ×× ××סר×.` 5974 : ''; 5975 _showResultModal('â ', `"${storeName}" × ×××§×`, `${d.coupons_deleted} ×§×פ×× ×× ××סר×.${warn}`); 5976 if (row) row.remove(); 5977 _storesData = _storesData.filter(s => s.id !== storeId); 5978 } 5979 } catch(e) { 5980 if (row) row.style.opacity = ''; 5981 _showResultModal('â', 'ש××××', e.message || 'ש×××ת רשת'); 5982 } 5983} 5984 5985function _showStoreDeleteModal(storeName, couponCount, storeDomain) { 5986 return new Promise(resolve => { 5987 const overlay = document.createElement('div'); 5988 overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;display:flex;align-items:center;justify-content:center'; 5989 overlay.innerHTML = ` 5990 <div style="background:#1a1a1a;border:1px solid #333;border-radius:14px;padding:28px 32px;min-width:340px;max-width:460px;text-align:right;direction:rtl;font-family:'DM Sans',sans-serif"> 5991 <div style="font-size:16px;font-weight:600;margin-bottom:6px;color:#fff">×××קת ×× ×ת</div> 5992 <div style="font-size:13px;color:#888;margin-bottom:18px">×× ×ת: <span style="color:#fff">${storeName || 'â'}</span>${couponCount ? `<span style="color:#888"> · ${couponCount} ×§×פ×× ××</span>` : ''}</div> 5993 <div style="display:flex;flex-direction:column;gap:8px"> 5994 <label style="display:flex;align-items:center;gap:10px;cursor:pointer;padding:10px 14px;border:1px solid #8b2222;border-radius:8px;background:#3a1a1a"> 5995 <input type="radio" name="_sdel_action" value="delete" checked style="accent-color:#e05555;cursor:pointer;flex-shrink:0" /> 5996 <span style="font-size:13px;color:#ff6b6b">ð£ ×××§ ×× ×ת ××× ××§×פ×× ××</span> 5997 </label> 5998 <label id="_sdel_merge_label" style="display:flex;align-items:flex-start;gap:10px;cursor:pointer;padding:10px 14px;border:1px solid #444;border-radius:8px;background:#2a2a2a"> 5999 <input type="radio" name="_sdel_action" value="merge" style="accent-color:var(--accent);cursor:pointer;flex-shrink:0;margin-top:2px" /> 6000 <div style="flex:1"> 6001 <div style="font-size:13px;color:#fff;margin-bottom:8px">ð ×××§ ×× ×ת ×××× ×§×פ×× ×× ×ש××ת ×:</div> 6002 <input id="_sdel_target" type="text" placeholder="domain.com" value="${storeDomain || ''}" 6003 style="background:#1a1a1a;border:1px solid #555;border-radius:8px;padding:7px 12px;font-size:13px;color:#fff;outline:none;font-family:'DM Sans',sans-serif;width:100%;box-sizing:border-box;direction:ltr;text-align:left" /> 6004 </div> 6005 </label> 6006 </div> 6007 <div style="display:flex;gap:10px;justify-content:flex-end;margin-top:20px"> 6008 <button id="_sdel_cancel" style="background:transparent;border:1px solid #444;border-radius:8px;padding:8px 18px;color:#aaa;cursor:pointer;font-size:13px;font-family:'DM Sans',sans-serif">×××××</button> 6009 <button id="_sdel_ok" style="background:var(--accent);border:none;border-radius:8px;padding:8px 22px;color:#000;cursor:pointer;font-size:13px;font-weight:600;font-family:'DM Sans',sans-serif">××ש×ר</button> 6010 </div> 6011 </div>`; 6012 document.body.appendChild(overlay); 6013 6014 // clicking merge label focuses the input 6015 overlay.querySelector('#_sdel_merge_label').addEventListener('click', () => { 6016 overlay.querySelector('input[value="merge"]').checked = true; 6017 setTimeout(() => overlay.querySelector('#_sdel_target').focus(), 50); 6018 }); 6019 overlay.querySelector('#_sdel_target').addEventListener('focus', () => { 6020 overlay.querySelector('input[value="merge"]').checked = true; 6021 }); 6022 6023 overlay.querySelector('#_sdel_ok').addEventListener('click', e => { 6024 e.stopPropagation(); 6025 const action = overlay.querySelector('input[name="_sdel_action"]:checked')?.value || 'delete'; 6026 const targetDomain = overlay.querySelector('#_sdel_target')?.value?.trim() || ''; 6027 if (action === 'merge' && !targetDomain) { 6028 overlay.querySelector('#_sdel_target').style.borderColor = '#e05555'; 6029 overlay.querySelector('#_sdel_target').focus(); 6030 return; 6031 } 6032 document.body.removeChild(overlay); 6033 resolve({ action, targetDomain }); 6034 }); 6035 overlay.querySelector('#_sdel_cancel').addEventListener('click', e => { 6036 e.stopPropagation(); 6037 document.body.removeChild(overlay); 6038 resolve(null); 6039 }); 6040 }); 6041} 6042 6043async function _storeSetOrigin(storeId, val) { 6044 const origin = val === '' ? null : val; 6045 try { 6046 const r = await fetch(`/api/admin/stores/${storeId}/set-origin`, { 6047 method: 'PATCH', 6048 headers: {'Content-Type':'application/json'}, 6049 body: JSON.stringify({ origin }) 6050 }); 6051 if (r.ok) { 6052 showToast(origin ? `â origin = ${origin}` : 'â origin cleared'); 6053 const s = _storesData.find(x => x.id === storeId); 6054 if (s) s.origin = origin; 6055 } else { 6056 showToast('ש×××× ××¢×××× ×××× ×'); 6057 } 6058 } catch(e) { 6059 showToast('ש×××× ××¢×××× ×××× ×'); 6060 } 6061} 6062 6063// ââ ××× ×××× × view ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 6064let _noOriginData = [], _noOriginPage = 0; 6065const _noOriginPerPage = 50; 6066 6067// ââ Store Categories view ââââââââââââââââââââââââââââââââââââââââââââââââââ 6068const ALL_CATS = ['Beauty','Fashion','Finance','Food','Health','Home','Kids','Leisure','Sports','Tech','Travel','Vacation']; 6069const CAT_COLORS = { 6070 Beauty:'#e879a0',Fashion:'#a78bfa',Finance:'#34d399',Food:'#fb923c', 6071 Health:'#f87171',Home:'#60a5fa',Kids:'#fbbf24',Leisure:'#4ade80', 6072 Sports:'#38bdf8',Tech:'#818cf8',Travel:'#f472b6',Vacation:'#a3e635' 6073}; 6074let _catData = [], _catPage = 0; 6075const _catPerPage = 50; 6076
6077async function loadStoreCategoriesView() { 6078 const tbody = document.getElementById('cat-tbody'); 6079 if (!tbody) return; 6080 tbody.innerHTML = `<tr><td colspan="6" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr>`; 6081 try { 6082 const res = await fetch('/api/admin/stores/all'); 6083 _catData = await res.json(); 6084 const badge = document.getElementById('src-cat-count'); 6085 if (badge) badge.textContent = _catData.length; 6086 _catPage = 0; 6087 _catRender(); 6088 } catch(e) { 6089 tbody.innerHTML = `<tr><td colspan="6" style="text-align:center;color:#fc8181;padding:24px">ש×××× ×××¢×× ×</td></tr>`; 6090 } 6091} 6092 6093function _catFilter() { _catPage = 0; _catRender(); } 6094 6095function catGoPage(p) { _catPage = p; _catRender(); } 6096 6097function _catRender() { 6098 const q = (document.getElementById('cat-search')?.value || '').toLowerCase(); 6099 const filt = document.getElementById('cat-filter-sel')?.value || 'all'; 6100 let rows = _catData.slice(); 6101 if (q) rows = rows.filter(s => 6102 s.name.toLowerCase().includes(q) || 6103 (s.store_he || '').includes(q) || 6104 (s.domain || '').toLowerCase().includes(q) 6105 ); 6106 if (filt === 'empty') rows = rows.filter(s => !(s.categories || []).length); 6107 if (filt === 'has') rows = rows.filter(s => (s.categories || []).length > 0); 6108 6109 const totalPages = Math.max(1, Math.ceil(rows.length / _catPerPage)); 6110 if (_catPage >= totalPages) _catPage = totalPages - 1; 6111 const slice = rows.slice(_catPage * _catPerPage, (_catPage + 1) * _catPerPage); 6112 6113 const pgDiv = document.getElementById('cat-pagination'); 6114 if (pgDiv) { 6115 if (totalPages <= 1) { pgDiv.innerHTML = ''; } else { 6116 const nums = _pageNums(_catPage, totalPages); 6117 const numBtns = nums.map(n => n === 'â¦' 6118 ? `<span style="color:var(--gray5);padding:0 4px;font-size:13px">â¦</span>` 6119 : `<button class="td-btn${n-1===_catPage?' page-active':''}" onclick="catGoPage(${n-1})" style="min-width:32px">${n}</button>` 6120 ).join(''); 6121 pgDiv.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;gap:4px;padding:8px 18px 14px;border-top:1px solid var(--gray3);flex-wrap:wrap"> 6122 <button class="td-btn" onclick="catGoPage(${_catPage-1})" ${_catPage===0?'disabled':''} style="padding:4px 10px">â¹ ××§×××</button> 6123 ${numBtns} 6124 <button class="td-btn" onclick="catGoPage(${_catPage+1})" ${_catPage>=totalPages-1?'disabled':''} style="padding:4px 10px">××× âº</button> 6125 </div>`; 6126 } 6127 } 6128 6129 const tbody = document.getElementById('cat-tbody'); 6130 if (!tbody) return; 6131 if (!slice.length) { 6132 tbody.innerHTML = `<tr><td colspan="6" style="text-align:center;color:var(--gray5);padding:24px">×× × ×צ×× ×× ×××ת</td></tr>`; 6133 return; 6134 } 6135 tbody.innerHTML = slice.map((s, i) => { 6136 const cats = s.categories || []; 6137 const tagBtns = ALL_CATS.map(c => { 6138 const on = cats.includes(c); 6139 const col = CAT_COLORS[c]; 6140 return `<button onclick="_catToggle('${s.id}','${c}')" style=" 6141 border:1px solid ${col};border-radius:20px;padding:2px 10px;font-size:11px;cursor:pointer; 6142 background:${on ? col : 'transparent'};color:${on ? '#000' : col}; 6143 font-weight:${on?'700':'400'};transition:all 0.15s;margin:2px">${c}</button>`; 6144 }).join(''); 6145 return `<tr id="cat-row-${s.id}"> 6146 <td style="color:var(--gray5);font-size:12px">${_catPage * _catPerPage + i + 1}</td> 6147 <td style="font-size:13px;font-weight:600">${s.name}</td> 6148 <td style="font-size:13px;direction:rtl">${s.store_he || '<span style="color:var(--gray5)">â</span>'}</td> 6149 <td style="font-size:12px;color:var(--gray6)">${s.domain || '<span style="color:var(--gray5)">â</span>'}</td> 6150 <td><div style="display:flex;flex-wrap:wrap;gap:0">${tagBtns}</div></td> 6151 <td id="cat-status-${s.id}" style="text-align:center;font-size:13px"></td> 6152 </tr>`; 6153 }).join(''); 6154} 6155 6156async function _catToggle(storeId, cat) { 6157 const store = _catData.find(s => s.id === storeId); 6158 if (!store) return; 6159 const statusEl = document.getElementById(`cat-status-${storeId}`); 6160 const cats = store.categories ? [...store.categories] : []; 6161 const idx = cats.indexOf(cat); 6162 if (idx === -1) cats.push(cat); else cats.splice(idx, 1); 6163 store.categories = cats; 6164 if (statusEl) statusEl.innerHTML = `<span style="color:var(--gray5);font-size:11px">ש××ר...</span>`;
6165 try { 6166 const res = await fetch(`/api/admin/stores/${storeId}`, { 6167 method: 'PATCH', 6168 headers: {'Content-Type':'application/json'}, 6169 body: JSON.stringify({categories: cats}) 6170 }); 6171 if (!res.ok) throw new Error(await res.text()); 6172 if (statusEl) { statusEl.innerHTML = `<span style="color:#00e87a">â</span>`; setTimeout(()=>{ if(statusEl) statusEl.innerHTML=''; }, 1800); } 6173 _catRender(); 6174 } catch(e) { 6175 store.categories = cats.includes(cat) ? cats.filter(c=>c!==cat) : [...cats, cat]; // revert 6176 if (statusEl) statusEl.innerHTML = `<span style="color:#fc8181" title="${e.message}">â</span>`; 6177 } 6178} 6179// ââ end Store Categories âââââââââââââââââââââââââââââââââââââââââââââââââââ 6180 6181async function loadNoOriginStores() { 6182 const tbody = document.getElementById('no-origin-tbody'); 6183 if (!tbody) return; 6184 tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr>`; 6185 try { 6186 const res = await fetch('/api/admin/stores/all'); 6187 const all = await res.json(); 6188 _noOriginData = all.filter(s => !s.origin); 6189 const badge = document.getElementById('src-noorigin-count'); 6190 if (badge) badge.textContent = _noOriginData.length; 6191 _noOriginPage = 0; 6192 _renderNoOriginTable(); 6193 } catch(e) { 6194 tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:#fc8181;padding:24px">ש×××× ×××¢×× ×</td></tr>`; 6195 } 6196} 6197 6198function _renderNoOriginTable() { 6199 const tbody = document.getElementById('no-origin-tbody'); 6200 if (!tbody) return; 6201 const totalPages = Math.max(1, Math.ceil(_noOriginData.length / _noOriginPerPage)); 6202 if (_noOriginPage >= totalPages) _noOriginPage = totalPages - 1; 6203 const slice = _noOriginData.slice(_noOriginPage * _noOriginPerPage, (_noOriginPage + 1) * _noOriginPerPage); 6204 6205 const pgDiv = document.getElementById('no-origin-pagination'); 6206 if (pgDiv) { 6207 if (totalPages <= 1) { pgDiv.innerHTML = ''; } else { 6208 const nums = _pageNums(_noOriginPage, totalPages); 6209 const numBtns = nums.map(n => n === 'â¦' 6210 ? `<span style="color:var(--gray5);padding:0 4px;font-size:13px">â¦</span>` 6211 : `<button class="td-btn${n-1===_noOriginPage?' page-active':''}" onclick="_noOriginGoPage(${n-1})" style="min-width:32px">${n}</button>` 6212 ).join(''); 6213 pgDiv.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;gap:4px;padding:8px 18px 14px;border-top:1px solid var(--gray3);flex-wrap:wrap"> 6214 <button class="td-btn" onclick="_noOriginGoPage(${_noOriginPage-1})" ${_noOriginPage===0?'disabled':''} style="padding:4px 10px">â¹ ××§×××</button> 6215 ${numBtns} 6216 <button class="td-btn" onclick="_noOriginGoPage(${_noOriginPage+1})" ${_noOriginPage>=totalPages-1?'disabled':''} style="padding:4px 10px">××× âº</button> 6217 </div>`; 6218 } 6219 } 6220 6221 if (!slice.length) { 6222 tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--gray5);padding:24px">××× ×× ×××ת ××× ×××× ×</td></tr>`; 6223 return; 6224 } 6225 tbody.innerHTML = slice.map((s, i) => `<tr> 6226 <td style="color:var(--gray5);font-size:12px">${_noOriginPage * _noOriginPerPage + i + 1}</td> 6227 <td>${s.name}${s.store_he ? ` <span style="color:var(--gray5);font-size:11px">(${s.store_he})</span>` : ''}</td> 6228 <td style="color:var(--gray6);font-size:12px">${s.domain || '<span style="color:#fc8181">â</span>'}</td> 6229 <td style="text-align:center;color:var(--gray5)">${s.coupon_count || 0}</td> 6230 <td style="text-align:center"> 6231 <select onchange="_noOriginSet('${s.id}',this)" 6232 style="background:var(--gray2);border:1px solid var(--gray3);border-radius:6px;padding:3px 8px;font-size:12px;color:var(--white);cursor:pointer;outline:none"> 6233 <option value="">â ×× ××××¢</option> 6234 <option value="il">ð®ð± IL</option> 6235 <option value="int">ð INT</option> 6236 </select> 6237 </td> 6238 </tr>`).join(''); 6239} 6240 6241function _noOriginGoPage(p) { _noOriginPage = p; _renderNoOriginTable(); } 6242 6243async function _noOriginSet(storeId, sel) { 6244 const origin = sel.value || null; 6245 try { 6246 const r = await fetch(`/api/admin/stores/${storeId}/set-origin`, { 6247 method: 'PATCH', headers: {'Content-Type':'application/json'}, 6248 body: JSON.stringify({ origin }) 6249 }); 6250 if (r.ok) { 6251 showToast(origin ? `â origin = ${origin}` : 'â cleared'); 6252 _noOriginData = _noOriginData.filter(s => s.id !== storeId); 6253 const badge = document.getElementById('src-noorigin-count'); 6254 if (badge) badge.textContent = _noOriginData.length; 6255 _renderNoOriginTable(); 6256 } else { showToast('ש××××'); sel.value = ''; } 6257 } catch(e) { showToast('ש××××'); sel.value = ''; } 6258} 6259 6260// ââ תצ××ת ×§×פ×× ×× â coupon preview ââââââââââââââââââââââââââââââââââââââââââ 6261let _cprevAllData = [], _cprevFilter = 'all', _cprevPage = 0, _cprevDomainMap = {}; 6262const _cprevPerPage = 48; 6263 6264async function loadCouponPreview() { 6265 const grid = document.getElementById('coupon-preview-grid'); 6266 if (!grid) return; 6267 grid.innerHTML = `<div style="color:var(--gray5);text-align:center;grid-column:1/-1;padding:40px">×××¢×...</div>`; 6268 try { 6269 const [cRes, sRes] = await Promise.all([ 6270 fetch('/api/admin/coupons/all-preview'), 6271 fetch('/api/admin/stores/all') 6272 ]); 6273 _cprevAllData = await cRes.json(); 6274 const stores = await sRes.json(); 6275 const originMap = {}, domainMap = {};
6276 stores.forEach(s => { 6277 originMap[s.name] = s.origin || null; 6278 if (s.domain) domainMap[s.name] = s.domain; 6279 }); 6280 _cprevDomainMap = domainMap; 6281 _cprevAllData.forEach(c => { c._origin = originMap[c.store] || null; }); 6282 _cprevPage = 0; 6283 _cprevRender(); 6284 } catch(e) { 6285 grid.innerHTML = `<div style="color:#fc8181;text-align:center;grid-column:1/-1;padding:40px">ש×××× ×××¢×× ×</div>`; 6286 } 6287} 6288 6289function _cprevSetFilter(val) { 6290 _cprevFilter = val; 6291 _cprevPage = 0; 6292 ['all','il','int'].forEach(k => { 6293 const btn = document.getElementById('cprev-filter-' + k); 6294 if (!btn) return; 6295 btn.style.background = k===val ? 'var(--accent)' : ''; 6296 btn.style.color = k===val ? '#000' : ''; 6297 btn.classList.toggle('btn-ghost', k !== val); 6298 }); 6299 _cprevRender(); 6300} 6301 6302function _cprevGrouped() { 6303 let rows = _cprevAllData.slice(); 6304 if (_cprevFilter === 'il') rows = rows.filter(c => c._origin === 'il'); 6305 if (_cprevFilter === 'int') rows = rows.filter(c => c._origin === 'int'); 6306 // Group by store, preserve insertion order (first coupon per store sets order) 6307 const map = new Map(); 6308 rows.forEach(c => { 6309 const s = c.store || ''; 6310 if (!map.has(s)) map.set(s, []); 6311 map.get(s).push(c); 6312 }); 6313 return [...map.entries()].sort((a,b) => a[0].localeCompare(b[0])); 6314} 6315 6316function _cprevCouponCard(c) { 6317 const disc = c.discount ? formatDiscount(c.discount) : ''; 6318 const code = c.code || ''; 6319 const notes = c.notes || ''; 6320 return `<div class="coupon-card" style="min-width:220px;max-width:260px;flex:0 0 auto"> 6321 ${disc ? `<div class="cc-discount" style="font-size:19px;margin-bottom:6px">${disc}</div>` : ''} 6322 ${notes ? `<div class="cc-notes" style="margin-bottom:8px">${notes}</div>` : ''} 6323 ${code ? `<div style="background:var(--gray3);border:1px solid rgba(0,232,122,0.35);border-radius:8px;padding:9px 12px;font-family:monospace;font-size:14px;font-weight:700;color:var(--white);letter-spacing:2px;text-align:center;direction:ltr">${code}</div>` : ''} 6324 </div>`; 6325} 6326 6327function _cprevRender() { 6328 const grid = document.getElementById('coupon-preview-grid'); 6329 const pgDiv = document.getElementById('coupon-preview-pagination'); 6330 if (!grid) return; 6331 6332 const groups = _cprevGrouped(); 6333 const totalPages = Math.max(1, Math.ceil(groups.length / _cprevPerPage)); 6334 if (_cprevPage >= totalPages) _cprevPage = totalPages - 1; 6335 const slice = groups.slice(_cprevPage * _cprevPerPage, (_cprevPage + 1) * _cprevPerPage); 6336 6337 if (pgDiv) { 6338 if (totalPages <= 1) { pgDiv.innerHTML = ''; } else { 6339 const nums = _pageNums(_cprevPage, totalPages); 6340 const numBtns = nums.map(n => n === 'â¦' 6341 ? `<span style="color:var(--gray5);padding:0 4px">â¦</span>` 6342 : `<button class="td-btn${n-1===_cprevPage?' page-active':''}" onclick="_cprevGoPage(${n-1})" style="min-width:32px">${n}</button>` 6343 ).join(''); 6344 pgDiv.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;gap:4px;flex-wrap:wrap"> 6345 <button class="td-btn" onclick="_cprevGoPage(${_cprevPage-1})" ${_cprevPage===0?'disabled':''} style="padding:4px 10px">â¹ ××§×××</button> 6346 ${numBtns} 6347 <button class="td-btn" onclick="_cprevGoPage(${_cprevPage+1})" ${_cprevPage>=totalPages-1?'disabled':''} style="padding:4px 10px">××× âº</button> 6348 </div>`; 6349 } 6350 } 6351 6352 if (!slice.length) { 6353 grid.innerHTML = `<div style="color:var(--gray5);text-align:center;grid-column:1/-1;padding:40px">××× ×§×פ×× ××</div>`; 6354 return; 6355 } 6356 6357 grid.innerHTML = slice.map(([storeName, coupons]) => { 6358 const origin = coupons[0]._origin; 6359 const originBadge = origin === 'il' 6360 ? `<span style="background:#1a3a1a;color:#4ade80;border-radius:4px;padding:1px 6px;font-size:10px;font-weight:700">ð®ð±</span>` 6361 : origin === 'int' 6362 ? `<span style="background:#1a1a3a;color:#93c5fd;border-radius:4px;padding:1px 6px;font-size:10px;font-weight:700">ð</span>` 6363 : `<span style="background:var(--gray3);color:var(--gray5);border-radius:4px;padding:1px 6px;font-size:10px">â</span>`; 6364 const domain = _cprevDomainMap[storeName] || ''; 6365 const faviconUrls = storeToFavicon(storeName, '') || []; 6366 const favicon = faviconUrls[0] || null; 6367 const faviconFallbacksStr = faviconUrls.slice(1).join('|'); 6368 const safeStore = storeName.replace(/'/g, "\\'"); 6369 const countPill = `<span style="background:var(--gray3);color:var(--gray5);border-radius:12px;padding:2px 8px;font-size:11px;font-weight:600">${coupons.length} ×§×פ×× ××</span>`;
6370 6371 return `<div style="grid-column:1/-1;border:1px solid var(--gray3);border-radius:14px;overflow:hidden;margin-bottom:4px"> 6372 <!-- store header --> 6373 <div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--gray2);border-bottom:1px solid var(--gray3);direction:ltr"> 6374 <div class="cc-logo" style="width:52px;height:52px;flex-shrink:0;border-radius:12px"> 6375 ${favicon 6376 ? `<img src="${favicon}" alt="${safeStore}" onerror="_faviconErr(this,'${faviconFallbacksStr}','${safeStore}')">` 6377 : _storeInitial(storeName)} 6378 </div> 6379 <div style="flex:1;display:flex;align-items:center;gap:8px;min-width:0;direction:rtl"> 6380 <span style="font-size:20px;font-weight:800;color:var(--white);white-space:nowrap">${storeName}</span> 6381 ${originBadge} 6382 ${countPill} 6383 ${domain ? `<span style="font-size:12px;color:var(--gray5);direction:ltr">${domain}</span>` : ''} 6384 </div> 6385 </div> 6386 <!-- coupon cards row --> 6387 <div style="display:flex;gap:12px;padding:12px 16px;overflow-x:auto;background:#111;flex-wrap:wrap"> 6388 ${coupons.map(_cprevCouponCard).join('')} 6389 </div> 6390 </div>`; 6391 }).join(''); 6392} 6393 6394function _cprevGoPage(p) { _cprevPage = p; _cprevRender(); window.scrollTo(0,0); } 6395 6396let _orphanSearchResults = []; 6397let _allStoresForOrphan = []; 6398 6399async function _orphanSearchStores() { 6400 const modal = document.getElementById('orphan-search-modal'); 6401 const list = document.getElementById('orphan-modal-list'); 6402 const sub = document.getElementById('orphan-modal-sub'); 6403 _orphanSearchResults = []; 6404 modal.style.display = 'flex'; 6405 list.innerHTML = `<div style="color:var(--gray5);text-align:center;padding:32px">××פש ×ת×××ת...</div>`; 6406 sub.textContent = '××פש...'; 6407 _orphanUpdateSummary(); 6408 try { 6409 const [searchRes, storesRes] = await Promise.all([ 6410 fetch('/api/admin/coupons/orphans/search-stores'), 6411 fetch('/api/admin/stores/all') 6412 ]); 6413 _orphanSearchResults = await searchRes.json(); 6414 const storesData = await storesRes.json(); 6415 _allStoresForOrphan = (Array.isArray(storesData) ? storesData : (storesData.stores || [])).sort((a,b) => a.name.localeCompare(b.name)); 6416 const found = _orphanSearchResults.filter(d => d.matched_store_id); 6417 sub.textContent = `× ×צ×× ${found.length} ×ת×××ת ×ת×× ${_orphanSearchResults.length} ×× ×××ת`; 6418 _orphanRenderList(); 6419 } catch(e) { 6420 list.innerHTML = `<div style="color:#f87171;text-align:center;padding:24px">ש×××× ×××פ×ש</div>`; 6421 sub.textContent = 'ש××××'; 6422 } 6423} 6424 6425function _orphanRenderList() { 6426 const list = document.getElementById('orphan-modal-list'); 6427 const storeOptions = _allStoresForOrphan.map(s => 6428 `<option value="${s.id}">${s.name}${s.domain ? ' ('+s.domain+')' : ''}</option>` 6429 ).join(''); 6430 6431 list.innerHTML = _orphanSearchResults.map((d, i) => { 6432 const hasMatch = !!d.matched_store_id; 6433 const border = hasMatch ? '#166534' : 'var(--gray3)'; 6434 const icon = hasMatch ? `<span style="color:#4ade80;font-size:15px;flex-shrink:0">â</span>` 6435 : `<span style="color:#f87171;font-size:15px;flex-shrink:0">â</span>`;
6436 const matchLabel = hasMatch 6437 ? `<span style="color:#86efac">${d.matched_store_name}</span> <span style="color:var(--gray6)">(${d.matched_domain || '?'})</span>` 6438 : `<span style="color:var(--gray5)">×× × ×צ×× ×× ×ת</span>`; 6439 6440 return `<div id="orphan-row-${i}" style="display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--gray2);border-radius:8px;border:1px solid ${border}"> 6441 ${icon} 6442 <div style="flex:1;min-width:0"> 6443 <div style="font-size:13px;font-weight:600;color:var(--white)">${d.orphan_name}</div> 6444 <div id="orphan-label-${i}" style="font-size:11px;color:var(--gray5);margin-top:2px">${d.coupon_count} ×§×פ×× ×× â ${matchLabel}</div> 6445 <select id="orphan-sel-${i}" onchange="_orphanPickStore(${i},this.value)" 6446 style="display:none;margin-top:6px;width:100%;background:var(--gray3);color:var(--white);border:1px solid var(--gray4);border-radius:6px;padding:4px 6px;font-size:12px"> 6447 <option value="">â ××× / ×× ×ª××× â</option> 6448 ${storeOptions} 6449 </select> 6450 </div> 6451 <button onclick="_orphanToggleEdit(${i})" 6452 style="flex-shrink:0;background:none;border:1px solid var(--gray4);border-radius:6px;color:var(--gray5);font-size:11px;padding:3px 7px;cursor:pointer">â</button> 6453 </div>`; 6454 }).join(''); 6455 6456 _orphanUpdateSummary(); 6457} 6458 6459function _orphanToggleEdit(i) { 6460 const sel = document.getElementById(`orphan-sel-${i}`); 6461 const lbl = document.getElementById(`orphan-label-${i}`); 6462 const isOpen = sel.style.display !== 'none'; 6463 if (isOpen) { 6464 sel.style.display = 'none'; 6465 lbl.style.display = ''; 6466 } else { 6467 sel.style.display = ''; 6468 lbl.style.display = 'none'; 6469 sel.value = _orphanSearchResults[i].matched_store_id || ''; 6470 } 6471} 6472 6473function _orphanPickStore(i, storeId) { 6474 const d = _orphanSearchResults[i]; 6475 if (!storeId) { 6476 d.matched_store_id = null; 6477 d.matched_store_name = null; 6478 d.matched_domain = null; 6479 } else { 6480 const s = _allStoresForOrphan.find(s => s.id === storeId); 6481 d.matched_store_id = storeId; 6482 d.matched_store_name = s ? s.name : storeId; 6483 d.matched_domain = s ? s.domain : null; 6484 } 6485 _orphanToggleEdit(i); 6486 _orphanRenderList(); 6487} 6488 6489function _orphanUpdateSummary() { 6490 const found = _orphanSearchResults.filter(d => d.matched_store_id); 6491 const notFound = _orphanSearchResults.filter(d => !d.matched_store_id); 6492 const totalCoupons = found.reduce((s, d) => s + d.coupon_count, 0); 6493 const leftover = notFound.reduce((s, d) => s + d.coupon_count, 0); 6494 const sumEl = document.getElementById('orphan-modal-summary'); 6495 const approveBtn = document.getElementById('orphan-modal-approve'); 6496 if (sumEl) sumEl.textContent = found.length 6497 ? `${totalCoupons} ×§×פ×× ×× ×××××× ×-${found.length} ×× ×××ת · ${leftover} ×§×פ×× ×× ××ש×ר×` 6498 : ''; 6499 if (approveBtn) { 6500 approveBtn.disabled = found.length === 0; 6501 approveBtn.style.opacity = found.length > 0 ? '1' : '0.4'; 6502 } 6503} 6504 6505function _orphanModalClose() { 6506 document.getElementById('orphan-search-modal').style.display = 'none'; 6507} 6508 6509async function _orphanModalApprove() { 6510 const merges = _orphanSearchResults 6511 .filter(d => d.matched_store_id) 6512 .map(d => ({ orphan_name: d.orphan_name, target_store_id: d.matched_store_id })); 6513 if (!merges.length) return; 6514 const btn = document.getElementById('orphan-modal-approve'); 6515 btn.disabled = true; 6516 btn.textContent = 'â³ ××××...'; 6517 try { 6518 const r = await fetch('/api/admin/coupons/orphans/merge', { 6519 method: 'POST', 6520 headers: { 'Content-Type': 'application/json' }, 6521 body: JSON.stringify({ merges }) 6522 }); 6523 const d = await r.json(); 6524 if (!r.ok) throw new Error(d.detail || d.message || `HTTP ${r.status}`); 6525 _orphanModalClose(); 6526 showToast(`â ××××× ${d.merged_coupons} ×§×פ×× ×× ×-${merges.length} ×× ×××ת`); 6527 loadOrphanCoupons(); 6528 } catch(e) { 6529 alert('ש×××× ××××××: ' + e.message); 6530 btn.disabled = false; 6531 btn.textContent = '×שר ×××××'; 6532 } 6533} 6534 6535async function loadOrphanCoupons() { 6536 const tbody = document.getElementById('orphan-tbody'); 6537 if (!tbody) return; 6538 tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr>`; 6539 try { 6540 const res = await fetch('/api/admin/coupons/orphans'); 6541 const data = await res.json(); 6542 _updateOrphanBadge(data.length); 6543 const delBtn = document.getElementById('orphan-delete-btn'); 6544 if (delBtn) delBtn.style.display = data.length > 0 ? '' : 'none'; 6545 const searchBtn = document.getElementById('orphan-search-btn'); 6546 if (searchBtn) searchBtn.style.display = data.length > 0 ? '' : 'none'; 6547 if (!data.length) { 6548 tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--accent);padding:28px">â ××× ×§×פ×× ×× ×ת××××</td></tr>`; 6549 return; 6550 } 6551 tbody.innerHTML = data.map((c) => { 6552 const src = (c.source || 'â').replace(/</g,'<'); 6553 const date = c.created_at ? c.created_at.slice(0,10) : 'â'; 6554 return `<tr> 6555 <td style="padding:6px 4px 6px 10px"> 6556 <input type="checkbox" class="orphan-row-check" data-id="${c.id}"
6557 onchange="_orphanCheckChanged()" 6558 style="cursor:pointer;accent-color:var(--accent)"> 6559 </td> 6560 <td style="font-weight:600;color:#fca5a5">${c.store}</td> 6561 <td style="font-family:monospace;color:var(--white)">${c.code || 'â'}</td> 6562 <td style="font-size:11px;color:var(--gray5);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${src}">${src}</td> 6563 <td style="font-size:11px;color:var(--gray5)">${date}</td> 6564 </tr>`; 6565 }).join(''); 6566 } catch(e) { 6567 tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:#fc8181;padding:24px">ש×××× ×××¢×× ×</td></tr>`; 6568 } 6569} 6570 6571function _updateOrphanBadge(count) { 6572 const badge = document.getElementById('src-orphan-count'); 6573 if (!badge) return; 6574 badge.textContent = count || '0'; 6575 if (count > 0) { 6576 badge.style.background = '#7f1d1d'; 6577 badge.style.color = '#fca5a5'; 6578 } else { 6579 badge.style.background = ''; 6580 badge.style.color = ''; 6581 } 6582} 6583 6584async function _refreshOrphanCount() { 6585 try { 6586 const r = await fetch('/api/admin/coupons/orphans'); 6587 const d = await r.json(); 6588 _updateOrphanBadge(d.length); 6589 } catch(_) {} 6590} 6591 6592async function _deleteOrphanCoupons() { 6593 if (!confirm('×××××§ ×ת ×× ××§×פ×× ×× ××ת××××?')) return; 6594 const btn = document.getElementById('orphan-delete-btn'); 6595 if (btn) { btn.disabled = true; btn.textContent = 'â³ ××××§...'; } 6596 try { 6597 const r = await fetch('/api/admin/coupons/delete-orphans', { method: 'DELETE' }); 6598 const d = await r.json(); 6599 showToast(`ð × ×××§× ${d.coupons_deleted} ×§×פ×× ×× ×ת××××`); 6600 loadOrphanCoupons(); 6601 } catch(e) { 6602 alert('ש×××× ×××××§×'); 6603 if (btn) { btn.disabled = false; btn.textContent = 'ð ×××§ ×××'; } 6604 } 6605} 6606 6607function _orphanCheckChanged() { 6608 const all = [...document.querySelectorAll('.orphan-row-check')]; 6609 const checked = all.filter(c => c.checked); 6610 const countEl = document.getElementById('orphan-selected-count'); 6611 const selBtn = document.getElementById('orphan-delete-selected-btn'); 6612 const allChk = document.getElementById('orphan-check-all'); 6613 if (countEl) countEl.textContent = checked.length; 6614 if (selBtn) selBtn.style.display = checked.length > 0 ? '' : 'none'; 6615 if (allChk) allChk.indeterminate = checked.length > 0 && checked.length < all.length; 6616 if (allChk) allChk.checked = all.length > 0 && checked.length === all.length; 6617} 6618 6619function _orphanToggleAll(checked) { 6620 document.querySelectorAll('.orphan-row-check').forEach(c => c.checked = checked); 6621 _orphanCheckChanged(); 6622} 6623 6624async function _deleteSelectedOrphans() { 6625 const ids = [...document.querySelectorAll('.orphan-row-check:checked')].map(c => c.dataset.id); 6626 if (!ids.length) return; 6627 if (!confirm(`×××××§ ${ids.length} ×§×פ×× ×× × ××ר××?`)) return; 6628 const btn = document.getElementById('orphan-delete-selected-btn'); 6629 if (btn) { btn.disabled = true; btn.textContent = 'â³ ××××§...'; } 6630 try { 6631 const r = await fetch('/api/admin/coupons/bulk-delete', { 6632 method: 'POST', 6633 headers: { 'Content-Type': 'application/json' }, 6634 body: JSON.stringify({ ids }) 6635 }); 6636 const d = await r.json(); 6637 if (!r.ok) throw new Error(d.detail || d.message || `HTTP ${r.status}`); 6638 showToast(`ð × ×××§× ${d.deleted} ×§×פ×× ××`); 6639 loadOrphanCoupons(); 6640 } catch(e) { 6641 alert('ש×××× ×××××§×: ' + e.message); 6642 if (btn) { btn.disabled = false; btn.textContent = `ð ×××§ × ××ר××`; } 6643 } 6644} 6645 6646async function _ndDeleteOrphans() { 6647 if (!confirm('×××××§ ×ת ×× ××§×פ×× ×× ×©××× ×ת ש××× ××ר ×× ×§×××ת?')) return; 6648 const btn = document.getElementById('nd-delete-orphans-btn'); 6649 if (btn) { btn.disabled = true; btn.textContent = 'â³ ××××§...'; } 6650 try { 6651 const r = await fetch('/api/admin/coupons/delete-orphans', { method: 'DELETE' }); 6652 const d = await r.json(); 6653 showToast(`ð§¹ × ×××§× ${d.coupons_deleted} ×§×פ×× ×× ×ת×××× (${d.orphan_stores} ×× ×××ת)`); 6654 } catch(e) { 6655 alert('ש×××× ×××××§×'); 6656 } finally { 6657 if (btn) { btn.disabled = false; btn.textContent = 'ð§¹ ×××§ ×§×פ×× ×× ×ת××××'; } 6658 } 6659} 6660 6661async function _ndSave(storeId) { 6662 const nameInput = document.getElementById('nd-name-' + storeId); 6663 const heInput = document.getElementById('nd-he-' + storeId); 6664 const domainInput = document.getElementById('nd-domain-' + storeId); 6665 if (!nameInput || !heInput || !domainInput) return; 6666 6667 const name = nameInput.value.trim(); 6668 const he = heInput.value.trim(); 6669 const domain = domainInput.value.trim(); 6670 6671 // All 3 mandatory 6672 if (!name) { nameInput.style.borderColor = '#fc8181'; nameInput.focus(); showToast('â ï¸ ×©× ×× ××× ××××'); return; } 6673 if (!he) { heInput.style.borderColor = '#fc8181'; heInput.focus(); showToast('â ï¸ ×©× ×¢××¨× ××××'); return; } 6674 if (!domain) { domainInput.style.borderColor = '#fc8181'; domainInput.focus();
6674 showToast('â ï¸ ×××××× ××××'); return; } 6675 6676 // Language validation 6677 if (/[×-תװ-×´]/.test(name)) { nameInput.style.borderColor = '#fc8181'; showToast('â ï¸ ×©× ×× ××× â ××ת××ת ×××× ××ת ××××'); return; } 6678 if (/[a-zA-Z]/.test(he) && /[×-תװ-×´]/.test(he)) { heInput.style.borderColor = '#fc8181'; showToast('â ï¸ ×©× ×¢××¨× â ×× ×ער×× ×¢×ר×ת ××× ×××ת'); return; } 6679 6680 const row = nameInput.closest('tr'); 6681 const btns = row ? row.querySelectorAll('button') : []; 6682 btns.forEach(b => b.disabled = true); 6683 6684 try { 6685 const r = await fetch(`/api/admin/stores/${storeId}/set-domain`, { 6686 method: 'POST', 6687 headers: {'Content-Type':'application/json'}, 6688 body: JSON.stringify({ domain, name, store_he: he }) 6689 }); 6690 if (r.ok) { 6691 row.style.opacity = '0.4'; 6692 row.style.pointerEvents = 'none'; 6693 showToast('â × ×©×ר'); 6694 setTimeout(loadNullDomainStores, 600); 6695 } else { 6696 const err = await r.json().catch(() => ({})); 6697 showToast('â ï¸ ' + (err.detail || 'ש×××× ×ש××ר×')); 6698 btns.forEach(b => b.disabled = false); 6699 } 6700 } catch(e) { 6701 showToast('ש×××× ×ש××ר×'); 6702 btns.forEach(b => b.disabled = false); 6703 } 6704} 6705 6706// ââ Store Coupons Modal (××× ××××××) âââââââââââââââââââââââââââââââââââââ 6707let _ndcmSourceId = null; 6708let _ndcmTargetId = null; 6709let _ndcmAllStores = []; // stores WITH domain, for the target picker 6710 6711async function _ndShowCoupons(storeId, storeName) { 6712 _ndcmSourceId = storeId; 6713 _ndcmTargetId = null; 6714 _ndcmAllStores = []; 6715 6716 const modal = document.getElementById('nd-coupons-modal'); 6717 document.getElementById('ndcm-title').textContent = storeName; 6718 document.getElementById('ndcm-subtitle').textContent = '×××¢× ×§×פ×× ××...'; 6719 document.getElementById('ndcm-coupons').innerHTML = '<div style="padding:24px;text-align:center;color:var(--gray5)">×××¢×...</div>'; 6720 document.getElementById('ndcm-search').value = ''; 6721 document.getElementById('ndcm-dropdown').style.display = 'none'; 6722 document.getElementById('ndcm-target-preview').textContent = ''; 6723 const confirmBtn = document.getElementById('ndcm-confirm-btn'); 6724 confirmBtn.disabled = true; 6725 confirmBtn.style.opacity = '0.45'; 6726 modal.style.display = 'flex'; 6727 6728 try { 6729 const [couponsRes, storesRes] = await Promise.all([ 6730 fetch(`/api/admin/stores/${storeId}/coupons`), 6731 fetch('/api/admin/stores/all'), 6732 ]); 6733 const couponsData = await couponsRes.json(); 6734 const allStores = await storesRes.json(); 6735 6736 _ndcmAllStores = allStores.filter(s => s.domain && s.id !== storeId); 6737 6738 const coupons = couponsData.coupons || []; 6739 document.getElementById('ndcm-subtitle').textContent = 6740 `${coupons.length} ×§×פ×× ×× Â· ${couponsData.store}`; 6741 6742 if (!coupons.length) { 6743 document.getElementById('ndcm-coupons').innerHTML = 6744 '<div style="padding:24px;text-align:center;color:var(--gray5)">××× ×§×פ×× ×× ××× ×ת ××</div>'; 6745 return; 6746 } 6747 6748 const STATUS_COLORS = { active:'#00e87a', inactive:'#fc8181', unknown:'#888', pending_review:'#ffb347' }; 6749 document.getElementById('ndcm-coupons').innerHTML = coupons.map(c => { 6750 const statusColor = STATUS_COLORS[c.status] || '#888'; 6751 const expiry = c.expiry ? `×¢× ${c.expiry.slice(0,10)}` : ''; 6752 const src = (c.source || '').replace(/\[primary\]\|/,'').slice(0,60); 6753 return `<div style="border:1px solid var(--gray3);border-radius:8px;padding:10px 14px;margin-bottom:8px;background:var(--gray1)"> 6754 <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap"> 6755 <span style="font-family:monospace;font-size:14px;font-weight:700;color:var(--accent);letter-spacing:1px">${c.code}</span> 6756 <span style="font-size:13px;color:var(--white)">${c.discount || ''}</span> 6757 <span style="font-size:11px;color:${statusColor};background:${statusColor}22;border:1px solid ${statusColor}44;border-radius:4px;padding:1px 6px">${c.status || ''}</span>
6758 ${expiry ? `<span style="font-size:11px;color:var(--gray5)">${expiry}</span>` : ''} 6759 <span style="font-size:11px;color:var(--gray5);margin-right:auto">ð ${c.votes||0}</span> 6760 </div> 6761 ${c.notes ? `<div style="font-size:11px;color:var(--gray6);margin-top:4px">${c.notes}</div>` : ''} 6762 ${src ? `<div style="font-size:10px;color:var(--gray4);margin-top:3px;direction:ltr;text-align:left">${src}</div>` : ''} 6763 </div>`; 6764 }).join(''); 6765 6766 } catch(e) { 6767 document.getElementById('ndcm-coupons').innerHTML = 6768 '<div style="padding:24px;text-align:center;color:#fc8181">ש×××× ×××¢×× ×</div>'; 6769 } 6770} 6771 6772function _ndCloseCouponsModal() { 6773 document.getElementById('nd-coupons-modal').style.display = 'none'; 6774 document.getElementById('ndcm-dropdown').style.display = 'none'; 6775 _ndcmSourceId = null; 6776 _ndcmTargetId = null; 6777} 6778 6779function _ndcmFilter() { 6780 const q = (document.getElementById('ndcm-search')?.value || '').toLowerCase().trim(); 6781 const dd = document.getElementById('ndcm-dropdown'); 6782 if (!q) { dd.style.display = 'none'; return; } 6783 6784 const filtered = _ndcmAllStores.filter(s => 6785 s.name.toLowerCase().includes(q) || 6786 (s.store_he || '').toLowerCase().includes(q) || 6787 (s.domain || '').toLowerCase().includes(q) 6788 ).slice(0, 30); 6789 6790 if (!filtered.length) { 6791 dd.innerHTML = '<div style="padding:10px 14px;font-size:12px;color:var(--gray5)">×× × ×צ×× ×× ×××ת ×¢× ××××××</div>'; 6792 } else { 6793 dd.innerHTML = filtered.map(s => ` 6794 <div onclick="_ndcmSelectTarget('${s.id}','${s.name.replace(/'/g,"\\'")}','${s.domain}')" 6795 style="padding:9px 14px;cursor:pointer;border-bottom:1px solid var(--gray3);display:flex;justify-content:space-between;font-size:13px;color:var(--gray6)" 6796 onmouseover="this.style.background='rgba(255,255,255,0.05)'" onmouseout="this.style.background=''"> 6797 <span><strong style="color:var(--white)">${s.name}</strong>${s.store_he ? ' · ' + s.store_he : ''}</span> 6798 <span style="font-size:11px;color:var(--gray5)">${s.domain}</span> 6799 </div>`).join(''); 6800 } 6801 dd.style.display = 'block'; 6802} 6803 6804function _ndcmSelectTarget(targetId, targetName, targetDomain) { 6805 _ndcmTargetId = targetId; 6806 document.getElementById('ndcm-search').value = targetName; 6807 document.getElementById('ndcm-dropdown').style.display = 'none'; 6808 document.getElementById('ndcm-target-preview').innerHTML = 6809 `â <strong style="color:var(--white)">${targetName}</strong> <span style="color:var(--gray5)">(${targetDomain})</span>`; 6810 const btn = document.getElementById('ndcm-confirm-btn'); 6811 btn.disabled = false; 6812 btn.style.opacity = '1'; 6813} 6814 6815async function _ndcmConfirmMove() { 6816 if (!_ndcmSourceId || !_ndcmTargetId) return; 6817 const targetName = document.getElementById('ndcm-search').value; 6818 if (!confirm(`×××¢××ר ×ת ×× ××§×פ×× ×× ××× ×ת "${targetName}" ××××××§ ×ת ××× ×ת ×× ××××ת?`)) return; 6819 6820 const btn = document.getElementById('ndcm-confirm-btn'); 6821 btn.disabled = true; 6822 btn.textContent = 'â³ ××¢××ר...'; 6823 6824 try { 6825 const r = await fetch(`/api/admin/stores/${_ndcmSourceId}/merge-into/${_ndcmTargetId}`, { method: 'POST' }); 6826 if (r.ok) { 6827 const data = await r.json(); 6828 _ndCloseCouponsModal(); 6829 const dupeNote = data.coupons_deduplicated > 0 ? ` · ${data.coupons_deduplicated} ×פ×××× × ×××§×` : ''; 6830 showToast(`â ×××¢×ר ×ת×× "${data.merged_into}" · ${data.coupons_moved} ×§×פ×× ××${dupeNote}`); 6831 loadNullDomainStores(); 6832 } else { 6833 const err = await r.json().catch(() => ({})); 6834 showToast('â ï¸ ×©××××: ' + (err.detail || '××¢××¨× × ×ש××')); 6835 } 6836 } catch(e) { 6837 showToast('ש×××× ×××¢×ר×'); 6838 } finally { 6839 btn.disabled = false; 6840 btn.textContent = '××¢×ר ××××§ âµ'; 6841 btn.style.opacity = '1'; 6842 } 6843} 6844// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 6845 6846async function loadTrustedInternational() { 6847 const tbody = document.getElementById('trusted-intl-tbody'); 6848 if (!tbody) return; 6849 tbody.innerHTML = `<tr><td colspan="4" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr>`; 6850 try { 6851 const items = await fetch('/api/admin/trusted-international').then(r => r.json()); 6852 const badge = document.getElementById('src-trusted-intl-count'); 6853 if (badge) badge.textContent = items.length || '0'; 6854 if (!items.length) { 6855 tbody.innerHTML = `<tr><td colspan="4" style="text-align:center;color:var(--gray5);padding:28px">××× ×××××× ×× ×רש×××</td></tr>`; 6856 return; 6857 } 6858 tbody.innerHTML = items.map((s, i) => { 6859 const date = s.created_at ? new Date(s.created_at).toLocaleDateString('he-IL') : 'â'; 6860 return `<tr style="border-bottom:1px solid var(--gray3)"> 6861 <td style="padding:10px 12px;color:var(--gray5);font-size:12px">${i + 1}</td> 6862 <td style="padding:10px 12px"> 6863 <a href="https://${s.domain}" target="_blank" style="color:var(--accent);text-decoration:none;font-weight:600" dir="ltr">${s.domain}</a> 6864 </td> 6865 <td style="padding:10px 12px;color:var(--gray6);font-size:13px">${date}</td> 6866 <td style="padding:10px 12px;text-align:center"> 6867 <button class="btn-sm btn-ghost" style="color:#fc8181" onclick="_tiDelete('${s.domain}',this)">×××§</button> 6868 </td> 6869 </tr>`; 6870 }).join(''); 6871 } catch(e) { 6872 tbody.innerHTML = `<tr><td colspan="4" style="text-align:center;color:#fc8181;padding:24px">ש×××× ×××¢×× ×</td></tr>`; 6873 } 6874} 6875 6876async function _tiAddPrompt() {
6877 const domain = prompt('××× ×¡ ×××××× ××× ××××× (××××××: nike.com):'); 6878 if (!domain) return; 6879 try { 6880 const res = await fetch('/api/admin/trusted-international', { 6881 method: 'POST', 6882 headers: {'Content-Type':'application/json'}, 6883 body: JSON.stringify({domain: domain.trim()}) 6884 }); 6885 const data = await res.json(); 6886 if (data.ok) { showToast(`â ${data.domain} × ×סף ×רש×××`); loadTrustedInternational(); } 6887 else showToast('â ש×××× ×××ספ×'); 6888 } catch(e) { showToast('â ש×××× ×××ספ×'); } 6889} 6890 6891async function _tiDelete(domain, btn) { 6892 if (!confirm(`×××××§ ×ת ${domain} ××רש×××?\n×§×פ×× ×× ××ש×× ××× × ××¨× promocodis ××××××.`)) return; 6893 btn.disabled = true; 6894 try { 6895 await fetch(`/api/admin/trusted-international/${encodeURIComponent(domain)}`, {method:'DELETE'}); 6896 showToast(`â ${domain} ××סר ××רש×××`); 6897 loadTrustedInternational(); 6898 } catch(e) { btn.disabled = false; showToast('â ש×××× ×××××§×'); } 6899} 6900 6901async function loadBlockedSites() { 6902 const tbody = document.getElementById('blocked-sites-tbody'); 6903 if (!tbody) return; 6904 tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr>`; 6905 try { 6906 const sites = await fetch('/api/admin/blocked-sites').then(r => r.json()); 6907 const badge = document.getElementById('src-blocked-count'); 6908 if (badge) badge.textContent = sites.length || '0'; 6909 if (!sites.length) { 6910 tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--gray5);padding:28px">××× ×תר×× ×ס××××</td></tr>`; 6911 return; 6912 } 6913 tbody.innerHTML = sites.map((s, i) => { 6914 const domain = s.domain || (s.url || '').replace(/^https?:\/\/(www\.)?/, '').split('/')[0]; 6915 return `<tr style="border-bottom:1px solid var(--gray3)"> 6916 <td style="padding:10px 12px;color:var(--gray5);font-size:12px">${i + 1}</td> 6917 <td style="padding:10px 12px"> 6918 <a href="${s.url}" target="_blank" style="color:var(--accent);text-decoration:none;font-weight:600">${s.name || domain}</a> 6919 <div style="font-size:11px;color:var(--gray5);margin-top:2px" dir="ltr">${domain}</div> 6920 </td> 6921 <td style="padding:10px 12px;color:var(--gray6);font-size:13px">${s.description || 'â'}</td> 6922 <td style="padding:10px 12px;color:var(--gray6);font-size:13px">${s.block_reason || 'â'}</td> 6923 <td style="padding:10px 12px;text-align:center"> 6924 <button class="btn-sm btn-ghost" onclick="_bsUnblock('${s.id}','${s.name || domain}',this)">â© ×סר ×ס×××</button> 6925 </td> 6926 </tr>`; 6927 }).join(''); 6928 } catch(e) { 6929 tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:#fc8181;padding:24px">ש×××× ×××¢×× ×</td></tr>`; 6930 } 6931} 6932 6933async function _bsUnblock(siteId, name, btn) { 6934 if (!confirm(`××ס×ר ×ת ××ס××× ×©× ${name}?\n××תר ×× ××פ××¢ ××תר ××ס××.`)) return; 6935 btn.disabled = true; 6936 try { 6937 await fetch(`/api/admin/blocked-sites/${siteId}/unblock`, {method:'PATCH'}); 6938 showToast(`â ${name} ××סר ×רש××ת ××תר×× ××ס××××`); 6939 loadBlockedSites(); 6940 } catch(e) { 6941 btn.disabled = false; 6942 showToast('â ש×××× ××סרת ××ס×××'); 6943 } 6944} 6945 6946async function loadNoUrlCoupons() { 6947 const tbody = document.getElementById('nourl-tbody'); 6948 if (!tbody) return; 6949 tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:var(--gray5);padding:24px">×××¢×...</td></tr>`; 6950 try { 6951 const res = await fetch('/api/admin/no-url-coupons'); 6952 _noUrlData = await res.json(); 6953 _noUrlPage = 0; 6954 _noUrlExpanded.clear(); 6955 const badge = document.getElementById('src-nourl-count'); 6956 if (badge) badge.textContent = _noUrlData.length || '0'; 6957 _renderNoUrlTable(); 6958 } catch(e) { 6959 tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:#fc8181;padding:24px">ש×××× ×××¢×× ×</td></tr>`; 6960 } 6961} 6962 6963function _noUrlSetGeo(geo, btn) { 6964 _noUrlGeo = geo; 6965 _noUrlPage = 0; 6966 document.querySelectorAll('.nourl-geo-btn').forEach(b => b.classList.remove('on')); 6967 btn.classList.add('on'); 6968 _renderNoUrlTable(); 6969} 6970 6971function _noUrlToggle(store) { 6972 if (_noUrlExpanded.has(store)) _noUrlExpanded.delete(store); 6973 else _noUrlExpanded.add(store); 6974 _renderNoUrlTable(); 6975} 6976 6977function _renderNoUrlTable() { 6978 const tbody = document.getElementById('nourl-tbody'); 6979 const pagEl = document.getElementById('nourl-pagination'); 6980 if (!tbody) return; 6981 6982 const filtered = _noUrlFilteredData(); 6983 const pages = Math.max(1, Math.ceil(filtered.length / _noUrlPerPg)); 6984 if (_noUrlPage >= pages) _noUrlPage = 0; 6985 const slice = filtered.slice(_noUrlPage * _noUrlPerPg, (_noUrlPage + 1) * _noUrlPerPg); 6986 6987 if (!filtered.length) { 6988 tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:var(--accent);padding:32px">â ×× ××× ×××ת ×¢× ×§×פ×× ×× ×ש ××× ×תר ××¢×</td></tr>`; 6989 if (pagEl) pagEl.innerHTML = ''; 6990 return; 6991 } 6992 6993 const globalOffset = _noUrlPage * _noUrlPerPg; 6994 6995 tbody.innerHTML = slice.map((r, localIdx) => { 6996 const globalIdx = globalOffset + localIdx; 6997 const safeStore = r.store.replace(/'/g,"\\'"); 6998 const inputId = `nourl-domain-${globalIdx}`; 6999 const expanded = _noUrlExpanded.has(r.store); 7000 const isIL = _noUrlIsIL(r); 7001 const geoBadge = isIL 7002 ? `<span style="font-size:10px;background:rgba(0,232,122,0.12);color:var(--accent);border-radius:10px;padding:1px 6px;margin-right:5px">ð®ð±</span>` 7003 : `<span style="font-size:10px;background:rgba(99,179,237,0.12);color:#63b3ed;border-radius:10px;padding:1px 6px;margin-right:5px">ð</span>`;
7004 const suggested = _noUrlGuessDomain(r.coupons); 7005 7006 // Build expandable coupon sub-rows 7007 const expandRows = expanded ? r.coupons.map(c => { 7008 const code = (c.code||'').toUpperCase() === 'NO_CODE' ? 'â' : (c.code||'â'); 7009 const dt = c.created_at ? new Date(c.created_at).toLocaleDateString('he-IL',{day:'2-digit',month:'2-digit',year:'2-digit'}) : ''; 7010 const fullUrl = _noUrlParseUrl(c.source||''); 7011 let srcLink; 7012 if (fullUrl) { 7013 let domain = fullUrl; 7014 try { domain = new URL(fullUrl).hostname.replace(/^www\./, ''); } catch(e) {} 7015 const isTg = fullUrl.includes('t.me/'); 7016 srcLink = `<a href="${fullUrl}" target="_blank" title="${fullUrl}" 7017 style="color:${isTg?'#29b6f6':'var(--accent)'};font-size:11px;white-space:nowrap"> 7018 ${isTg?'ð¨ ':''} ${domain}</a>`; 7019 } else { 7020 srcLink = `<span style="color:var(--gray5);font-size:11px">${(c.source||'â').slice(0,60)}</span>`; 7021 } 7022 return `<tr style="background:rgba(255,255,255,0.02);border-top:1px solid var(--gray3)"> 7023 <td></td> 7024 <td style="padding-right:28px;font-size:11px;color:var(--gray5);white-space:nowrap">${dt}</td> 7025 <td style="font-family:monospace;font-size:12px;color:var(--accent2);text-align:center">${code}</td> 7026 <td style="font-size:12px;white-space:nowrap">${c.discount||'â'}</td> 7027 <td style="font-size:11px;color:var(--gray6)">${(c.notes||'').slice(0,50)}</td> 7028 <td colspan="2" style="max-width:300px">${srcLink}</td> 7029 </tr>`; 7030 }).join('') : ''; 7031 7032 return `<tr id="nourl-row-${globalIdx}" style="border-bottom:${expanded?'none':'1px solid var(--gray3)'}"> 7033 <td style="color:var(--gray5);font-size:11px;text-align:center;white-space:nowrap">${globalIdx+1}</td> 7034 <td style="font-weight:600;color:var(--white)"> 7035 ${geoBadge}${r.store} 7036 <button onclick="_noUrlToggle('${safeStore}')" 7037 title="${expanded?'ס××ר':'×¤×ª× ×§×פ×× ××'}" 7038 style="margin-right:8px;background:none;border:1px solid var(--gray4);border-radius:4px;color:var(--gray6);font-size:11px;padding:1px 7px;cursor:pointer;line-height:1.4"> 7039 ${expanded ? 'Ⲡס××ר' : `â¼ ${r.count} ×§×פ×× ××`} 7040 </button> 7041 </td> 7042 <td> 7043 <div style="display:flex;align-items:center;gap:6px"> 7044 <input id="${inputId}" type="text" placeholder="example.co.il" value="${suggested}" 7045 style="width:160px;background:var(--gray3);border:1px solid ${suggested?'var(--accent)':'var(--gray4)'};border-radius:6px;padding:5px 8px;color:var(--white);font-size:12px;outline:none;box-sizing:border-box" 7046 onkeydown="if(event.key==='Enter')_noUrlSave(${globalIdx},'${safeStore}')"> 7047 ${suggested ? `<span style="font-size:10px;color:var(--accent);white-space:nowrap" title="××צע ×××××××ת ××ת××ת ×××§×ר">⦠×צע×</span>` : ''} 7048 </div> 7049 </td> 7050 <td colspan="4"> 7051 <div style="display:flex;gap:6px;align-items:center"> 7052 <button class="td-btn" onclick="_noUrlSave(${globalIdx},'${safeStore}')">ð¾ ש××ר</button> 7053 <button class="td-btn danger" onclick="_noUrlDelete(${globalIdx},'${safeStore}')">ð ×××§ ×××</button> 7054 </div> 7055 </td> 7056 </tr> 7057 ${expandRows}`; 7058 }).join(''); 7059 7060 // Pagination 7061 if (pagEl) { 7062 if (pages <= 1) { pagEl.innerHTML = ''; return; } 7063 const nums = _pageNums(_noUrlPage, pages); 7064 pagEl.innerHTML = `<div style="display:flex;gap:6px;justify-content:center;padding:16px 0;flex-wrap:wrap">` + 7065 nums.map(n => n === 'â¦' 7066 ? `<span style="color:var(--gray5);padding:0 4px;font-size:13px">â¦</span>` 7067 : `<button class="td-btn${n-1===_noUrlPage?' page-active':''}" onclick="_noUrlGoPage(${n-1})" style="min-width:32px">${n}</button>` 7068 ).join('') + `</div>`; 7069 } 7070} 7071 7072function _noUrlGoPage(p) { _noUrlPage = p; _renderNoUrlTable(); } 7073 7074async function _noUrlSave(idx, store) { 7075 const input = document.getElementById(`nourl-domain-${idx}`); 7076 const domain = (input?.value || '').trim();
7077 if (!domain) { showToast('â ï¸ ××× ×¡ ××××××'); return; } 7078 try { 7079 const res = await fetch('/api/admin/no-url-coupons/set-domain', { 7080 method: 'POST', headers: {'Content-Type':'application/json'}, 7081 body: JSON.stringify({ store, domain }), 7082 }); 7083 const d = await res.json(); 7084 if (!res.ok) { showToast(`â ${d.detail||'ש××××'}`); return; } 7085 showToast(`â ${store} â ${d.domain}`); 7086 _noUrlData = _noUrlData.filter(r => r.store !== store); 7087 _noUrlExpanded.delete(store); 7088 const badge = document.getElementById('src-nourl-count'); 7089 if (badge) badge.textContent = _noUrlData.length; 7090 _renderNoUrlTable(); 7091 } catch(e) { showToast('â ש×××ת רשת'); } 7092} 7093 7094async function _noUrlDelete(idx, store) { 7095 if (!confirm(`××××§ ×ת ×× ××§×פ×× ×× ×©× "${store}"?`)) return; 7096 try { 7097 const res = await fetch('/api/admin/no-url-coupons/delete-store', { 7098 method: 'DELETE', headers: {'Content-Type':'application/json'}, 7099 body: JSON.stringify({ store }), 7100 }); 7101 const d = await res.json(); 7102 if (!res.ok) { showToast(`â ${d.detail||'ש××××'}`); return; } 7103 showToast(`ð × ×××§× ${d.deleted} ×§×פ×× ×× ×©× ${store}`); 7104 _noUrlData = _noUrlData.filter(r => r.store !== store); 7105 _noUrlExpanded.delete(store); 7106 const badge = document.getElementById('src-nourl-count'); 7107 if (badge) badge.textContent = _noUrlData.length; 7108 _renderNoUrlTable(); 7109 } catch(e) { showToast('â ש×××ת רשת'); } 7110} 7111 7112// ââ Affiliate programs ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 7113 7114const _AFF_LIST = [ 7115 // ââ Israeli networks (register once â see catalog of Israeli brands) ââââââââ 7116 { id:'wesell', type:'il-network', icon:'ð®ð±', name:'WeSell', 7117 desc:'×רשת ×××××× ×××תר ××שר×× â ×××ת ×§×פ××× ××, 10,000+ ש×תפ××. ×××¨×©× ××××ת ×××× ×× ×××ת ×שר××××ת ×©× (Castro, Golf, Fox ××¢××).', 7118 stores:['Castro','Golf','Fox','Renuar','Adika','ACE','Home Center','Steimatzky','Delta','Vardinon','Shilav','Fattal','Issta'], 7119 commission:'××©×ª× ×', url:'https://wesell.co.il' }, 7120 { id:'affiracle', type:'il-network', icon:'â¨', name:'Affiracle', 7121 desc:'פ××פ×ר×× ×שר×××ת ×-100+ ××ת××× ××§××××× â ×××¤× ×, ××§ ××××¦×¨× ××ת', 7122 stores:['Castro','Golf','Fox','Renuar','Adika','TerminalX','Delta','ACE','Home Center'], 7123 commission:'××©×ª× ×', url:'https://affiracle.com/en/' }, 7124 { id:'indoleads', type:'network', icon:'ð', name:'Indoleads', 7125 desc:'רשת ××××××ת ×¢× ×ספר ת××× ××ת ×שר××××ת ×××שר×ת', 7126 stores:['Ivory'], 7127 commission:'3â8%', url:'https://indoleads.com' }, 7128 { id:'tradedoubler', type:'network', icon:'ð', name:'TradeDoubler', 7129 desc:'רשת ××ר×פ××ת â Wolt ×שר×× ×¨×©××× ××× ×¨×©××ת', 7130 stores:['Wolt'], 7131 commission:'××©×ª× ×', url:'https://www.tradedoubler.com/en/become-a-publisher/' }, 7132 // ââ International networks âââââââââââââââââââââââââââââââââââââââââââââââââ
7133 { id:'impact', type:'network', icon:'ð¼', name:'Impact.com', 7134 desc:'×רשת ×××× ×××××ת ×××××× â ×××ת ××ת××× ××ר××§×××× ××××××××', 7135 stores:["Nike","Adidas","Target","Walmart","Levi's","HelloFresh","EveryPlate","Booking.com"], 7136 commission:'3â15%', url:'https://app.impact.com/signup/publisher' }, 7137 { id:'awin', type:'network', icon:'ð', name:'Awin', 7138 desc:'×רשת ×××ר×פ××ת ××××××× â ×××§× ×××××× ×××ת×× ×××¤× ×', 7139 stores:['H&M','ASOS','Adidas','Booking.com','Zara'], 7140 commission:'3â12%', url:'https://www.awin.com/gb/publishers' }, 7141 { id:'cj', type:'network', icon:'ð', name:'Commission Junction (CJ)', 7142 desc:'רשת ×ת××§× ×¢× ××ת×× ×××§×¨× ×××ר××§××××', 7143 stores:["Sephora","Nordstrom","Macy's","Ulta","Levi's","HelloFresh","American Express"], 7144 commission:'3â10%', url:'https://www.cj.com/publisher-signup' }, 7145 { id:'rakuten', type:'network', icon:'ð', name:'Rakuten Advertising', 7146 desc:'××ת×× ×¤×¨×××× â תש××××× ×××× ×× ××¢×××ת ××××ת', 7147 stores:['Sephora','Nordstrom','Walmart','Ulta'], 7148 commission:'2â10%', url:'https://rakutenadvertising.com/publishers/' }, 7149 { id:'shareasale', type:'network', icon:'ð', name:'ShareASale', 7150 desc:'×××¤× ××ת××× ×§×× ×× ×××× ×× ××× â ××××× ×¨××', 7151 stores:["Macy's",'Nordstrom'], 7152 commission:'5â20%', url:'https://www.shareasale.com/info/publisher.cfm' }, 7153 { id:'admitad', type:'network', icon:'ð', name:'Admitad', 7154 desc:'×××§ ×××ת××× ×××××××× ×ס×× ××× â AliExpress, Temu ××¢××', 7155 stores:['AliExpress','Temu','SHEIN'], 7156 commission:'3â9%', url:'https://publishers.admitad.com/en/register/' }, 7157 // ââ Direct programs ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 7158 { id:'amazon', type:'direct', icon:'ð¦', name:'Amazon Associates', 7159 desc:'1â10% ×¢× ×××¢× ×× ××צר. ×§× ×ש××, ת××× ×¨×××× ××.', 7160 stores:['Amazon'], 7161 commission:'1â10%', url:'https://affiliate-program.amazon.com' }, 7162 { id:'ebay', type:'direct', icon:'ð·', name:'eBay Partner Network', 7163 desc:'×¢××× ×¢× ×× ××××¨× ×©××××¢× ××¨× ××§×ש×ר ש××', 7164 stores:['eBay'], 7165 commission:'1â4%', url:'https://partnernetwork.ebay.com' }, 7166 { id:'iherb', type:'direct', icon:'ð¿', name:'iHerb Affiliate', 7167 desc:'5% ×¢××× â תש××× ××××ר××, ×××¦×¨× ×ר×××ת ×ת×ספ××', 7168 stores:['iHerb'], 7169 commission:'5%', url:'https://www.iherb.com/info/affiliates' }, 7170 { id:'shein', type:'direct', icon:'ð', name:'SHEIN Affiliate', 7171 desc:'10â20% ×¢××× â ×§×× ××¢× ×××× ×××× ××שר××', 7172 stores:['SHEIN'], 7173 commission:'10â20%', url:'https://affiliate.shein.com' }, 7174 { id:'booking', type:'direct', icon:'ð¨', name:'Booking.com Affiliate', 7175 desc:'25â40% ×××¢××× ×©× Booking â ×××××××× ×ש××§', 7176 stores:['Booking.com'], 7177 commission:'25â40% ×-Booking', url:'https://www.booking.com/affiliate-program/v2/index.html' }, 7178 { id:'temu', type:'direct', icon:'ð', name:'Temu Affiliate', 7179 desc:'×¢××× ×¢× ×¨××ש×ת ××ש×ת â ×§×× ×¦××× ×××ר', 7180 stores:['Temu'], 7181 commission:'5â20%', url:'https://www.temu.com/affiliate.html' }, 7182 { id:'aliexpress', type:'direct', icon:'ð¨ð³', name:'AliExpress Portals', 7183 desc:'ת××× ×ת ×ש××¨× ×©× AliExpress â ×¢××× ×¢× ×× ××צר', 7184 stores:['AliExpress'], 7185 commission:'3â9%', url:'https://portals.aliexpress.com' }, 7186 { id:'ksp', type:'direct', icon:'ð¥', name:'KSP ש×תפ××', 7187 desc:'ת××× ×ת ש×תפ×× ×¢×¦×××ת â 4% ×¢× ××ש×××, cookie 24 שע×ת, ××× ×××× 250 ש"×', 7188 stores:['KSP'], 7189 commission:'4%', url:'https://ksp.co.il/af/' }, 7190 { id:'isrotel', type:'direct', icon:'ð', name:'Isrotel Affiliate', 7191 desc:'5% ×¢× ×¢×¨× ××××× × ×××× â עסק××ת ×××צע×ת 3,000 ש"×', 7192 stores:['Isrotel'], 7193 commission:'5%', url:'https://console.isrotel.co.il/aff/signup.php' }, 7194 { id:'ivory', type:'direct', icon:'ð¨', name:'Ivory Affiliate (Indoleads)', 7195 desc:'3.84% â ××ש××× ××××§×ר×× ××§×, ××¨× Indoleads', 7196 stores:['Ivory'], 7197 commission:'3.84%', url:'https://indoleads.com' }, 7198 { id:'wolt', type:'direct', icon:'ð', name:'Wolt Affiliates (TradeDoubler)', 7199 desc:'ת××× ×ת ש×תפ×× ×¨×©××ת ×©× Wolt â ××¨× ×¨×©×ª TradeDoubler', 7200 stores:['Wolt'], 7201 commission:'××©×ª× ×', url:'https://explore.wolt.com/en/isr/affiliates' }, 7202 { id:'10bis', type:'direct', icon:'ð', name:'10bis ש×תפ××', 7203 desc:'×§×× ××¤× ×× ××¢××× ×¢× ×××× ×ת ××××', 7204 stores:['10bis'], 7205 commission:'××©×ª× ×', url:'https://www.10bis.co.il' }, 7206 { id:'terminalx', type:'direct', icon:'ð ', name:'TerminalX ש×תפ××', 7207 desc:'××× ×××ª×¨× ××××¤× × ××××××× ××שר×× â ×¤× × ×ש×ר×ת', 7208 stores:['TerminalX'], 7209 commission:'5â8%', url:'https://www.terminalx.com' }, 7210 { id:'bug', type:'direct', icon:'ð»', name:'Bug ש×תפ××', 7211 desc:'×××§×ר×× ××§× â ×¤× × ×ש×ר×ת ××××£ ש××××§', 7212 stores:['Bug'], 7213 commission:'2â4%', url:'https://www.bug.co.il' }, 7214 { id:'superpharm', type:'direct', icon:'ð', name:'Super-Pharm ש×תפ××', 7215 desc:'פ×ר×× ××§×ס××××§× â ×¤× × ×ש×ר×ת ××××£ ש××××§', 7216 stores:['Super-Pharm'], 7217 commission:'3â6%', url:'https://www.super-pharm.co.il' }, 7218]; 7219 7220// Map store name â affiliate program id (for newsletter table column) 7221const _STORE_AFF_MAP = {}; 7222_AFF_LIST.forEach(p =>
7222 p.stores.forEach(s => { 7223 if (!_STORE_AFF_MAP[s]) _STORE_AFF_MAP[s] = []; 7224 _STORE_AFF_MAP[s].push(p.id); 7225})); 7226 7227const _AFF_STATUS_KEY = id => `aff_status_${id}`; 7228const _AFF_STATUS_LABELS = { 7229 '': { text:'×× ×ת××ת×', bg:'rgba(100,100,100,0.15)', color:'var(--gray5)', icon:'â' }, 7230 'pending': { text:'××××ª× × ×××ש×ר', bg:'rgba(255,228,94,0.12)', color:'var(--accent2)', icon:'â³' }, 7231 'registered': { text:'רש×× â ', bg:'rgba(0,232,122,0.12)', color:'var(--accent)', icon:'â ' }, 7232 'rejected': { text:'× ×××', bg:'rgba(255,80,80,0.1)', color:'#ff6b6b', icon:'â' }, 7233}; 7234const _AFF_STATUS_CYCLE = ['', 'pending', 'registered', 'rejected']; 7235 7236function _affGetStatus(id) { return localStorage.getItem(_AFF_STATUS_KEY(id)) || ''; } 7237function _affSetStatus(id, status) { 7238 localStorage.setItem(_AFF_STATUS_KEY(id), status); 7239 // Update the status button in-place â no re-sort, rows stay in position 7240 const row = document.querySelector(`tr[data-aff-id="${id}"]`); 7241 if (row) { 7242 const btn = row.querySelector('.aff-status-btn'); 7243 const stl = _AFF_STATUS_LABELS[status] || _AFF_STATUS_LABELS['']; 7244 if (btn) { 7245 btn.style.background = stl.bg; 7246 btn.style.color = stl.color; 7247 btn.style.border = `1px solid ${stl.color}33`; 7248 btn.innerHTML = `${stl.icon} ${stl.text}`; 7249 } 7250 } 7251 // update sidebar badge and stats bar 7252 const reg = _AFF_LIST.filter(p => _affGetStatus(p.id) === 'registered').length; 7253 const pend = _AFF_LIST.filter(p => _affGetStatus(p.id) === 'pending').length; 7254 const none = _AFF_LIST.filter(p => _affGetStatus(p.id) === '').length; 7255 const el = document.getElementById('src-aff-count'); 7256 if (el) el.textContent = reg || 'â'; 7257 const statsBar = document.getElementById('aff-stats-bar'); 7258 if (statsBar) statsBar.innerHTML = 7259 `<span style="color:var(--accent)">${reg}</span> רש×××× Â· ` + 7260 `<span style="color:var(--accent2)">${pend}</span> ××××ª× × Â· ` + 7261 `<span style="color:var(--gray5)">${none}</span> ×× ×ת×××ª× Â· ` + 7262 `ס×"× ${_AFF_LIST.length} ת××× ××ת`; 7263} 7264function _affCycleStatus(id) { 7265 const cur = _affGetStatus(id); 7266 const idx = _AFF_STATUS_CYCLE.indexOf(cur); 7267 const next = _AFF_STATUS_CYCLE[(idx + 1) % _AFF_STATUS_CYCLE.length]; 7268 _affSetStatus(id, next); 7269} 7270 7271let _affActiveFilter = 'all'; 7272let _affSortKey = 'status'; 7273let _affSortDir = 1; 7274 7275function _affSetFilter(f, btn) { 7276 _affActiveFilter = f; 7277 document.querySelectorAll('[id^="aff-f-"]').forEach(b => b.classList.remove('on')); 7278 btn.classList.add('on'); 7279 _affRender(); 7280} 7281function _affSetSortKey(key) { 7282 _affSortKey = key; _affSortDir = 1; 7283 const dirBtn = document.getElementById('aff-sort-dir-btn'); 7284 if (dirBtn) dirBtn.textContent = 'â'; 7285 _affRender(); 7286} 7287function _affToggleDir() { 7288 _affSortDir *= -1; 7289 const dirBtn = document.getElementById('aff-sort-dir-btn'); 7290 if (dirBtn) dirBtn.textContent = _affSortDir === 1 ? 'â' : 'â'; 7291 _affRender(); 7292} 7293 7294function _affRender() { 7295 const tbody = document.getElementById('aff-tbody'); 7296 const statsBar = document.getElementById('aff-stats-bar'); 7297 if (!tbody) return; 7298 7299 let rows = _AFF_LIST.slice(); 7300 const f = _affActiveFilter; 7301 if (f === 'network') rows = rows.filter(p => p.type === 'network'); 7302 if (f === 'direct') rows = rows.filter(p => p.type === 'direct'); 7303 if (f === 'il') rows = rows.filter(p => p.type === 'il' || p.type === 'il-network'); 7304 if (f === 'registered') rows = rows.filter(p => _affGetStatus(p.id) === 'registered'); 7305 if (f === 'pending') rows = rows.filter(p => _affGetStatus(p.id) === 'pending'); 7306 if (f === 'none') rows = rows.filter(p => _affGetStatus(p.id) === ''); 7307 7308 // sort 7309 if (_affSortKey === 'name') { 7310 rows.sort((a,b) => _affSortDir * a.name.localeCompare(b.name, 'he')); 7311 } else if (_affSortKey === 'commission') { 7312 rows.sort((a,b) => _affSortDir * (parseFloat(a.commission) - parseFloat(b.commission))); 7313 } else { // status (default): registered â pending â none â rejected 7314 const order = { registered:0, pending:1, '':2, rejected:3 }; 7315 rows.sort((a,b) => _affSortDir * ((order[_affGetStatus(a.id)]||2) - (order[_affGetStatus(b.id)]||2))); 7316 } 7317 7318 const total = _AFF_LIST.length; 7319 const reg = _AFF_LIST.filter(p => _affGetStatus(p.id) === 'registered').length; 7320 const pend = _AFF_LIST.filter(p => _affGetStatus(p.id) === 'pending').length; 7321 const none = _AFF_LIST.filter(p => _affGetStatus(p.id) === '').length; 7322 if (statsBar) statsBar.innerHTML = 7323 `<span style="color:var(--accent)">${reg}</span> רש×××× Â· ` + 7324 `<span style="color:var(--accent2)">${pend}</span> ××××ª× × Â· ` + 7325 `<span style="color:var(--gray5)">${none}</span> ×× ×ת×××ª× Â· ` + 7326 `ס×"× ${total} ת××× ××ת`; 7327 7328 const typeLabel = { 'il-network':'ð®ð± רשת ×שר×××ת', network:'ð רשת', direct:'ð¯ ×ש×ר', il:'ð®ð± ×שר×××' }; 7329 7330 tbody.innerHTML = rows.map(p => { 7331 const st = _affGetStatus(p.id); 7332 const stl = _AFF_STATUS_LABELS[st] || _AFF_STATUS_LABELS['']; 7333 const storesHtml = p.stores.length 7334 ? p.stores.map(s => `<span style="display:inline-block;background:var(--gray3);border-radius:4px;padding:1px 6px;font-size:10px;color:var(--gray6);margin:1px 2px">${s}</span>`).join('') 7335 : '<span style="color:var(--gray4);font-size:11px">××××</span>'; 7336 return `<tr data-aff-id="${p.id}" style="border-bottom:1px solid var(--gray2);transition:background .15s" onmouseover="this.style.background='var(--gray2)'" onmouseout="this.style.background=''"> 7337 <td style="padding:12px 12px"> 7338 <div style="display:flex;align-items:center;gap:8px">
7339 <span style="font-size:20px">${p.icon}</span> 7340 <div> 7341 <div style="font-size:13px;font-weight:600;color:var(--white)">${p.name}</div> 7342 <div style="font-size:11px;color:var(--gray5);margin-top:2px">${p.desc}</div> 7343 <div style="margin-top:3px"><span style="font-size:10px;color:var(--gray4);background:var(--gray2);border-radius:4px;padding:1px 5px">${typeLabel[p.type]||p.type}</span></div> 7344 </div> 7345 </div> 7346 </td> 7347 <td style="padding:12px 12px;max-width:220px">${storesHtml}</td> 7348 <td style="padding:12px 12px;text-align:center;font-size:12px;color:var(--accent2);font-weight:600;white-space:nowrap">${p.commission}</td> 7349 <td style="padding:12px 12px;text-align:center"> 7350 <button class="aff-status-btn" onclick="_affCycleStatus('${p.id}')" style="background:${stl.bg};color:${stl.color};border:1px solid ${stl.color}33;border-radius:20px;padding:4px 12px;font-size:11px;cursor:pointer;white-space:nowrap" title="×××¥ ××××פת ס×××ס">${stl.icon} ${stl.text}</button> 7351 </td> 7352 <td style="padding:12px 12px;text-align:center"> 7353 <a href="${p.url}" target="_blank" style="font-size:11px;color:var(--accent);text-decoration:none;background:rgba(0,232,122,0.08);border:1px solid rgba(0,232,122,0.2);border-radius:6px;padding:4px 10px;white-space:nowrap">×××¨×©× â</a> 7354 </td> 7355 </tr>`; 7356 }).join(''); 7357 7358 // update sidebar badge 7359 const el = document.getElementById('src-aff-count'); 7360 if (el) el.textContent = reg || 'â'; 7361} 7362 7363// ââ Newsletter scraper âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 7364 7365let _nlStores = []; 7366let _nlActiveFilter = 'all'; 7367let _nlSortKey = 'status'; 7368let _nlSortDir = 1; // 1 = asc, -1 = desc 7369 7370async function loadNewsletterStores(force = false) { 7371 const tbody = document.getElementById('nl-stores-tbody'); 7372 const statsBar = document.getElementById('nl-stats-bar'); 7373 if (!tbody) return; 7374 _nlSyncInterval(); 7375 if (!force && _nlStores.length) { _nlRender(); return; } 7376 tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:40px;color:var(--gray5)">â³ ××××§ ת××ת ×××ר ×-DB...</td></tr>'; 7377 if (statsBar) statsBar.textContent = ''; 7378 try { 7379 const d = await fetch('/api/admin/newsletter-stores').then(r => r.json()); 7380 if (!d.ok) { tbody.innerHTML = `<tr><td colspan="8" style="text-align:center;padding:40px;color:#ff6b6b">ש××××: ${d.error||'?'}</td></tr>`; return; } 7381 _nlStores = d.stores || []; 7382 NL.page = 0; 7383 _nlRender(); 7384 } catch(e) { 7385 tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:40px;color:#ff6b6b">ש×××ת ××××ר</td></tr>'; 7386 } 7387} 7388 7389function _nlSetFilter(f, btn) { 7390 _nlActiveFilter = f; 7391 NL.page = 0; 7392 document.querySelectorAll('#nl-filter-all,#nl-filter-il,#nl-filter-intl,#nl-filter-reg,#nl-filter-unreg').forEach(b => b.classList.remove('on')); 7393 btn.classList.add('on'); 7394 _nlRender(); 7395} 7396 7397function _nlSort(key) { 7398 if (_nlSortKey === key) { _nlSortDir *= -1; } else { _nlSortKey = key; _nlSortDir = 1; } 7399 NL.page = 0; 7400 const sel = document.getElementById('nl-sort-sel'); 7401 if (sel) sel.value = key; 7402 const dirBtn = document.getElementById('nl-sort-dir-btn'); 7403 if (dirBtn) dirBtn.textContent = _nlSortDir === 1 ? 'â' : 'â'; 7404 _nlRender(); 7405} 7406function _nlSetSortKey(key) { 7407 _nlSortKey = key; _nlSortDir = 1; NL.page = 0; 7408 const dirBtn = document.getElementById('nl-sort-dir-btn'); 7409 if (dirBtn) dirBtn.textContent = 'â'; 7410 _nlRender(); 7411} 7412function _nlToggleDir() { 7413 _nlSortDir *= -1; NL.page = 0; 7414 const dirBtn = document.getElementById('nl-sort-dir-btn'); 7415 if (dirBtn) dirBtn.textContent = _nlSortDir === 1 ? 'â' : 'â'; 7416 _nlRender(); 7417} 7418 7419function _nlRender() { 7420 const tbody = document.getElementById('nl-stores-tbody'); 7421 const statsBar = document.getElementById('nl-stats-bar'); 7422 if (!tbody) return; 7423 7424 let rows = _nlStores.slice(); 7425 7426 // filter 7427 const _isNlIL = s => s.domain.endsWith('.co.il') || s.domain.endsWith('.il') || 7428 ['10bis','shufersal','ksp','castro','golf','bug','ivory','isrotel','fattal','issta', 7429 'terminalx','delta','vardinon','shilav','renuar','adika','superpharm','super-pharm','ace'].some(k => s.domain.includes(k)); 7430 if (_nlActiveFilter === 'il') rows = rows.filter(s => _isNlIL(s)); 7431 if (_nlActiveFilter === 'intl') rows = rows.filter(s => !_isNlIL(s)); 7432 if (_nlActiveFilter === 'reg') rows = rows.filter(s => s.registered); 7433 if (_nlActiveFilter === 'unreg') rows = rows.filter(s => !s.registered); 7434 7435 // sort 7436 rows.sort((a, b) => { 7437 let va, vb; 7438 if (_nlSortKey === 'name') { va = a.name.toLowerCase(); vb = b.name.toLowerCase(); } 7439 else if (_nlSortKey === 'status') { va = a.active ? 0 : a.registered ? 1 : 2; vb = b.active ? 0 : b.registered ? 1 : 2; } 7440 else if (_nlSortKey === 'emails') { va = a.email_count; vb = b.email_count; } 7441 else if (_nlSortKey === 'coupons') { va = a.coupon_count; vb = b.coupon_count; } 7442 else if (_nlSortKey === 'date') { va = a.last_email_ts; vb = b.last_email_ts; } 7443 if (va < vb) return -1 * _nlSortDir; 7444 if (va > vb) return 1 * _nlSortDir; 7445 return 0; 7446 }); 7447 7448 // stats bar 7449 const total = _nlStores.length; 7450 const regCount = _nlStores.filter(s => s.registered).length; 7451 const activeCount = _nlStores.filter(s => s.active).length; 7452 const newCount = _nlStores.filter(s => s.newsletter_new).length; 7453 const totalCoupons = _nlStores.reduce((s, x) => s + x.coupon_count, 0); 7454 if (statsBar) statsBar.innerHTML = 7455 `<span style="color:var(--accent)">${activeCount}</span> פע×××ת · ` + 7456 `<span style="color:var(--gray6)">${regCount}</span> רש×××ת · ` + 7457 `<span style="color:var(--gray5)">${total - regCount}</span> ×× ×¨×©×××ת · ` + 7458 `<span style="color:var(--accent2)">${totalCoupons}</span> ×§×פ×× ×× ×¡×"× ×× ×××××ר××` + 7459 (newCount ? ` · <span style="color:#ffb347;font-weight:600">â¡ ${newCount} ××ש×ת</span>` : ''); 7460 const nlBadge = document.getElementById('src-nl-count'); 7461 if (nlBadge) nlBadge.textContent = activeCount || '0'; 7462 7463 if (!rows.length) { 7464 tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:40px;color:var(--gray5)">××× ×ª×צ××ת</td></tr>'; 7465 document.getElementById('nl-pagination').innerHTML = ''; 7466 return; 7467 } 7468 7469 // pagination 7470 const nlPages = Math.max(1, Math.ceil(rows.length / NL.perPage)); 7471 if (NL.page >= nlPages) NL.page = nlPages - 1; 7472 const nlSlice = rows.slice(NL.page * NL.perPage, (NL.page + 1) * NL.perPage); 7473 7474 const pgDiv = document.getElementById('nl-pagination'); 7475 if (pgDiv) { 7476 if (nlPages <= 1) { pgDiv.innerHTML = ''; } else { 7477 const nums = _pageNums(NL.page, nlPages); 7478 const numBtns = nums.map(n => n === 'â¦' 7479 ? `<span style="color:var(--gray5);padding:0 4px;font-size:13px">â¦</span>` 7480 : `<button class="td-btn${n-1===NL.page?' page-active':''}" onclick="nlGoPage(${n-1})" style="min-width:32px">${n}</button>` 7481 ).join(''); 7482 pgDiv.innerHTML = ` 7483 <div style="display:flex;align-items:center;justify-content:center;gap:4px;padding:8px 18px 14px;border-top:1px solid var(--gray3);flex-wrap:wrap"> 7484 <button class="td-btn" onclick="nlGoPage(${NL.page-1})" ${NL.page===0?'disabled':''} style="padding:4px 10px">â¹ ××§×××</button> 7485 ${numBtns} 7486 <button class="td-btn" onclick="nlGoPage(${NL.page+1})" ${NL.page>=nlPages-1?'disabled':''} style="padding:4px 10px">××× âº</button> 7487 </div>`; 7488 } 7489 } 7490 7491 tbody.innerHTML = nlSlice.map((s, idx) => { 7492 const rowNum = NL.page * NL.perPage + idx + 1; 7493 const statusDot = s.active 7494 ? `<span style="display:inline-flex;align-items:center;gap:5px;background:rgba(0,232,122,0.12);border:1px solid rgba(0,232,122,0.3);border-radius:20px;padding:2px 8px;font-size:11px;color:var(--accent)"><span style="width:6px;height:6px;border-radius:50%;background:var(--accent);display:inline-block;animation:pulse 2s infinite"></span>פע××</span>` 7495 : s.registered 7496 ? `<span style="display:inline-flex;align-items:center;gap:5px;background:rgba(255,228,94,0.1);border:1px solid rgba(255,228,94,0.3);border-radius:20px;padding:2px 8px;font-size:11px;color:var(--accent2)">â רש×× / ×× ×¤×¢××</span>` 7497 : `<span style="display:inline-flex;align-items:center;gap:5px;background:rgba(255,80,80,0.08);border:1px solid rgba(255,80,80,0.2);border-radius:20px;padding:2px 8px;font-size:11px;color:#ff6b6b">â ×× ×¨×©××</span>`;
7498 7499 const lastDate = s.last_email_ts 7500 ? new Date(s.last_email_ts * 1000).toLocaleDateString('he-IL', {day:'2-digit',month:'2-digit',year:'2-digit'}) 7501 : 'â'; 7502 7503 const _NL_SITE_OVERRIDE = { 7504 'golf.co.il': 'https://www.golf-il.co.il', 7505 'adika.co.il': 'https://www.adikastyle.com', 7506 'americanexpress.co.il': 'https://www.americanexpress.co.il', 7507 'americanexpress.com': 'https://www.americanexpress.co.il', 7508 'aexp.com': 'https://www.americanexpress.co.il', 7509 }; 7510 const storeUrl = _NL_SITE_OVERRIDE[s.domain] || `https://www.${s.domain}`; 7511 const couponBadge = s.coupon_count > 0 7512 ? `<span style="background:rgba(0,232,122,0.15);color:var(--accent);border-radius:12px;padding:2px 8px;font-size:12px;font-weight:600">${s.coupon_count}</span>` 7513 : `<span style="color:var(--gray4);font-size:12px">0</span>`; 7514 7515 // Affiliate column 7516 const affIds = _STORE_AFF_MAP[s.name] || []; 7517 let affCell = '<span style="color:var(--gray4);font-size:11px">â</span>'; 7518 if (affIds.length) { 7519 const affStatus = _affGetStatus(affIds[0]); 7520 const stl = _AFF_STATUS_LABELS[affStatus] || _AFF_STATUS_LABELS['']; 7521 const prog = _AFF_LIST.find(p => p.id === affIds[0]); 7522 const label = prog ? prog.name.split(' ')[0] : affIds[0]; 7523 affCell = `<button onclick="_affCycleStatus('${affIds[0]}');_nlRender()" title="${prog?prog.name:affIds[0]}" style="background:${stl.bg};color:${stl.color};border:1px solid ${stl.color}33;border-radius:12px;padding:2px 9px;font-size:10px;cursor:pointer;white-space:nowrap">${stl.icon} ${label}</button>`; 7524 } 7525 7526 // Actions column: 7527 // ⢠Active (got emails) â green "â רש××" info badge, no need to re-register 7528 // ⢠Manually registered â green "â רש××" toggle (click = unmark) 7529 // ⢠Not registered at all â yellow "×××¨×©× â" link only (clean, no gray clutter) 7530 const manualBtn = s.manual_registered 7531 ? `<button onclick="_nlToggleReg('${s.domain}',this)" title="××× ×¨×ש×× ××× ×" style="font-size:10px;padding:2px 7px;border-radius:6px;border:1px solid rgba(0,232,122,0.4);background:rgba(0,232,122,0.1);color:var(--accent);cursor:pointer">â רש××</button>` 7532 : s.active 7533 ? `<span style="font-size:10px;padding:2px 7px;border-radius:6px;border:1px solid rgba(0,232,122,0.3);background:rgba(0,232,122,0.08);color:var(--accent)">â רש××</span>` 7534 : ''; // unregistered: no gray toggle â just ×××¨×©× below 7535 const signupLink = s.registered 7536 ? '' 7537 : `<a href="${storeUrl}" target="_blank" style="font-size:11px;color:var(--accent2);text-decoration:none;background:rgba(255,228,94,0.08);border:1px solid rgba(255,228,94,0.2);border-radius:6px;padding:3px 8px" title="×¤×ª× ×תר ××רש×× ×× ×××××ר">×××¨×©× â</a>`; 7538 return `<tr style="border-bottom:1px solid var(--gray2);transition:background .15s" onmouseover="this.style.background='var(--gray2)'" onmouseout="this.style.background=''"> 7539 <td style="padding:10px 8px;text-align:center;font-size:11px;color:var(--gray4)">${rowNum}</td> 7540 <td style="padding:10px 12px;font-size:13px;font-weight:500;color:var(--white)">${s.newsletter_new ? `<span style="display:inline-block;background:rgba(255,165,0,0.18);color:#ffb347;border:1px solid rgba(255,165,0,0.4);border-radius:6px;padding:1px 7px;font-size:10px;font-weight:700;margin-bottom:3px;letter-spacing:.5px">NEW</span><br>` : ''}${s.name}<div style="font-size:11px;color:var(--gray4);direction:ltr">${s.domain}</div></td> 7541 <td style="padding:10px 12px;text-align:center">${statusDot}</td> 7542 <td style="padding:10px 12px;text-align:center;font-size:13px;color:${s.email_count>0?'var(--white)':'var(--gray4)'}">${s.email_count||'â'}</td> 7543 <td style="padding:10px 12px;text-align:center">${couponBadge}</td> 7544 <td style="padding:10px 12px;text-align:center;font-size:12px;color:var(--gray5)">${lastDate}</td> 7545 <td style="padding:10px 12px;text-align:center">${affCell}</td> 7546 <td style="padding:10px 12px;text-align:center"> 7547 <div style="display:flex;gap:5px;justify-content:center;align-items:center;flex-wrap:wrap">${manualBtn}${signupLink}${s.newsletter_new ? `<button onclick="_nlDismissNew('${s.name.replace(/'/g,"\\'")}',this)" title="ס×× ×××××§" style="font-size:10px;padding:2px 7px;border-radius:6px;border:1px solid rgba(255,165,0,0.4);background:rgba(255,165,0,0.12);color:#ffb347;cursor:pointer">â ××קת×</button>` : ''}</div> 7548 </td> 7549 </tr>`; 7550 }).join(''); 7551} 7552 7553// Override filter buttons to use _nlSetFilter (static HTML uses onclick) 7554function _nlFilter(f, btn) { _nlSetFilter(f, btn); } 7555 7556async function _nlDismissNew(name, btn) { 7557 btn.disabled = true; 7558 try { 7559 const res = await fetch('/api/admin/newsletter-stores/dismiss-new', { 7560 method: 'POST', 7561 headers: {'Content-Type':'application/json'}, 7562 body: JSON.stringify({name}) 7563 }).then(r => r.json()); 7564 if (res.ok) { 7565 const s = _nlStores.find(x => x.name === name); 7566 if (s) s.newsletter_new = false;
7567 _nlRender(); 7568 } 7569 } catch(e) { btn.disabled = false; } 7570} 7571 7572async function _nlToggleReg(domain, btn) { 7573 btn.disabled = true; 7574 try { 7575 const res = await fetch('/api/admin/newsletter-stores/toggle-registered', { 7576 method: 'POST', 7577 headers: {'Content-Type': 'application/json'}, 7578 body: JSON.stringify({domain}) 7579 }).then(r => r.json()); 7580 if (res.ok) { 7581 // Update local store data and re-render 7582 const s = _nlStores.find(x => x.domain === domain); 7583 if (s) { 7584 s.manual_registered = res.registered; 7585 s.registered = s.email_count > 0 || res.registered; 7586 s.active = s.registered; 7587 } 7588 _nlRender(); 7589 } 7590 } finally { 7591 btn.disabled = false; 7592 } 7593} 7594 7595async function autoSignupNewsletters(btn) { 7596 btn.disabled = true; 7597 const orig = btn.innerHTML; 7598 btn.innerHTML = 'â³ ×ת××ר ××× ×××ת...'; 7599 7600 try { 7601 const res = await fetch('/api/admin/newsletter-signup/auto', {method:'POST'}).then(r=>r.json()); 7602 7603 // Build detailed results modal 7604 const ok = res.results.filter(r => r.status === 'ok'); 7605 const skipped = res.results.filter(r => r.status === 'skipped'); 7606 const failed = res.results.filter(r => r.status === 'failed'); 7607 7608 const methodIcon = m => m === 'klaviyo' ? 'ð¹' : m === 'mailchimp' ? 'ð' : m === 'form' ? 'ð' : '?'; 7609 7610 const rows = (list, color, icon) => list.map(r => 7611 `<tr style="border-bottom:1px solid rgba(255,255,255,0.05)"> 7612 <td style="padding:6px 10px;color:${color};font-size:13px">${icon}</td> 7613 <td style="padding:6px 10px;font-size:13px;font-weight:500">${r.store}</td> 7614 <td style="padding:6px 10px;font-size:11px;color:var(--gray5);direction:ltr">${r.domain}</td> 7615 <td style="padding:6px 10px;font-size:11px;color:var(--gray4)">${r.method ? methodIcon(r.method)+' '+r.method : r.reason || 'â'}</td> 7616 </tr>` 7617 ).join(''); 7618 7619 const modal = document.createElement('div'); 7620 modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px'; 7621 modal.innerHTML = ` 7622 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:16px;max-width:720px;width:100%;max-height:80vh;overflow:hidden;display:flex;flex-direction:column"> 7623 <div style="padding:20px 24px;border-bottom:1px solid var(--gray3);display:flex;justify-content:space-between;align-items:center"> 7624 <div> 7625 <div style="font-size:17px;font-weight:700">ת×צ××ת ר×ש×× ××××××× ×× ×××××ר××</div> 7626 <div style="font-size:12px;color:var(--gray5);margin-top:4px"> 7627 <span style="color:#00e87a">â ${ok.length} ×צ××××</span> · 7628 <span style="color:var(--gray4)">â ${skipped.length} ××ר רש××××</span> · 7629 <span style="color:#ff6b6b">â ${failed.length} × ×ש××</span> 7630 </div> 7631 </div> 7632 <button onclick="this.closest('div[style*=fixed]').remove()" style="background:var(--gray3);border:none;color:var(--gray5);width:32px;height:32px;border-radius:50%;cursor:pointer;font-size:16px">â</button> 7633 </div> 7634 <div style="overflow-y:auto;flex:1"> 7635 ${ok.length ? ` 7636 <div style="padding:12px 24px 4px;font-size:11px;font-weight:700;color:#00e87a;letter-spacing:1px">â ×צ×××× (${ok.length})</div> 7637 <table style="width:100%;border-collapse:collapse">${rows(ok,'#00e87a','â ')}</table>` : ''} 7638 ${failed.length ? ` 7639 <div style="padding:12px 24px 4px;font-size:11px;font-weight:700;color:#ff6b6b;letter-spacing:1px">â × ×ש×× (${failed.length})</div> 7640 <table style="width:100%;border-collapse:collapse">${rows(failed,'#ff6b6b','â')}</table>` : ''} 7641 ${skipped.length ? ` 7642 <div style="padding:12px 24px 4px;font-size:11px;font-weight:700;color:var(--gray4);letter-spacing:1px">â ××ר רש×××× (${skipped.length})</div> 7643 <table style="width:100%;border-collapse:collapse">${rows(skipped,'var(--gray4)','â')}</table>` : ''} 7644 </div> 7645 <div style="padding:14px 24px;border-top:1px solid var(--gray3);text-align:center"> 7646 <button onclick="this.closest('div[style*=fixed]').remove()" style="background:var(--accent);color:#000;border:none;border-radius:8px;padding:8px 28px;font-weight:700;cursor:pointer;font-family:'DM Sans',sans-serif">ס××ר</button> 7647 </div> 7648 </div>`; 7649 document.body.appendChild(modal); 7650 7651 btn.innerHTML = `â ${ok.length} ×צ×××× Â· ${skipped.length} ××ר רש×××× Â· ${failed.length} × ×ש××`;
7652 _nlLoad(); 7653 } catch(e) { 7654 btn.innerHTML = 'â ש××××: ' + e.message; 7655 } finally { 7656 setTimeout(() => { btn.innerHTML = orig; btn.disabled = false; }, 6000); 7657 } 7658} 7659 7660async function scanNewsletters(btn, days) { 7661 btn.disabled = true; 7662 const orig = btn.textContent; 7663 btn.textContent = 'Ⳡס×רק ××××××...'; 7664 const resultEl = document.getElementById('nl-result'); 7665 const resultText = document.getElementById('nl-result-text'); 7666 if (resultEl) resultEl.style.display = 'none'; 7667 try { 7668 const res = await fetch(`/api/admin/scan-newsletters?days=${days}`, { method: 'POST' }); 7669 const d = await res.json(); 7670 if (!d.ok) { 7671 showToast(`â ש××××: ${d.error}`); 7672 } else { 7673 if (resultEl && resultText) { 7674 resultText.innerHTML = `â × ×¡×¨×§× <strong>${d.emails_scanned}</strong> ×××××× Â· × ×צ×× <strong>${d.coupons_found}</strong> ×§×פ×× ×× Â· × ×©××¨× <strong>${d.coupons_saved}</strong> ××ש××`; 7675 resultEl.style.display = 'block'; 7676 } 7677 if (d.coupons_saved > 0) { loadCoupons(); loadStats(); } 7678 } 7679 } catch(e) { 7680 showToast('â ש×××× ×סר×קת × ×××××ר××'); 7681 } finally { 7682 btn.disabled = false; 7683 btn.textContent = orig; 7684 } 7685} 7686 7687async function setNlInterval(select) { 7688 const seconds = parseInt(select.value); 7689 try { 7690 const d = await fetch('/api/admin/newsletter-watcher/set-interval', { 7691 method: 'POST', 7692 headers: {'Content-Type': 'application/json'}, 7693 body: JSON.stringify({seconds}) 7694 }).then(r => r.json()); 7695 if (d.ok) showToast('â ×ר××× ×סר××§× ×¢××××'); 7696 else showToast('â ש××××: ' + d.error); 7697 } catch(e) { showToast('â ש×××ת רשת'); } 7698} 7699 7700async function _nlSyncInterval() { 7701 try { 7702 const d = await fetch('/api/admin/newsletter-watcher/status').then(r => r.json()); 7703 const sel = document.getElementById('nl-interval-select'); 7704 if (sel && d.poll_interval_seconds) { 7705 const opt = [...sel.options].find(o => parseInt(o.value) === d.poll_interval_seconds); 7706 if (opt) sel.value = opt.value; 7707 } 7708 } catch(e) {} 7709} 7710 7711// ââ Feature 5: Scrape full promocode.co.il âââââââââââââââââââââââââââââââââââ 7712 7713let _promoPoller = null; 7714 7715async function scrapePromocodeAll(btn) { 7716 // If already running, just open the progress modal 7717 const st = await fetch('/api/admin/scrape-promocode/status').then(r=>r.json()).catch(()=>null); 7718 if (st && st.running) { _showPromoProgress(st); _startPromoPoller(); return; } 7719 7720 btn.disabled = true; 7721 btn.textContent = 'â³ ×ת×××...'; 7722 try { 7723 const res = await fetch('/api/admin/scrape-promocode', { method: 'POST' }); 7724 const d = await res.json(); 7725 if (!d.ok && d.msg) { showToast('â ï¸ ' + d.msg); btn.disabled=false; btn.textContent='ð סר××§ ×ת ×× promocode.co.il'; return; } 7726 _showPromoProgress(d.status); 7727 _startPromoPoller(); 7728 } catch(e) { 7729 showToast('â ש×××× ××ת××ת ×סר××§×'); 7730 btn.disabled = false; 7731 btn.textContent = 'ð סר××§ ×ת ×× promocode.co.il'; 7732 } 7733} 7734 7735function _showPromoProgress(st) { 7736 let modal = document.getElementById('promo-progress-modal'); 7737 if (!modal) { 7738 modal = document.createElement('div'); 7739 modal.id = 'promo-progress-modal'; 7740 modal.style.cssText = 'position:fixed;bottom:24px;left:24px;background:var(--gray2);border:1px solid var(--gray3);border-radius:12px;padding:18px 22px;min-width:300px;z-index:9999;box-shadow:0 8px 32px #0008;direction:rtl'; 7741 document.body.appendChild(modal); 7742 } 7743 const pagesDone = st.pages_done ?? 0; 7744 const found = st.found ?? 0; 7745 const saved = st.saved ?? 0; 7746 const pagesKnown = st.pages_known ?? false; 7747 const pagesTotal = pagesKnown ? (st.pages_total ?? pagesDone) : null; 7748 const pct = pagesTotal ? Math.round(pagesDone / pagesTotal * 100) : 0; 7749 const pagesLabel = pagesTotal ? `${pagesDone} / ${pagesTotal}` : `${pagesDone}+`; 7750 const done = st.finished_at && !st.running; 7751 modal.innerHTML = ` 7752 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
7753 <span style="font-weight:600;font-size:14px">${done ? 'â סר××§× ××ש×××' : 'ð ס×רק promocode.co.il...'}</span> 7754 ${done ? `<button onclick="document.getElementById('promo-progress-modal').remove()" style="background:none;border:none;color:var(--gray5);font-size:16px;cursor:pointer">â</button>` : ''} 7755 </div> 7756 <div style="background:var(--gray3);border-radius:6px;height:8px;margin-bottom:10px;overflow:hidden"> 7757 <div style="background:var(--accent);height:100%;width:${pct}%;transition:width .3s;border-radius:6px"></div> 7758 </div> 7759 <div style="font-size:13px;color:var(--gray6);display:flex;gap:16px;flex-wrap:wrap"> 7760 <span>ð ${pagesLabel} ×פ××</span> 7761 <span>ð« ${found} × ×צ××</span> 7762 <span>ð¾ ${saved} × ×©×ר×</span> 7763 </div> 7764 ${st.error ? `<div style="color:#ff6b6b;font-size:12px;margin-top:8px">ש××××: ${st.error}</div>` : ''} 7765 `; 7766} 7767 7768function _startPromoPoller() { 7769 if (_promoPoller) return; 7770 const btn = document.querySelector('[onclick*="scrapePromocodeAll"]'); 7771 _promoPoller = setInterval(async () => { 7772 try { 7773 const st = await fetch('/api/admin/scrape-promocode/status').then(r=>r.json()); 7774 _showPromoProgress(st); 7775 if (!st.running) { 7776 clearInterval(_promoPoller); _promoPoller = null; 7777 if (btn) { btn.disabled=false; btn.textContent='ð סר××§ ×ת ×× promocode.co.il'; } 7778 loadCoupons(); loadStats(); 7779 } 7780 } catch(e) { clearInterval(_promoPoller); _promoPoller = null; } 7781 }, 3000); 7782} 7783 7784// On page load, resume progress display if a scrape is already running 7785(async () => { 7786 try { 7787 const st = await fetch('/api/admin/scrape-promocode/status').then(r=>r.json()); 7788 if (st.running) { _showPromoProgress(st); _startPromoPoller(); } 7789 } catch(e) {} 7790})(); 7791 7792async function runBackfill(btn) { 7793 btn.disabled = true; 7794 const orig = btn.textContent; 7795 btn.textContent = 'Ⳡס×רק...'; 7796 try { 7797 const res = await fetch('/api/admin/backfill-pending', { method: 'POST' }); 7798 const d = await res.json(); 7799 showToast(`â ×××× ${d.moved_to_pending} ×§×פ×× ×× ×××§×ר×ת ×× ×××ר×× â ×××¢××¨× ×××××§×`); 7800 loadPendingView(); 7801 } catch(e) { 7802 showToast('â ש×××× ×××××× ××§×ר×ת'); 7803 } finally { 7804 btn.disabled = false; 7805 btn.textContent = orig; 7806 } 7807} 7808 7809async function fixRecords(btn) { 7810 btn.disabled = true; 7811 btn.textContent = 'ð§ ×תק×...'; 7812 try { 7813 const res = await fetch('/api/admin/fix-records', { method: 'POST' }); 7814 const d = await res.json(); 7815 const t = document.getElementById('toast'); 7816 t.textContent = `â ת××§× ×: ${d.fixed_codes} ×§××××, ${d.fixed_notes} ×ער×ת ת×ר×××, ${d.deleted_bad} רש×××ת × ×××§×`; 7817 t.classList.add('show'); 7818 setTimeout(() => t.classList.remove('show'), 4000); 7819 loadCoupons(); loadStats(); 7820 } catch(e) { 7821 alert('ש×××× ×ת××§×× ×¨×©×××ת'); 7822 } finally { 7823 btn.disabled = false; 7824 btn.textContent = 'ð§ ×ª×§× ×¨×©×××ת'; 7825 } 7826} 7827
7828async function runDedup(btn) { 7829 btn.disabled = true; 7830 btn.textContent = 'ð§¹ ×× ×§×...'; 7831 try { 7832 const res = await fetch('/api/admin/dedup', { method: 'POST' }); 7833 const d = await res.json(); 7834 const t = document.getElementById('toast'); 7835 t.textContent = `â ×פ×××××ת: ${d.duplicates_deleted} × ×××§× ×-${d.duplicate_groups_merged} ×§××צ×ת · ${d.store_names_normalised} ש××ת ×× ×××ת ת××§× ×`; 7836 t.classList.add('show'); 7837 setTimeout(() => t.classList.remove('show'), 5000); 7838 loadCoupons(); loadStats(); 7839 } catch(e) { 7840 alert('ש×××× ×× ××§×× ×פ×××××ת'); 7841 } finally { 7842 btn.disabled = false; 7843 btn.textContent = 'ð§¹ × ×§× ×פ×××××ת'; 7844 } 7845} 7846 7847async function runFixPublishedDates(btn) { 7848 btn.disabled = true; 7849 const orig = btn.textContent; 7850 btn.textContent = 'ð ס×רק ת×ר××××...'; 7851 try { 7852 const res = await fetch('/api/admin/fix-published-dates', { method: 'POST' }); 7853 const d = await res.json(); 7854 showToast( 7855 `â ×¢×××× × ${d.coupons_updated} ×§×פ×× ×× ×¢× ×ª×ר×× Â· ` + 7856 `${d.pages_with_date_found}/${d.unique_source_pages} ×פ×× ×¢× ×ª×ר×× Â· ` + 7857 `${d.coupons_no_date_found} ××× ×ª×ר××` 7858 ); 7859 } catch(e) { 7860 showToast('â ש×××× ×ת××§×× ×ª×ר××× ×¤×¨×¡××'); 7861 } finally { 7862 btn.disabled = false; 7863 btn.textContent = orig; 7864 } 7865} 7866 7867async function runScanInstagram(btn) { 7868 btn.disabled = true; 7869 const orig = btn.textContent; 7870 btn.textContent = 'ð¸ ס×רק ××× ×¡××ר×...'; 7871 try { 7872 const res = await fetch('/api/admin/scan-instagram', { method: 'POST' }); 7873 const d = await res.json(); 7874 if (d.error) { 7875 showToast(`â ï¸ ${d.error}`); 7876 } else { 7877 showToast(`â × ×צ×× ${d.found} ×§×פ×× ×× Â· ${d.posts_fetched} פ×ס××× ×Ö¾${d.profiles_scraped} פר×פ××××`); 7878 if (d.found > 0) loadAdminData(); 7879 } 7880 } catch(e) { 7881 showToast('â ש×××× ×סר×קת ××× ×¡××ר×'); 7882 } finally { 7883 btn.disabled = false; 7884 btn.textContent = orig; 7885 } 7886} 7887 7888 7889// ââ Sources & Web admin views (live data) âââââââââââââââââââââââââââââââââââââ 7890let _srcData = null; 7891 7892function _relTime(iso) { 7893 if (!iso) return 'â'; 7894 const diff = Date.now() - new Date(iso).getTime(); 7895 const min = Math.floor(diff / 60000); 7896 if (min < 2) return '×¢×ש××'; 7897 if (min < 60) return `××¤× × ${min} ×ק׳`; 7898 const hr = Math.floor(min / 60); 7899 if (hr < 24) return `××¤× × ${hr} שע׳`; 7900 const days = Math.floor(hr / 24); 7901 if (days < 30) return `××¤× × ${days} ××××`; 7902 return new Date(iso).toLocaleDateString('he-IL'); 7903} 7904 7905async function _fetchSourceData(force = false) { 7906 if (_srcData && !force) return _srcData; 7907 const r = await fetch('/api/admin/sources'); 7908 _srcData = await r.json(); 7909 const tgEl = document.getElementById('src-tg-count'); 7910 const webEl = document.getElementById('src-web-count'); 7911 if (tgEl) tgEl.textContent = _srcData.telegram.length; 7912 if (webEl) webEl.textContent = _srcData.web.length; 7913 return _srcData; 7914} 7915 7916function _renderTelegramTable(data) { 7917 if (!data) return; 7918 const tbody = document.getElementById('tg-sources-tbody'); 7919 if (!tbody) return; 7920 if (!data.telegram.length) { 7921 tbody.innerHTML = `<tr><td colspan="7" style="color:var(--gray5);text-align:center;padding:24px">××× ×¢×¨××¦× ××××¨× â ×××¥ "+ ××סף ער××¥" ××× ××ת×××</td></tr>`; 7922 return; 7923 } 7924 const sortVal = document.getElementById('tg-sort')?.value || 'count'; 7925 const sorted = [...data.telegram].sort((a,b) => { 7926 if (sortVal === 'count') return b.count - a.count; 7927 if (sortVal === 'recent') return (b.last_scan||'').localeCompare(a.last_scan||''); 7928 if (sortVal === 'name') return a.name.localeCompare(b.name, 'he'); 7929 return 0; 7930 }); 7931 tbody.innerHTML = sorted.map((ch, idx) => { 7932 const hasUrl = ch.url && ch.url.startsWith('http'); 7933 const safeUrl = (ch.url || '').replace(/'/g, "\\'"); 7934 const safeName = ch.name.replace(/'/g, "\\'");
7935 const tgIcon = `<svg width="11" height="11" viewBox="0 0 24 24" fill="#29b6f6" style="vertical-align:-1px;margin-left:4px"><path d="M11.944 0A12 12 0 1 0 24 12 12 12 0 0 0 11.944 0zm5.992 8.287-2.021 9.525c-.148.658-.55.818-1.113.508l-3-2.21-1.448 1.393c-.16.16-.295.295-.605.295l.213-3.052 5.56-5.023c.242-.214-.054-.333-.373-.12l-6.871 4.326-2.962-.924c-.644-.204-.657-.644.136-.953l11.574-4.462c.537-.194 1.006.131.91.697z"/></svg>`; 7936 const nameHtml = hasUrl 7937 ? `<a href="${ch.url}" target="_blank" style="color:#29b6f6;text-decoration:none">${tgIcon}${ch.name}</a>` 7938 : `<span style="color:#29b6f6">${tgIcon}${ch.name}</span>`; 7939 const editBtn = `<button class="td-btn" onclick="openEditSourceModal('${safeName}','${safeUrl}')">ער××</button>`; 7940 const actions = hasUrl 7941 ? `${editBtn}<button class="td-btn" onclick="scanSource('${safeUrl}',this)">סר××§</button> 7942 <button class="td-btn danger" onclick="removeSource('${safeUrl}',this)">×סר</button>` 7943 : `${editBtn}<button class="td-btn danger" onclick="removeTgByName('${safeName}',this)">×סר</button>`; 7944 const newBadge = (ch.new_7d > 0) 7945 ? `<span style="color:#ffe45e;font-weight:600">+${ch.new_7d}</span>` 7946 : `<span style="color:var(--gray5)">0</span>`; 7947 return `<tr> 7948 <td style="color:var(--gray5);font-size:11px;text-align:center">${idx+1}</td> 7949 <td class="td-store">${nameHtml}</td> 7950 <td style="text-align:center">${ch.count}</td> 7951 <td style="text-align:center">${newBadge}</td> 7952 <td>${_relTime(ch.last_scan)}</td> 7953 <td><span style="color:var(--accent)">פע××</span></td> 7954 <td><div class="td-actions">${actions}</div></td> 7955 </tr>`; 7956 }).join(''); 7957} 7958 7959// ââ Feature 2: IL/International geo filter ââââââââââââââââââââââââââââââââââââ 7960let _webGeoFilter = 'all'; // 'all' | 'il' | 'intl' 7961 7962const _KNOWN_IL_DOMAINS = new Set([ 7963 'couponet.co.il','icoupons.co.il','jemix.co.il','groo.co.il','buyme.co.il', 7964 'influ.codes','deals.co.il','secretcoupon.co.il','israelicoupons.co.il', 7965 'israelcoupon.co.il','promocode.co.il','aliclub.co.il','pepper.co.il', 7966 'dealtas.com','dealhunter.co.il', 7967 // Israeli stores 7968 'castro.com','ksp.co.il','bug.co.il','golf.co.il','terminalx.com', 7969 'renuar.co.il','adika.co.il','fox.co.il','shufersal.co.il','delta.co.il', 7970 'super-pharm.co.il','10bis.co.il','wolt.com','rami-levy.co.il', 7971 'homecenter.co.il','ace.co.il','hamashbir.co.il','buy2.co.il', 7972 'steimatzky.co.il','isrotel.co.il','fattal.co.il','issta.co.il', 7973 'newpharm.co.il','ivory.co.il','payngo.co.il','zer4u.co.il', 7974 'vardinon.co.il','zaga.co.il','optikana.co.il','intima.co.il', 7975 'urban.co.il','kitan.co.il','beitely.co.il','hashmalneto.co.il', 7976 'tami4.co.il','discount.co.il','pandazzz.co.il','shamhi.co.il', 7977]); 7978 7979function _isIsraeliSource(s) { 7980 const url = (s.url || s.name || '').toLowerCase(); 7981 // Check .co.il or .il TLD 7982 if (url.includes('.co.il') || url.match(/\.il(\/|$)/)) return true; 7983 // Check known Israeli domains set 7984 try { 7985 const host = new URL(url.startsWith('http') ? url : 'https://' + url).hostname.replace(/^www\./, ''); 7986 if (_KNOWN_IL_DOMAINS.has(host)) return true; 7987 // Telegram channels are Israeli by default (they're in TELEGRAM_CHANNELS) 7988 if (host === 't.me') return true; 7989 } catch(e) {} 7990 return false; 7991} 7992 7993function _setWebGeo(geo, btn) { 7994 _webGeoFilter = geo; 7995 document.querySelectorAll('#web-geo-all,#web-geo-il,#web-geo-intl').forEach(b => b.classList.remove('on')); 7996 btn.classList.add('on'); 7997 WEB.page = 0; 7998 _renderWebTable(_srcData); 7999} 8000 8001function _renderWebTable(data) { 8002 if (!data) return; 8003 const tbody = document.getElementById('web-sources-tbody'); 8004 if (!tbody) return; 8005 if (!data.web.length) { 8006 tbody.innerHTML = `<tr><td colspan="7" style="color:var(--gray5);text-align:center;padding:24px">××× ×תר×× ×××¢×§× â ×××¥ "+ ××סף ×תר" ××× ××ת×××</td></tr>`; 8007 document.getElementById('web-pagination').innerHTML = ''; 8008 return; 8009 } 8010 const sortVal = document.getElementById('web-sort')?.value || 'count'; 8011 // Apply IL/International geo filter 8012 let webFiltered = data.web; 8013 if (_webGeoFilter === 'il') webFiltered = data.web.filter(s => _isIsraeliSource(s)); 8014 if (_webGeoFilter === 'intl') webFiltered = data.web.filter(s => !_isIsraeliSource(s)); 8015 const sorted = [...webFiltered].sort((a,b) => { 8016 if (sortVal === 'new') { 8017 // unreviewed first, then newest added_at first 8018 const revDiff = (a.reviewed ? 1 : 0) - (b.reviewed ? 1 : 0); 8019 if (revDiff !== 0) return revDiff; 8020 return (b.added_at||'').localeCompare(a.added_at||''); 8021 }
8022 if (sortVal === 'count') return b.count - a.count; 8023 if (sortVal === 'recent') return (b.last_scan||'').localeCompare(a.last_scan||''); 8024 if (sortVal === 'name') return a.name.localeCompare(b.name); 8025 return 0; 8026 }); 8027 const pages = Math.max(1, Math.ceil(sorted.length / WEB.perPage)); 8028 WEB.page = Math.min(WEB.page, pages - 1); 8029 const slice = sorted.slice(WEB.page * WEB.perPage, (WEB.page + 1) * WEB.perPage); 8030 tbody.innerHTML = slice.map((s, idx) => { 8031 const safeUrl = s.url.replace(/'/g, "\\'"); 8032 const rowNum = WEB.page * WEB.perPage + idx + 1; 8033 const addedDate = s.added_at ? new Date(s.added_at).toLocaleDateString('he-IL',{day:'2-digit',month:'2-digit',year:'2-digit'}) : 'â'; 8034 const reviewedCell = s.reviewed 8035 ? `<button class="td-btn" style="color:var(--accent);min-width:32px;padding:3px 8px" title="× ×××§ â ×××¥ ××××××" onclick="toggleSourceReviewed('${safeUrl}',${s.reviewed},this)">â</button>` 8036 : `<button class="td-btn" style="color:var(--gray5);min-width:32px;padding:3px 8px;border:1px dashed var(--gray4)" title="ס×× ×× ×××§" onclick="toggleSourceReviewed('${safeUrl}',${s.reviewed},this)">â</button>`; 8037 return `<tr${s.reviewed ? '' : ' style="background:rgba(255,200,0,0.04)"'}> 8038 <td style="color:var(--gray5);font-size:11px;text-align:center">${rowNum}</td> 8039 <td class="td-store"><a href="${s.url}" target="_blank" style="color:var(--accent);text-decoration:none">${s.name}</a>${s.reviewed ? '' : ' <span style="font-size:10px;background:rgba(255,200,0,0.15);color:#ffcc00;border-radius:4px;padding:1px 5px">××ש</span>'}</td> 8040 <td>${s.count}</td> 8041 <td>${_relTime(s.last_scan)}</td> 8042 <td style="font-size:12px;color:var(--gray5)">${addedDate}</td> 8043 <td style="text-align:center">${reviewedCell}</td> 8044 <td><div class="td-actions"> 8045 <button class="td-btn" onclick="scanSource('${safeUrl}',this)">סר××§</button> 8046 <button class="td-btn danger" onclick="removeSource('${safeUrl}',this)">×סר</button> 8047 </div></td> 8048 </tr>`; 8049 }).join(''); 8050 // Pagination bar 8051 const pgDiv = document.getElementById('web-pagination'); 8052 if (!pgDiv) return; 8053 if (pages <= 1) { pgDiv.innerHTML = ''; return; } 8054 const nums = _pageNums(WEB.page, pages); 8055 const numBtns = nums.map(n => n === 'â¦' 8056 ? `<span style="color:var(--gray5);padding:0 4px;font-size:13px">â¦</span>` 8057 : `<button class="td-btn${n-1===WEB.page?' page-active':''}" onclick="webGoPage(${n-1})" style="min-width:32px">${n}</button>` 8058 ).join(''); 8059 pgDiv.innerHTML = ` 8060 <div style="font-size:11px;color:var(--gray5);text-align:center;padding:8px 0 2px">${sorted.length} ×תר××</div> 8061 <div style="display:flex;align-items:center;justify-content:center;gap:4px;padding:8px 18px 14px;border-top:1px solid var(--gray3);flex-wrap:wrap"> 8062 <button class="td-btn" onclick="webGoPage(${WEB.page-1})" ${WEB.page===0?'disabled':''} style="padding:4px 10px">â¹ ××§×××</button> 8063 ${numBtns} 8064 <button class="td-btn" onclick="webGoPage(${WEB.page+1})" ${WEB.page>=pages-1?'disabled':''} style="padding:4px 10px">××× âº</button> 8065 </div>`; 8066} 8067function webGoPage(n) { 8068 WEB.page = Math.max(0, n); 8069 _renderWebTable(_srcData); 8070} 8071function nlGoPage(n) { 8072 NL.page = Math.max(0, n); 8073 _nlRender(); 8074} 8075 8076// ââ Instagram view ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 8077let _igData = null; 8078 8079function _renderInstagramTable() { 8080 const tbody = document.getElementById('ig-sources-tbody'); 8081 if (!tbody || !_igData) return; 8082 const profiles = _igData.profiles || []; 8083 const titleEl = document.getElementById('ig-table-title'); 8084 if (titleEl) titleEl.textContent = `${profiles.length} פר×פ×××× Â· ${profiles.reduce((s,p)=>s+p.count,0)} ×§×פ×× ××`; 8085 const countEl = document.getElementById('src-ig-count'); 8086 if (countEl) countEl.textContent = profiles.length; 8087 8088 const sortVal = document.getElementById('ig-sort')?.value || 'count'; 8089 const sorted = [...profiles].sort((a,b) => {
8090 if (sortVal === 'count') return b.count - a.count; 8091 if (sortVal === 'name') return (a.name||'').localeCompare(b.name||'', 'he'); 8092 if (sortVal === 'recent') return (b.last_scan||'').localeCompare(a.last_scan||''); 8093 return 0; 8094 }); 8095 8096 tbody.innerHTML = sorted.map((p, idx) => { 8097 const igIcon = `<svg width="12" height="12" viewBox="0 0 24 24" fill="url(#ig-grad)" style="vertical-align:-2px;margin-left:5px"><defs><linearGradient id="ig-grad" x1="0%" y1="100%" x2="100%" y2="0%"><stop offset="0%" style="stop-color:#f09433"/><stop offset="25%" style="stop-color:#e6683c"/><stop offset="50%" style="stop-color:#dc2743"/><stop offset="75%" style="stop-color:#cc2366"/><stop offset="100%" style="stop-color:#bc1888"/></linearGradient></defs><path 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"/></svg>`; 8098 const safeHandle = (p.handle||'').replace(/'/g,"\\'"); 8099 const nameHtml = `<a href="${p.url}" target="_blank" style="color:#e1306c;text-decoration:none;font-weight:500">${igIcon}${p.name}</a> 8100 <span style="color:var(--gray5);font-size:11px;margin-right:6px">@${p.handle}</span>`; 8101 const status = p.count > 0 8102 ? `<span style="color:var(--accent)">פע××</span>` 8103 : `<span style="color:var(--gray5)">××ת××</span>`; 8104 return `<tr> 8105 <td style="color:var(--gray5);font-size:11px;text-align:center">${idx+1}</td> 8106 <td class="td-store">${nameHtml}</td> 8107 <td>${p.count}</td> 8108 <td>${_relTime(p.last_scan)}</td> 8109 <td>${status}</td> 8110 <td><div class="td-actions"> 8111 <button class="td-btn" onclick="igScanOne('${safeHandle}',this)">סר××§</button> 8112 <a href="${p.url}" target="_blank" class="td-btn" style="text-decoration:none">פר×פ×× â</a> 8113 </div></td> 8114 </tr>`; 8115 }).join(''); 8116} 8117 8118async function initInstagramView() { 8119 const tbody = document.getElementById('ig-sources-tbody'); 8120 if (tbody) tbody.innerHTML = `<tr><td colspan="6" style="color:var(--gray5);text-align:center;padding:24px">×××¢× ×¤×¨×פ××××...</td></tr>`; 8121 try { 8122 const res = await fetch('/api/admin/instagram/profiles', { cache: 'no-store' }); 8123 _igData = await res.json(); 8124 _renderInstagramTable(); 8125 } catch(e) { 8126 if (tbody) tbody.innerHTML = `<tr><td colspan="6" style="color:#ff6b6b;text-align:center;padding:24px">ש×××× ×××¢×× ×ª פר×פ××××</td></tr>`; 8127 } 8128} 8129 8130async function igTestLogin(btn) { 8131 btn.disabled = true; 8132 const orig = btn.textContent; 8133 btn.textContent = 'ð ××××§...'; 8134 try { 8135 const res = await fetch('/api/admin/instagram/test'); 8136 const d = await res.json(); 8137 if (d.ok) { 8138 showToast(`â ××××ר @${d.username} · ${d.test || ''} [${d._v||''}]`); 8139 } else { 8140 showToast(`â [${d._v||'old'}] ${d.error || '××××ר × ×ש×'}`); 8141 if (d.trace) console.error('Instagram test trace:', d.trace); 8142 } 8143 } catch(e) { 8144 showToast('â ש××××: ' + e.message); 8145 } finally { 8146 btn.disabled = false; 8147 btn.textContent = orig; 8148 } 8149} 8150 8151function _goInstagram() { 8152 const btn = document.querySelector('.sidebar-item[onclick*="instagram"]'); 8153 if (btn) setAdminView(btn, 'instagram'); 8154} 8155 8156async function igScanAll(btn) { 8157 btn.disabled = true; 8158 const orig = btn.textContent; 8159 btn.textContent = 'Ⳡס×רק...'; 8160 try { 8161 const res = await fetch('/api/admin/scan-instagram', { method: 'POST' }); 8162 const d = await res.json(); 8163 if (d.error) { 8164 showToast(`â ï¸ ${d.error}`); 8165 } else { 8166 showToast(`â × ×צ×× ${d.found} ×§×פ×× ×× ×Ö¾${d.profiles_scraped} פר×פ×××× (${d.posts_fetched} פ×ס×××)`); 8167 await initInstagramView(); 8168 if (d.found > 0) loadAdminData(); 8169 } 8170 } catch(e) { 8171 showToast('â ש×××× ×סר××§×: ' + e.message); 8172 } finally { 8173 btn.disabled = false;
8174 btn.textContent = orig; 8175 } 8176} 8177 8178async function igScanOne(handle, btn) { 8179 btn.disabled = true; 8180 const orig = btn.textContent; 8181 btn.textContent = 'â³...'; 8182 try { 8183 const res = await fetch(`/api/admin/instagram/scan/${encodeURIComponent(handle)}`, { method: 'POST' }); 8184 const d = await res.json(); 8185 if (d.error) { 8186 showToast(`â @${handle}: ${d.error}`); 8187 } else { 8188 showToast(`â @${handle}: ${d.posts} פ×ס××× Â· ${d.found} ×§×פ×× ×× ××ש××`); 8189 await initInstagramView(); 8190 if (d.found > 0) loadAdminData(); 8191 } 8192 } catch(e) { 8193 showToast('â ש××××: ' + e.message); 8194 } finally { 8195 btn.disabled = false; 8196 btn.textContent = orig; 8197 } 8198} 8199 8200async function initSourcesView() { 8201 const tbody = document.getElementById('tg-sources-tbody'); 8202 if (tbody) tbody.innerHTML = `<tr><td colspan="5" style="color:var(--gray5);text-align:center;padding:20px">×××¢×...</td></tr>`; 8203 try { 8204 const data = await _fetchSourceData(); 8205 _renderTelegramTable(data); 8206 } catch(e) { 8207 if (tbody) tbody.innerHTML = `<tr><td colspan="5" style="color:#fc8181;text-align:center;padding:20px">ש×××× ×××¢×× ×ª ××§×ר×ת</td></tr>`; 8208 } 8209} 8210 8211async function _autoResolveTgNames(data) {
8212 const nameOnly = (data.telegram || []).filter(ch => !ch.url || !ch.url.startsWith('http')); 8213 if (!nameOnly.length) return; 8214 for (const ch of nameOnly) { 8215 try { 8216 const r = await fetch('/api/admin/sources/resolve-tg', { 8217 method: 'POST', 8218 headers: { 'Content-Type': 'application/json' }, 8219 body: JSON.stringify({ name: ch.name }) 8220 }); 8221 if (!r.ok) continue; 8222 const { url } = await r.json(); 8223 if (url) { 8224 // Update DB records and refresh table silently 8225 await fetch('/api/admin/sources/relink', { 8226 method: 'POST', 8227 headers: { 'Content-Type': 'application/json' }, 8228 body: JSON.stringify({ old_pattern: `Telegram: ${ch.name}`, new_source: url }) 8229 }); 8230 _srcData = null; 8231 const fresh = await _fetchSourceData(true); 8232 _renderTelegramTable(fresh); 8233 } 8234 } catch(e) {} 8235 } 8236} 8237 8238async function initWebView() { 8239 const tbody = document.getElementById('web-sources-tbody'); 8240 if (tbody) tbody.innerHTML = `<tr><td colspan="5" style="color:var(--gray5);text-align:center;padding:20px">×××¢×...</td></tr>`; 8241 try { 8242 const data = await _fetchSourceData(); 8243 _renderWebTable(data); 8244 } catch(e) { 8245 if (tbody) tbody.innerHTML = `<tr><td colspan="5" style="color:#fc8181;text-align:center;padding:20px">ש×××× ×××¢×× ×ª ××§×ר×ת</td></tr>`; 8246 } 8247} 8248 8249async function toggleSourceReviewed(url, currentReviewed, btn) { 8250 const newVal = !currentReviewed; 8251 btn.disabled = true; 8252 try { 8253 await fetch('/api/admin/sources/reviewed', { 8254 method: 'PATCH', 8255 headers: { 'Content-Type': 'application/json' }, 8256 body: JSON.stringify({ url, reviewed: newVal }) 8257 }); 8258 const entry = (_srcData?.web || []).find(s => s.url === url); 8259 if (entry) entry.reviewed = newVal; 8260 _renderWebTable(_srcData); 8261 } catch(e) { 8262 btn.disabled = false; 8263 } 8264} 8265 8266async function scanSource(url, btn) { 8267 const orig = btn.textContent; 8268 btn.disabled = true; 8269 btn.textContent = 'â³'; 8270 try { 8271 await fetch('/api/admin/sources/scan', { 8272 method: 'POST', 8273 headers: { 'Content-Type': 'application/json' }, 8274 body: JSON.stringify({ url }) 8275 }); 8276 btn.textContent = 'â '; 8277 _srcData = null; 8278 setTimeout(async () => { 8279 btn.textContent = orig; btn.disabled = false; 8280 const d = await _fetchSourceData(true); 8281 _renderTelegramTable(d); _renderWebTable(d); 8282 }, 1500); 8283 } catch(e) { 8284 btn.textContent = 'â'; 8285 setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 1500); 8286 } 8287} 8288 8289async function removeSource(url, btn) { 8290 if (!confirm(`×××××§ ×ת ×× ××§×פ×× ×× ×××§×ר ××?\n${url}`)) return; 8291 btn.disabled = true; 8292 const pattern = url.replace(/^https?:\/\//, ''); 8293 try { 8294 const r = await fetch('/api/admin/sources/remove', { 8295 method: 'DELETE', 8296 headers: { 'Content-Type': 'application/json' }, 8297 body: JSON.stringify({ pattern }) 8298 }); 8299 const result = await r.json(); 8300 _srcData = null; 8301 const d = await _fetchSourceData(true); 8302 _renderTelegramTable(d); _renderWebTable(d); 8303 const t = document.getElementById('toast'); 8304 if (t) { t.textContent = `ðï¸ × ×××§× ${result.deleted} ×§×פ×× ××`; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),3000); } 8305 } catch(e) { 8306 alert('ש×××× ×××××§×'); btn.disabled = false; 8307 } 8308} 8309 8310async function removeTgByName(name, btn) { 8311 if (!confirm(`×××××§ ×ת ×× ××§×פ×× ×× ××ער××¥ "${name}"?`)) return; 8312 btn.disabled = true; 8313 try { 8314 const r = await fetch('/api/admin/sources/remove', { 8315 method: 'DELETE', 8316 headers: { 'Content-Type': 'application/json' }, 8317 body: JSON.stringify({ pattern: `Telegram: ${name}` }) 8318 }); 8319 const result = await r.json(); 8320 _srcData = null; 8321 const d = await _fetchSourceData(true); 8322 _renderTelegramTable(d); _renderWebTable(d); 8323 const t = document.getElementById('toast'); 8324 if (t) { t.textContent = `ðï¸ × ×××§× ${result.deleted} ×§×פ×× ××`; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),3000); } 8325 } catch(e) { 8326 alert('ש×××× ×××××§×'); btn.disabled = false;
8327 } 8328} 8329 8330function openAddCouponModal() { 8331 ['ac-store','ac-code','ac-discount','ac-min','ac-notes'].forEach(id => { const el=document.getElementById(id); if(el) el.value=''; }); 8332 const expiry = document.getElementById('ac-expiry'); if(expiry) expiry.value=''; 8333 const err = document.getElementById('ac-error'); if(err) err.style.display='none'; 8334 document.getElementById('add-coupon-overlay').style.display = 'flex'; 8335} 8336function closeAddCouponModal() { 8337 document.getElementById('add-coupon-overlay').style.display = 'none'; 8338} 8339async function submitAddCoupon(btn) { 8340 const store = document.getElementById('ac-store').value.trim(); 8341 const code = document.getElementById('ac-code').value.trim().toUpperCase() || 'NO_CODE'; 8342 const discount = document.getElementById('ac-discount').value.trim(); 8343 const cat = document.getElementById('ac-cat').value; 8344 const expiry = document.getElementById('ac-expiry').value || null; 8345 const minOrder = document.getElementById('ac-min').value.trim() || null; 8346 const notes = document.getElementById('ac-notes').value.trim() || null; 8347 const errEl = document.getElementById('ac-error'); 8348 8349 if (!store || !discount) { 8350 errEl.textContent = '×× ×ת ××× ×× ×× ×©××ת ××××'; errEl.style.display='block'; return; 8351 } 8352 btn.disabled = true; btn.textContent = 'Ⳡש××ר...'; 8353 try { 8354 const r = await fetch('/api/admin/coupons', { 8355 method: 'POST', 8356 headers: { 'Content-Type': 'application/json' }, 8357 body: JSON.stringify({ store, code, discount, categories: cat ? [cat] : [], expiry, min_order: minOrder, notes, source: '××× ×', status: 'unknown', votes: 0 }) 8358 }); 8359 if (!r.ok) throw new Error(await r.text()); 8360 closeAddCouponModal(); 8361 const t = document.getElementById('toast'); 8362 if (t) { t.textContent = `â ×§×פ×× × ×סף ××צ×××`; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),3000); } 8363 loadAdminTable(); 8364 } catch(e) { 8365 errEl.textContent = 'ש×××× ×ש×××¨× â × ×¡× ×©××'; errEl.style.display='block'; 8366 btn.disabled=false; btn.textContent='ש××ר ×§×פ××'; 8367 } 8368} 8369 8370async function linkTgChannel(name, btn) { 8371 const url = prompt(`××× ×¡ ×§×ש×ר t.me ×ער××¥ "${name}":\n××××××: https://t.me/shufersal`); 8372 if (!url) return; 8373 if (!url.startsWith('https://t.me/') && !url.startsWith('http://t.me/')) { 8374 alert('××§×ש×ר ×××× ××ת××× ×-https://t.me/'); return; 8375 } 8376 btn.disabled = true; 8377 btn.textContent = 'â³'; 8378 try { 8379 // Re-label all existing coupons from "Telegram: name" â the t.me URL 8380 const r = await fetch('/api/admin/sources/relink', { 8381 method: 'POST', 8382 headers: { 'Content-Type': 'application/json' }, 8383 body: JSON.stringify({ old_pattern: `Telegram: ${name}`, new_source: url }) 8384 }); 8385 if (!r.ok) throw new Error(); 8386 // Now scan the channel for new coupons 8387 await fetch('/api/admin/sources/scan', { 8388 method: 'POST', 8389 headers: { 'Content-Type': 'application/json' }, 8390 body: JSON.stringify({ url }) 8391 }); 8392 _srcData = null; 8393 const d = await _fetchSourceData(true); 8394 _renderTelegramTable(d); _renderWebTable(d); 8395 const t = document.getElementById('toast'); 8396 if (t) { t.textContent = `â ×ער××¥ ×§×שר ×סר××§ ××צ×××`; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),3000); } 8397 } catch(e) { 8398 alert('ש×××× ××§×ש×ר ×ער××¥'); btn.disabled = false; btn.textContent = 'ð קשר ער××¥'; 8399 } 8400} 8401 8402let _addSourceType = 'telegram'; 8403function addSourcePrompt(type) { 8404 _addSourceType = type; 8405 const inp = document.getElementById('as-url-input'); 8406 if (inp) inp.value = ''; 8407 const err = document.getElementById('as-error'); 8408 if (err) err.style.display = 'none'; 8409 const title = document.getElementById('as-modal-title'); 8410 if (title) title.textContent = type === 'telegram' ? '+ ××סף ער××¥ ×××ר×' : '+ ××סף ×תר ×סר××§×'; 8411 const hint = document.getElementById('as-url-hint'); 8412 if (hint) hint.textContent = type === 'telegram' ? '××××××: t.me/channelname' : '××××××: https://example.co.il'; 8413 if (inp) inp.placeholder = type === 'telegram' ? 't.me/channelname' : 'https://example.co.il'; 8414 document.getElementById('add-source-overlay').style.display = 'flex'; 8415 setTimeout(() => inp && inp.focus(), 50); 8416} 8417function closeAddSourceModal() { 8418 document.getElementById('add-source-overlay').style.display = 'none'; 8419} 8420 8421// ââ Edit Source Modal ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 8422let _editSourceOldName = ''; 8423let _editSourceOldUrl = ''; 8424 8425 8426function _esUrlChanged(val) { 8427 val = val.trim(); 8428 if (!val) return; 8429 // Extract channel handle from t.me URL 8430 const m = val.match(/t\.me\/(?:s\/)?([^/?#|\s]+)/); 8431 if (!m) return; 8432 const handle = m[1]; 8433 const nameInp = document.getElementById('es-name-input'); 8434 // Only auto-fill if the name field is empty or still holds the previous auto-value 8435 const current = (nameInp.value || '').trim(); 8436 const prevAuto = _editSourceOldUrl 8437 ? (() => { const pm = _editSourceOldUrl.match(/t\.me\/(?:s\/)?([^/?#|\s]+)/); return pm ? '@' + pm[1] : ''; })() 8438 : ''; 8439 if (!current || current === prevAuto || current === _editSourceOldName) { 8440 nameInp.value = '@' + handle; 8441 } 8442} 8443 8444function openEditSourceModal(name, url) { 8445 // url may contain "|Custom Name" â strip it for the URL field 8446 const bareUrl = url ? url.split('|')[0] : ''; 8447 _editSourceOldName = name; 8448 _editSourceOldUrl = bareUrl; 8449 document.getElementById('es-url-input').value = bareUrl; 8450 document.getElementById('es-name-input').value = name; 8451 document.getElementById('es-error').style.display = 'none'; 8452 document.getElementById('edit-source-overlay').style.display = 'flex'; 8453 document.getElementById('es-name-input').focus(); 8454} 8455function closeEditSourceModal() { 8456 document.getElementById('edit-source-overlay').style.display = 'none'; 8457} 8458async function submitEditSource() { 8459 const newName = document.getElementById('es-name-input').value.trim(); 8460 let newUrl = document.getElementById('es-url-input').value.trim(); 8461 const errEl = document.getElementById('es-error'); 8462 errEl.style.display = 'none'; 8463 8464 if (!newName) { errEl.textContent = '×©× ×ער××¥ ×× ×××× ××××ת ר××§'; errEl.style.display='block'; return; } 8465 8466 // Normalise URL if provided 8467 if (newUrl) { 8468 if (!newUrl.startsWith('http')) newUrl = 'https://' + newUrl; 8469 if (!newUrl.includes('t.me')) { 8470 errEl.textContent = '××§×ש×ר ×××× ××××ת ×©× ××××¨× (t.me/...)'; 8471 errEl.style.display = 'block'; 8472 return; 8473 } 8474 } 8475 8476 const btn = document.getElementById('es-submit-btn'); 8477 btn.disabled = true; btn.textContent = '...ש××ר'; 8478 8479 try { 8480 // Determine old pattern to find existing DB records 8481 // Strip any "|Custom Name" suffix from the stored URL to get the raw URL pattern 8482 const rawOldUrl = _editSourceOldUrl ? _editSourceOldUrl.split('|')[0] : ''; 8483 const oldPattern = rawOldUrl.startsWith('http') 8484 ? rawOldUrl // match by URL (strips old custom name) 8485 : `Telegram: ${_editSourceOldName}`; // name-only format 8486 8487 // Determine new source value 8488 // If URL given: store as "url|Custom Name" when name differs from auto-derived @handle 8489 let newSource; 8490 if (newUrl) { 8491 const autoName = '@' + newUrl.replace(/.*t\.me\/(?:s\/)?/, '').replace(/[/?#].*/, ''); 8492 newSource = (newName && newName !== autoName) ? `${newUrl}|${newName}` : newUrl; 8493 } else { 8494 newSource = `Telegram: ${newName}`; 8495 } 8496 8497 await fetch('/api/admin/sources/relink', { 8498 method: 'POST', 8499 headers: {'Content-Type':'application/json'}, 8500 body: JSON.stringify({ old_pattern: oldPattern, new_source: newSource }) 8501 }); 8502 8503 closeEditSourceModal(); 8504 _srcData = null; 8505 const d = await _fetchSourceData(true); 8506 _renderTelegramTable(d); _renderWebTable(d); 8507 const t = document.getElementById('toast'); 8508 if (t) { t.textContent = 'â ×××§×ר ×¢×××× ××צ×××'; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),2500); } 8509 } catch(e) { 8510 errEl.textContent = 'ש×××× ×ש×××¨× â × ×¡× ×©××'; 8511 errEl.style.display = 'block'; 8512 } finally { 8513 btn.disabled = false; btn.textContent = 'ש××ר'; 8514 } 8515} 8516function _fixSourceUrl(raw, type) { 8517 let url = raw.trim(); 8518 // Auto-fix: strip @ prefix (Telegram handles) 8519 if (url.startsWith('@')) url = 't.me/' + url.slice(1); 8520 // Auto-fix: add https:// if missing 8521 if (!url.startsWith('http')) url = 'https://' + url; 8522 return url; 8523} 8524async function submitAddSource() { 8525 const inp = document.getElementById('as-url-input'); 8526 const errEl = document.getElementById('as-error'); 8527 const btn = document.getElementById('as-submit-btn'); 8528 const url = _fixSourceUrl(inp.value, _addSourceType); 8529 8530 // Validate t.me for telegram type 8531 if (_addSourceType === 'telegram' && !url.includes('t.me')) { 8532 errEl.textContent = '××§×ש×ר ×××× ××××ת ער××¥ ××××¨× (t.me/...)'; errEl.style.display = 'block'; return; 8533 } 8534 if (!url.startsWith('http')) { 8535 errEl.textContent = '×ת××ת ×× ×ª×§×× ×'; errEl.style.display = 'block'; return; 8536 } 8537 8538 // Register source (placeholder record) â no auto-scan 8539 btn.disabled = true; btn.textContent = 'Ⳡר×ש×...'; 8540 try { 8541 await fetch('/api/admin/sources/register', { 8542 method: 'POST', 8543 headers: { 'Content-Type': 'application/json' }, 8544 body: JSON.stringify({ url }) 8545 }); 8546 closeAddSourceModal(); 8547 _srcData = null; 8548 const d = await _fetchSourceData(true); 8549 _renderTelegramTable(d); _renderWebTable(d); 8550 const t = document.getElementById('toast'); 8551 if (t) { t.textContent = `â ×××§×ר × ×¨×©× â ×××¥ סר××§ ×שת××× ××××`; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),3500); } 8552 } catch(e) { 8553 errEl.textContent = 'ש×××× ×ר×ש×× ×××§×ר'; errEl.style.display = 'block'; 8554 } finally { 8555 btn.disabled = false; btn.textContent = 'רש×× ××§×ר'; 8556 } 8557} 8558 8559let _logActiveDid = ''; // '' = all devices 8560let _logActiveUser = ''; // '' = all users 8561 8562async function loadLogs(did, user, page) { 8563 if (did !== undefined) { _logActiveDid = did; LOG.page = 0; } 8564 if (user !== undefined) { _logActiveUser = user; LOG.page = 0; } 8565 if (page !== undefined) LOG.page = page; 8566 const el = document.getElementById('log-lines'); 8567 const summary = document.getElementById('log-summary'); 8568 if (!el) return; 8569 el.textContent = '×××¢× ×××××...'; 8570 8571 try { 8572 // ââ Sync local events to server first (so admin's own events show up) âââââ 8573 await _syncAnalyticsToServer(false).catch(() => {}); 8574 8575 // ââ Fetch devices, admin users list, and log rows in parallel âââââââââââââ 8576 const devicesUrl = '/api/admin/logs/devices'; 8577 const [allRows, devices, adminUsers] = await Promise.all([ 8578 fetch(`/api/admin/logs?limit=500`, { cache: 'no-store' }).then(r => r.json()), 8579 fetch(devicesUrl, { cache: 'no-store' }).then(r => r.json()).catch(() => []), 8580 fetch('/api/admin/users', { cache: 'no-store' }).then(r => r.json()).catch(() => []), 8581 ]); 8582 8583 // ââ Build known-users set: from admin_users table + current logged-in user â
8584 const _getU = (data) => { 8585 if (!data) return ''; 8586 if (typeof data === 'object') return data._u || ''; 8587 return ''; 8588 }; 8589 const knownAdminUsers = new Set((adminUsers || []).map(u => u.username).filter(Boolean)); 8590 const currentUser = _currentUsername(); 8591 if (currentUser) knownAdminUsers.add(currentUser); 8592 // Also add any users found in event data (even if not in admin_users table) 8593 allRows.forEach(r => { const u = _getU(r.data); if (u) knownAdminUsers.add(u); }); 8594 const knownUsers = [...knownAdminUsers].sort(); 8595 8596 // ââ Filter rows by active user / device âââââââââââââââââââââââââââââââââââ 8597 // For user filter: find all device IDs that have ANY event tagged _u===user 8598 // then show ALL events from those DIDs (so sessions etc. appear too) 8599 let rows; 8600 if (_logActiveUser) { 8601 const userDids = new Set( 8602 allRows 8603 .filter(r => r.data && typeof r.data === 'object' && r.data._u === _logActiveUser) 8604 .map(r => r.did).filter(Boolean) 8605 ); 8606 rows = allRows.filter(r => { 8607 const u = r.data && typeof r.data === 'object' ? r.data._u : ''; 8608 if (u === _logActiveUser) return true; // direct match 8609 if (r.did && userDids.has(r.did)) return true; // same device 8610 return false; 8611 }); 8612 } else if (_logActiveDid) { 8613 rows = allRows.filter(r => r.did === _logActiveDid); 8614 } else { 8615 rows = allRows; 8616 } 8617 8618 // ââ Filter bar â by user then by device âââââââââââââââââââââââââââââââââââ 8619 const filterBar = document.getElementById('log-device-filter'); 8620 if (filterBar) { 8621 const _btn = (onclick, label, active) => 8622 `<button onclick="${onclick}" style="padding:4px 14px;border:none;border-radius:20px;font-size:12px;cursor:pointer;font-family:'DM Sans',sans-serif;background:${active?'var(--accent)':'var(--gray3)'};color:${active?'#000':'var(--gray6)'};transition:.15s">${label}</button>`; 8623 8624 let html = _btn(`loadLogs('','')`, '×××', !_logActiveDid && !_logActiveUser); 8625 8626 // User buttons â always shown if we know of any admin users 8627 if (knownUsers.length) { 8628 html += `<span style="color:var(--gray4);font-size:11px;margin:0 4px">|</span>`; 8629 html += knownUsers.map(u => 8630 _btn(`loadLogs('','${u}')`, `ð¤ ${u}`, _logActiveUser === u) 8631 ).join(''); 8632 } 8633 8634 // Device buttons â only show devices NOT already linked to a known admin user 8635 // A device is "linked" if any of its events has _u matching a known admin 8636 const linkedDids = new Set( 8637 allRows 8638 .filter(r => r.data && typeof r.data === 'object' && knownAdminUsers.has(r.data._u)) 8639 .map(r => r.did).filter(Boolean) 8640 ); 8641 const anonDevices = devices.filter(dev => !linkedDids.has(dev.did)); 8642 if (anonDevices.length) { 8643 html += `<span style="color:var(--gray4);font-size:11px;margin:0 4px">|</span>`; 8644 let anonIdx = 0; 8645 html += anonDevices.map(dev => { 8646 anonIdx++; 8647 const short = dev.did.slice(-6).toUpperCase(); 8648 // Find first-seen timestamp and IP for this device 8649 const devRows = allRows.filter(r => r.did === dev.did).sort((a,b) => (a.ts||0)-(b.ts||0)); 8650 const firstRow = devRows[0]; 8651 const firstDate = firstRow && firstRow.ts ? new Date(firstRow.ts).toLocaleDateString('he-IL',{day:'2-digit',month:'2-digit'}) : ''; 8652 const firstIp = firstRow && firstRow.ip ? firstRow.ip : ''; 8653 const tooltip = [firstDate, firstIp].filter(Boolean).join(' · '); 8654 return _btn(`loadLogs('${dev.did}','')`, 8655 `ðï¸ ×× ×× ××× ${anonIdx} <span style="opacity:.6;font-size:10px">${short}${tooltip?' · '+tooltip:''}</span>`, 8656 _logActiveDid === dev.did); 8657 }).join(''); 8658 } 8659 filterBar.innerHTML = html; 8660 } 8661 8662 // ââ Summary chips âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 8663 const counts = {}; 8664 rows.forEach(r => { counts[r.type] = (counts[r.type] || 0) + 1; }); 8665 const _chip = (label, n, color) => 8666 `<div style="background:var(--gray3);border-radius:20px;padding:4px 12px;font-size:12px;font-family:'DM Sans',sans-serif;color:${color||'var(--gray6)'}">${label} <strong style="color:var(--white)">${n}</strong></div>`; 8667 if (summary) summary.innerHTML = [ 8668 _chip('ð ××פ×ש××', counts.search || 0, ''), 8669 _chip('ð ×עתק×ת', counts.copy || 0, ''), 8670 _chip('ð ××××§××', counts.thumb_up || 0, 'var(--accent)'), 8671 _chip('ð ××ס××××§××', counts.thumb_down || 0, '#ff6b6b'), 8672 _chip('ðï¸ ×× ×ס×ת', counts.show_site || 0, ''), 8673 _chip('ð±ï¸ ×××צ×ת', counts.coupon_field_click || 0, ''),
8674 _chip('ð« פ×פ×פ××', counts.coupon_popup || 0, '#a78bfa'), 8675 _chip('ð ×¢×× ×§×פ׳', counts.more_coupons || 0, '#ffe45e'), 8676 _chip('ð¡ ס××´×', rows.length, 'var(--accent)'), 8677 ].join(''); 8678 8679 // ââ Event label map âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 8680 // ââ Event type config âââââââââââââââââââââââââââââââââââââââââââââââââââââ 8681 const _EVT = { 8682 session: { icon:'ð', label:'×× ××¡× ××תר', color:'var(--gray5)' }, 8683 search: { icon:'ð', label:'××פ×ש', color:'#63b3ed' }, 8684 search_time: { icon:'â±ï¸', label:'ת×צ××ת ××פ×ש', color:'#63b3ed' }, 8685 search_dropped: { icon:'â©ï¸', label:'××פ×ש ××××', color:'var(--gray5)' }, 8686 search_failed: { icon:'â', label:'××פ×ש × ×ש×', color:'#ff6b6b' }, 8687 copy: { icon:'ð', label:'×עתקת ×§××', color:'var(--accent)' }, 8688 coupon_field_click: { icon:'ð', label:'××××¦× ×¢× ×§××', color:'var(--gray6)' }, 8689 show_site: { icon:'ðï¸', label:'×× ××¡× ××× ×ת', color:'var(--gray6)' }, 8690 thumb_up: { icon:'ð', label:'××××§', color:'var(--accent)' }, 8691 thumb_down: { icon:'ð', label:'××ס××××§', color:'#ff6b6b' }, 8692 coupon_popup: { icon:'ð«', label:'פת××ת פ×פ×פ', color:'#a78bfa' }, 8693 modal_copy: { icon:'ð', label:'××¢×ª×§× ×פ×פ×פ', color:'var(--accent)' }, 8694 more_coupons: { icon:'ð', label:'×¢×× ×§×פ×× ×× ×©×', color:'#ffe45e' }, 8695 login: { icon:'ð', label:'×× ×סת ×× ××', color:'#e1a0ff' }, 8696 }; 8697 8698 const _detail = (type, data) => { 8699 const d = (data && typeof data === 'object') ? data : {}; 8700 const v = d._v ?? data ?? ''; 8701 switch (type) { 8702 case 'search': return String(v); 8703 case 'search_dropped': return String(v); 8704 case 'search_failed': return d.error || String(v); 8705 case 'copy': return String(v); 8706 case 'search_time': return `"${d.q||''}" · ${d.results||0} ת×צ××ת · ${d.elapsed||'?'}s · ${d.sites||0} ×תר××`; 8707 case 'coupon_popup': return d.store || ''; 8708 case 'modal_copy': return d.store || ''; 8709 case 'more_coupons': return typeof data === 'string' ? data : (d.store || ''); 8710 case 'login': return d._v || ''; 8711 default: return ''; 8712 } 8713 }; 8714 8715 // ââ Pagination ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 8716 const pgDiv = document.getElementById('log-pagination'); 8717 const totPages = Math.max(1, Math.ceil(rows.length / LOG.perPage)); 8718 LOG.page = Math.min(LOG.page, totPages - 1); 8719 const slice = rows.slice(LOG.page * LOG.perPage, (LOG.page + 1) * LOG.perPage); 8720 8721 // Render pagination buttons 8722 if (pgDiv) { 8723 if (totPages <= 1) { 8724 pgDiv.innerHTML = ''; 8725 } else { 8726 const nums = _pageNums(LOG.page, totPages); 8727 pgDiv.innerHTML = 8728 `<button class="td-btn" onclick="loadLogs(undefined,undefined,${LOG.page-1})" ${LOG.page===0?'disabled':''} style="padding:4px 10px">â¹ ××§×××</button>` + 8729 nums.map(n => n==='â¦' 8730 ? `<span style="padding:4px 6px;color:var(--gray5)">â¦</span>` 8731 : `<button class="td-btn${n-1===LOG.page?' page-active':''}" onclick="loadLogs(undefined,undefined,${n-1})" style="min-width:32px">${n}</button>` 8732 ).join('') + 8733 `<button class="td-btn" onclick="loadLogs(undefined,undefined,${LOG.page+1})" ${LOG.page>=totPages-1?'disabled':''} style="padding:4px 10px">××× âº</button>`; 8734 } 8735 } 8736 8737 // ââ Render table ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 8738 if (!rows.length) { 8739 el.innerHTML = `<div style="padding:32px;text-align:center;color:var(--gray5)">××× ×¤×¢×××ת רש××× ×¢××××.</div>`; 8740 if (pgDiv) pgDiv.innerHTML = ''; 8741 return; 8742 } 8743 8744 const th = s => `<th style="padding:8px 14px;text-align:right;font-size:11px;color:var(--gray5);font-weight:500;white-space:nowrap;border-bottom:1px solid var(--gray3);background:var(--gray2)">${s}</th>`; 8745 const tdS = (content, extra='') => `<td style="padding:7px 14px;font-size:12px;border-bottom:1px solid rgba(255,255,255,0.04);white-space:nowrap;${extra}">${content}</td>`;
8746 8747 el.innerHTML = `<table style="width:100%;border-collapse:collapse;direction:rtl"> 8748 <thead><tr>${th('ת×ר××')}${th('שע×')}${th('××')}${th('פע×××')}${th('פר×××')}${th('IP')}</tr></thead> 8749 <tbody> 8750 ${slice.map(r => { 8751 const dt = new Date(r.ts); 8752 const dateStr = isNaN(dt) ? 'â' : dt.toLocaleDateString('he-IL', {day:'2-digit', month:'2-digit', year:'2-digit'}); 8753 const timeStr = isNaN(dt) ? 'â' : dt.toLocaleTimeString('he-IL', {hour:'2-digit', minute:'2-digit'}); 8754 const data = r.data; 8755 const rowUser = (data && typeof data === 'object') ? (data._u || '') : ''; 8756 const evt = _EVT[r.type] || { icon:'ð', label: r.type, color:'var(--gray5)' }; 8757 const detail = _detail(r.type, data); 8758 const ip = (data && typeof data === 'object' && data._v && r.type === 'login') 8759 ? (String(data._v).replace('login from ', '')) 8760 : ''; 8761 const whoBadge = rowUser 8762 ? `<span style="font-size:11px;padding:2px 8px;border-radius:10px;background:rgba(255,228,94,0.15);color:var(--accent2);font-weight:600">ð¤ ${rowUser}</span>` 8763 : `<span style="font-size:11px;color:var(--gray4)">×× ×× ×××</span>`; 8764 return `<tr style="transition:background .1s" onmouseover="this.style.background='rgba(255,255,255,0.03)'" onmouseout="this.style.background=''"> 8765 ${tdS(dateStr, 'color:var(--gray5)')} 8766 ${tdS(timeStr, 'color:var(--gray5);font-family:monospace')} 8767 ${tdS(whoBadge)} 8768 ${tdS(`<span style="color:${evt.color}">${evt.icon} ${evt.label}</span>`)} 8769 ${tdS(`<span style="color:var(--gray6);max-width:320px;overflow:hidden;text-overflow:ellipsis;display:inline-block">${detail}</span>`)} 8770 ${tdS(`<span style="font-family:monospace;font-size:11px;color:var(--gray4)">${ip || 'â'}</span>`)} 8771 </tr>`; 8772 }).join('')} 8773 </tbody></table>`; 8774 8775 } catch(e) { 8776 el.textContent = '×× × ××ª× ×××¢×× ×××××: ' + e.message; 8777 } 8778} 8779function copyCode(code, store, src) { 8780 // Admin table copy â no new tab needed 8781 trackEvent('copy', code); 8782 _sendCouponEvent('copy', store, code, null); 8783 navigator.clipboard.writeText(code).catch(() => {}); 8784 const t = document.getElementById('toast'); 8785 t.textContent = '××עתק: ' + code; 8786 t.classList.add('show'); 8787 setTimeout(() => t.classList.remove('show'), 3000); 8788} 8789 8790// ââ Reveal modal ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 8791 8792// Reference to the store tab opened in the background 8793function ccReveal(el, code, store, src, id, storeHome) { 8794 8795 // On the filtered store page AND same store: show modal inline â don't open a new tab or navigate away 8796 if (_storeFilterMode && (store || '').toLowerCase() === _storeFilterName) { 8797 const card = el ? el.closest('.coupon-card') : null; 8798 const ds = card ? card.dataset : {}; 8799 const goUrl = '/go?' + new URLSearchParams({ store: store || '' }).toString(); 8800 _storeDataReady.then(() => 8801 showRevealModal(code, store, storeHome, goUrl, 8802 ds.discountRaw || '', id, ds.notes || '', ds.source || '', 8803 ds.ageLabel || '', ds.pubLabel || '', ds.votes || '0', ds.newsletter || '') 8804 ); 8805 return; 8806 } 8807 8808 const goParams = new URLSearchParams({ store: store || '' }); 8809 if (storeHome) goParams.set('url', storeHome); 8810 const goUrl = '/go?' + goParams.toString(); 8811 8812 // Read extra data from card data attributes 8813 const card = el ? el.closest('.coupon-card') : null; 8814 const ds = card ? card.dataset : {}; 8815 8816 const revealParams = new URLSearchParams({ 8817 showReveal: '1', 8818 rc: code || '', 8819 rs: store || '', 8820 rh: storeHome || '', 8821 ru: goUrl, 8822 rd: ds.discountRaw || '', 8823 ri: id || '', 8824 rn: ds.notes || '', 8825 rsrc: ds.source || '', 8826 ral: ds.ageLabel || '', 8827 rpl: ds.pubLabel || '', 8828 rv: ds.votes || '0', 8829 rns: ds.newsletter || '', 8830 }); 8831 const revealUrl = window.location.origin + '/?' + revealParams.toString(); 8832 8833 window.name = 'couponai_store_tab'; 8834 window.open(revealUrl, '_blank'); 8835 window.location.href = goUrl; 8836} 8837 8838function ccCopy(code, store, src, id) { ccReveal(null, code, store, src, id, ''); } 8839function openBehind(url) { window.open(url, '_blank', 'noopener,noreferrer'); } 8840 8841// ââ Modal state âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 8842let _rmCode = '', _rmGoUrl = '', _rmId = '', _rmStore = ''; 8843let _storeFilterMode = false; // true when on filtered store page (after "×¢×× ×§×פ×× ×× ×©×") 8844let _storeFilterName = ''; // canonical store name that triggered the filter 8845 8846function showRevealModal(code, store, storeHome, goUrl, discount, id, notes, sourceSrc, ageLabel, pubLabel, votes, newsletter) { 8847 _rmCode = code || ''; 8848 _rmGoUrl = goUrl || ''; 8849 _rmId = id || ''; 8850 _rmStore = store || ''; 8851 8852 // Header 8853 document.getElementById('rm-header-title').textContent = 8854 discount ? `×§×פ×× ${discount} - ${_storeDisplayModal(store)}` : `×§×פ×× ×× ×× - ${_storeDisplayModal(store)}`; 8855 8856 // Logo â use stored logo from Supabase Storage, fall back to Google favicon, then initials 8857 const logoEl = document.getElementById('rm-logo'); 8858 const _storeKey = (store || '').toLowerCase(); 8859 const _storedLogo = STORE_LOGOS[_storeKey] || null; 8860 const _storeDomain = STORE_DOMAINS[_storeKey] || null; 8861 8862 const _setModalLogo = (primarySrc, fallbackSrc) => { 8863 const img = document.createElement('img'); 8864 img.alt = store; 8865 img.onerror = fallbackSrc 8866 ? function() { 8867 img.onerror = () => { logoEl.innerHTML = _storeInitial(store, 64); }; 8868 img.src = fallbackSrc; 8869 } 8870 : function() { logoEl.innerHTML = _storeInitial(store, 64); }; 8871 img.src = primarySrc; 8872 logoEl.innerHTML = ''; 8873 logoEl.appendChild(img); 8874 }; 8875 8876 if (_storedLogo) { 8877 const apiFav = _storeDomain ? `/api/favicon?domain=${encodeURIComponent(_storeDomain)}` : null; 8878 _setModalLogo(_storedLogo, apiFav); 8879 } else if (_storeDomain) { 8880 _setModalLogo(`/api/favicon?domain=${encodeURIComponent(_storeDomain)}`, null); 8881 } else { 8882 logoEl.innerHTML = _storeInitial(store, 64); 8883 } 8884 8885 // Discount 8886 const discountEl = document.getElementById('rm-discount'); 8887 discountEl.textContent = discount || ''; 8888 discountEl.style.display = discount ? '' : 'none'; 8889 8890 // Newsletter badge 8891 const nlEl = document.getElementById('rm-newsletter'); 8892 nlEl.innerHTML = newsletter === '1' ? '<div class="cc-newsletter-badge">âï¸ ××§×××ת ××ש×× ×××× â ר×ש×× ×× ×××××ר</div>' : ''; 8893 nlEl.style.display = newsletter === '1' ? '' : 'none'; 8894 8895 // Notes 8896 const notesEl = document.getElementById('rm-notes'); 8897 notesEl.textContent = notes || ''; 8898 notesEl.style.display = notes ? '' : 'none'; 8899 8900 // Code 8901 document.getElementById('rm-code-val').textContent = code || ''; 8902 8903 // Vote buttons 8904 const voteState = id ? getVoteState(id) : ''; 8905 const upBtn = document.getElementById('rm-vote-up'); 8906 const dnBtn = document.getElementById('rm-vote-down'); 8907 const upvotes = Math.max(0, parseInt(votes) || 0); 8908 document.getElementById('rm-vote-up-count').textContent = upvotes; 8909 document.getElementById('rm-vote-down-count').textContent = 0; 8910 upBtn.disabled = voteState === 'up'; upBtn.classList.toggle('voted', voteState === 'up'); 8911 dnBtn.disabled = voteState === 'down'; dnBtn.classList.toggle('voted', voteState === 'down'); 8912 8913 // Footer store link 8914 document.getElementById('rm-more-store').textContent = _storeDisplayCard(store); 8915 document.getElementById('rm-more-link').href = `/store/${_storeSlug(store || '')}`; 8916 8917 // Views â only count once per device; subsequent opens show cached count 8918 const viewsEl = document.getElementById('rm-views'); 8919 viewsEl.textContent = ''; 8920 if (id) { 8921 const viewedKey = `viewed_${id}`; 8922 const cachedCount = localStorage.getItem(viewedKey); 8923 if (cachedCount !== null) { 8924 // Already counted â just display the cached value 8925 viewsEl.textContent = `ð ${cachedCount} צפ××ת`; 8926 } else { 8927 // First time this device views this coupon â increment and cache 8928 fetch(`/api/coupons/${id}/view`, { method: 'POST' }) 8929 .then(r => r.json()) 8930 .then(d => { 8931 const count = d.views || 0; 8932 viewsEl.textContent = `ð ${count} צפ××ת`; 8933 localStorage.setItem(viewedKey, String(count)); 8934 }) 8935 .catch(() => { viewsEl.textContent = ''; }); 8936 } 8937 } 8938 8939 // Auto-copy 8940 navigator.clipboard.writeText(code || '').catch(() => {}); 8941 8942 trackEvent('coupon_popup', { store: store || '', id: id || '' });
8943 _sendCouponEvent('modal_open', store, code, id); 8944 // Flush immediately â this fires in a new tab that closes quickly, can't wait for 5-min interval 8945 _syncAnalyticsToServer(true); 8946 document.getElementById('reveal-modal-overlay').classList.add('active'); 8947 document.body.style.overflow = 'hidden'; 8948} 8949 8950function closeRevealModal() { 8951 document.getElementById('reveal-modal-overlay').classList.remove('active'); 8952 document.body.style.overflow = ''; 8953 history.replaceState(null, '', window.location.pathname); 8954} 8955 8956function rmGoToStore() { 8957 if (_rmGoUrl) window.open(_rmGoUrl, '_blank'); 8958} 8959 8960function rmShowToast() { 8961 const t = document.getElementById('rm-toast'); 8962 if (!t) return; 8963 t.classList.add('visible'); 8964 setTimeout(() => t.classList.remove('visible'), 2000); 8965} 8966 8967function rmCopyCode() { 8968 navigator.clipboard.writeText(_rmCode).catch(() => {}); 8969 trackEvent('modal_copy', { store: _rmStore, id: _rmId }); 8970 _sendCouponEvent('copy', _rmStore, _rmCode, _rmId); 8971 _syncAnalyticsToServer(true); // flush immediately 8972 rmShowToast(); 8973} 8974 8975async function rmVote(dir) { 8976 if (!_rmId) return; 8977 // Snapshot the ID now â _rmId may change while awaiting if user opens another coupon 8978 const voteId = _rmId; 8979 const existing = getVoteState(voteId); 8980 // Same direction â no-op 8981 if ((dir === 1 && existing === 'up') || (dir === -1 && existing === 'down')) return; 8982 8983 const upBtn = document.getElementById('rm-vote-up'); 8984 const dnBtn = document.getElementById('rm-vote-down'); 8985 const upCnt = document.getElementById('rm-vote-up-count'); 8986 const dnCnt = document.getElementById('rm-vote-down-count'); 8987 8988 try { 8989 const voteUrl = `/api/coupons/${voteId}/vote`; 8990 const downUrl = `/api/coupons/${voteId}/downvote`; 8991 8992 if (dir === 1) { 8993 const r = await fetch(voteUrl, { method: 'POST' }); 8994 if (!r.ok) return; 8995 if (_rmId !== voteId) return; // modal moved to a different coupon â bail 8996 if (existing === 'down') { 8997 // Cancel the previous downvote (net +2 total) 8998 await fetch(voteUrl, { method: 'POST' }); 8999 if (_rmId !== voteId) return; 9000 dnCnt.textContent = Math.max(0, parseInt(dnCnt.textContent) - 1); 9001 dnBtn.disabled = false; dnBtn.classList.remove('voted'); 9002 } 9003 upCnt.textContent = parseInt(upCnt.textContent) + 1; 9004 upBtn.disabled = true; upBtn.classList.add('voted'); 9005 } else { 9006 const r = await fetch(downUrl, { method: 'POST' }); 9007 if (!r.ok) return; 9008 if (_rmId !== voteId) return; // modal moved to a different coupon â bail 9009 if (existing === 'up') { 9010 // Cancel the previous upvote (net -2 total) 9011 await fetch(downUrl, { method: 'POST' }); 9012 if (_rmId !== voteId) return; 9013 upCnt.textContent = Math.max(0, parseInt(upCnt.textContent) - 1); 9014 upBtn.disabled = false; upBtn.classList.remove('voted'); 9015 } 9016 dnCnt.textContent = parseInt(dnCnt.textContent) + 1; 9017 dnBtn.disabled = true; dnBtn.classList.add('voted'); 9018 } 9019 9020 localStorage.setItem(`vote_${voteId}`, dir === 1 ? 'up' : 'down'); 9021 } catch(_) {} 9022} 9023 9024// ââ On load: auto-search when opened via "×¢×× ×§×פ×× ×× ×©×" link (?q=STORE) ââââ 9025(function checkSearchParam() { 9026 const p = new URLSearchParams(window.location.search); 9027 const q = p.get('q'); 9028 if (!q || p.get('showReveal') === '1') return; 9029 const run = () => { 9030 const inp = document.getElementById('pub-search'); 9031 if (inp) { 9032 inp.value = q; 9033 history.replaceState(null, '', window.location.pathname); 9034 _storeFilterMode = true; 9035 _storeFilterName = q.trim().toLowerCase(); 9036 if (typeof aiSearch === 'function') aiSearch(); 9037 } 9038 }; 9039 // Use a small delay so the page's own init (COUPONS load, showPage) finishes first 9040 if (document.readyState === 'loading') { 9041 document.addEventListener('DOMContentLoaded', () => setTimeout(run, 150)); 9042 } else { 9043 setTimeout(run, 150); 9044 } 9045})(); 9046 9047 9048// ââ On load: check if opened via reverse popup ââââââââââââââââââââââââââââââââ 9049(function checkRevealParams() { 9050 const p = new URLSearchParams(window.location.search); 9051 if (p.get('showReveal') !== '1') return; 9052 const code = p.get('rc') || ''; 9053 const store = p.get('rs') || ''; 9054 const storeHome = p.get('rh') || ''; 9055 const goUrl = p.get('ru') || ''; 9056 const discount = p.get('rd') || ''; 9057 const id = p.get('ri') || ''; 9058 const notes = p.get('rn') || ''; 9059 const sourceSrc = p.get('rsrc') || ''; 9060 const ageLabel = p.get('ral') || ''; 9061 const pubLabel = p.get('rpl') || ''; 9062 const votes = p.get('rv') || '0'; 9063 if (!code) return; 9064 const newsletter = p.get('rns') || ''; 9065 const run = () => _storeDataReady.then(() => 9066 showRevealModal(code, store, storeHome, goUrl, discount, id, notes, sourceSrc, ageLabel, pubLabel, votes, newsletter) 9067 ); 9068 if (document.readyState === 'loading') { 9069 document.addEventListener('DOMContentLoaded', run); 9070 } else { 9071 run(); 9072 } 9073})(); 9074 9075// ââ Store Pages Demo âââââââââââââââââââââââââââââââââââââââââââââââââââââ 9076let _sdCurrentLayout = 'a'; 9077let _sdCurrentStore = 'ksp'; 9078 9079const _STORE_DEMO_DATA = { 9080 ksp: { 9081 name: 'KSP', nameHe: 'KSP', domain: 'ksp.co.il', origin: 'IL', 9082 categories: ['×××§×ר×× ××§×', '×××××× ×', '××ש×××'], 9083 desc: 'KSP ××× ××ת ×רשת×ת ××××§×ר×× ××§× ××××××ת ××שר××, ×¢× ×¢×©×¨×ת ×¡× ×פ×× ×ר××× ××רץ ××× ×ת ××× ×××× ×¢× ×¤×. ××× ×ת ×ת××× ×××ש×××, ר×××××, ס××ר×פ×× ×× ××××××¨× ×××××× ×.', 9084 longDesc: `<p>KSP ××× ××ת ×רשת×ת ××××§×ר×× ××§× ××××××ת ××שר××, ×¢× ×¢×©×¨×ת ×¡× ×פ×× ×ר××× ××רץ ××× ×ת ××× ×××× ×¢× ×¤×. ××× ×ת ×ת××× ×××ש×××, ר×××××, ס××ר×פ×× ××, ×××××¨× ×××××× × ×צ××× ×××§×¤× â ××××רת ×× ×××§×××ת פר×××× ××× ×עסק××.</p><p>KSP ××××¢× ××× ×©×××ר×× ×ª×ר×ת××× ××××××, ×××¢×ת×× ×§×¨×××ת × ××ª× ××צ×× ×§×× ×§×פ×× ×©××ס××£ ×¢×× ××× ×××××× ×¢× ××× ××××ר ×××ר ××××. ××§×פ×× ×× ×× ×¤×צ×× ×××תר ×× ×× ×× ××××××× ×¢× ×§× ×× ××¢× ×¡××× ×ס×××, ×ש××× ××× ×, ×× ×¢×¡×§××ת bundle ×¢× ×¦××× ×××××× ×.</p>`, 9085 tip: '×××× ×××××§ ×§×פ×× × KSP ××¤× × ×¨××שת ×××©× × ×××, ×ס×, ×× ×ר××ס ××¡× â ××× ×פר×××× ×©×¢×××× × ××ª× ××ס×× ××× ×ר××.', 9086 faq: [ 9087 { q: '××× ××שת×ש ××§×× ×§×פ×× KSP?', a: '×עת ××§× ××× ××תר ksp.co.il, ×××¥ ×¢× "×¡× ×§× ××ת" ××ש×× "×§×× ××××" ××× ×¡ ×ת ××§××. ××× ×× ×ª×××©× ×××××××ת.' }, 9088 { q: '××× × ××ª× ××ס×× ×¢× ×§×פ×× KSP?', a: '××× ××ת × ×¢×ת ××× 5% ×-20% ××ת×× ××§×פ××. ×¢× ×¨××ש×ת ×××××ת ××× ××ש××× × ××××× × ××ª× ××ס×× ×××ת שק×××.' }, 9089 { q: '××× ××§×פ×× ×× ×©× KSP פ××× ×ª××§×£?', a: '××, ר×× ××§×פ×× ×× ×ª×§×¤×× ×תק××¤× ×××××ת. ×××× ×××××§ ×ת ת×ר×× ×תפ××× ××¤× × ×ש×××ש.' }, 9090 ], 9091 coupons: [ 9092 { code: 'KSP15', discount: '15% ×× ×× ×¢× ×× ××תר', notes: '××× ×××× ×§× ××× 300âª', expiry: '30/06/2026', votes: 42 }, 9093 { code: 'KSPSHIP', discount: '×ש××× ××× ×', notes: '×¢× ×××× × ××¢× 200âª', expiry: null, votes: 28 }, 9094 { code: 'SCREEN20', discount: '20% ×¢× ×ס×××', notes: '××§×××ר××ת ×ס××× ××××', expiry: '15/07/2
9094026', votes: 19 }, 9095 { code: 'LAPTOP100', discount: '100⪠×× ××', notes: '×¢× ××ש××× × ××××× ××¢× 3,000âª', expiry: '01/07/2026', votes: 15 }, 9096 { code: 'STUDENT10', discount: '10% ×ס×××× ×××', notes: '×¢× ×ª×¢××ת ס×××× × ××××', expiry: null, votes: 11 }, 9097 { code: 'GAMING25', discount: '25% צ××× ×××××× ×', notes: '×¢××ר, ××§××ת, ×××× ××ת', expiry: '20/07/2026', votes: 8 }, 9098 ] 9099 }, 9100 superpharm: { 9101 name: 'ס×פר פ×ר×', nameHe: 'ס×פר פ×ר×', domain: 'super-pharm.co.il', origin: 'IL', 9102 categories: ['×§×ס××××§×', '×ר×××ת', 'תר×פ×ת'], 9103 desc: 'ס×פר פ××¨× ××× ×¨×©×ª ×× ×××ת ×פ×ר×צ××××§×, ××§×ס××××§× ×××ר×××ת ××××××× ××שר×× ×¢× ×××ת ×¡× ×פ×× ×ר××× ××רץ.', 9104 longDesc: `<p>ס×פר פ××¨× ××× ×¨×©×ª ×× ×××ת ×פ×ר×צ××××§×, ××§×ס××××§× ×××ר×××ת ××××××× ××שר××. ×¢× ×××ת ×¡× ×פ××, ××× ×צ××¢× ××××× ×¢×¦×× ×©× ××צר×× â ××××¦×¨× ××פ×× ××ש×××, ××¨× ×ª×¨×פ×ת ××× ×רש×, ××¢× ×××¦×¨× ×ª×× ××§×ת ×ת××× ×.</p><p>××ת ××ר××× ×× ×¤×צ×ת ××ס×× ×ס×פר פ××¨× ××× ×©×××ש ××§×פ×× × ×× ×× ××פ××קצ×× ×××תר, ×ש×××× ×¢× ×ר××ס ×××¢××× Dream Card.</p>`, 9105 tip: '××§×פ×× ×× ×©× ×¡×פר פ××¨× ×ר×× ×ª×§×¤×× ×××× ×§×¦×¨ â ש××× ×××××§ ×××ª× ×ת×××ת ×× ×××ש.', 9106 faq: [ 9107 { q: '××× ×שת×ש×× ××§×פ×× ×¡×פר פ××¨× ××× ××××?', a: '××¢××× ×תש××× ××תר super-pharm.co.il ×ש ש×× "×§×× ×§×פ××" â ××× ×ת ××§×× ××¤× × ×ש××ת ×ר××ש×.' }, 9108 { q: '××× × ××ª× ×ש×× ×§×פ×× ×¢× Dream Card?', a: '×××¨× ××× ×× â ×§×פ×× ×× ××××¢××× Dream Card ×× ××××ת × ×¤×¨××ת. ××××§ ×ת ×ª× ×× ××§×פ××.' }, 9109 ], 9110 coupons: [ 9111 { code: 'PHARM15', discount: '15% ×¢× ×§×ס××××§×', notes: '×ª× ××× ××××', expiry: '30/06/2026', votes: 55 }, 9112 { code: 'BEAUTY20', discount: '20% ×ש××× ××××¦×¨× ××קר×', notes: '××× ×××× 200âª', expiry: '15/07/2026', votes: 33 }, 9113 { code: 'HEALTH10', discount: '10% ת××¡×¤× ×ª××× ×', notes: '×¢× ×××× × ××תר', expiry: null, votes: 21 }, 9114 { code: 'FREESHIP', discount: '×ש××× ××× ×', notes: '××× ××× ××××', expiry: '01/07/2026', votes: 18 }, 9115 { code: 'BABY25', discount: '25% ×××¦×¨× ×ª×× ××§×ת', notes: '×§×××ר×× ×××¢×××ת', expiry: null, votes: 12 }, 9116 ] 9117 }, 9118 aliexpress: { 9119 name: 'AliExpress', nameHe: 'AliExpress', domain: 'aliexpress.com', origin: 'INTL', 9120 categories: ['×××§×ר×× ××§×', '×××¤× ×', '××ת', '××××\'×××'], 9121 desc: 'AliExpress ××× ×¤××פ×ר×ת ××§× ××ת ×××× ×××××ת ×©× Alibaba ×¢× ×××××× × ××צר×× ××××ר×× ×× ××××× ×××תר ××¢×××.', 9122 longDesc: `<p>AliExpress ××× ×¤××פ×ר×ת ××§× ××ת ×××× ×××××ת ×©× Alibaba, ×××פשרת ר×××©× ×ש××¨× ××××ר×× ×ס×× ××ר××× ××¢×××. ××תר ×צ××¢ ××צר×× ××§×××ר××ת ×××-ספ×ר â ×××§×ר×× ××§×, ×××¤× ×, צ××× ×××ת, ת×ש××××, צעצ××¢×× ××¢××.</p><p>××§×פ×× ×× ××××¢×× ×××× ×¦×ר×ת: ×§×פ×× × ×××ר ספצ×פ×, ×§×פ×× × ×תר ר××××, ××××¦×¢× ×¢×× × ××× 11.11 ×-Super Sale.</p>`, 9123 tip: '×××× ××ס××£ ×§×פ×× ×× ××¨× ×¡× ××§× ××ת ××¤× × ×ר×××©× ××ש×× ×ספר ×§×פ×× ×× ××× ×××ס××× ×קס××××.', 9124 faq: [ 9125 { q: '××× ×שת×ש×× ××§×× ×§×פ×× AliExpress?', a: '××¡× ××§× ××ת ×××¥ ×¢× "××סף ×§×× ×§×פ××". × ××ª× ×ש×× ×§×פ×× ×××ר ××§×פ×× ×תר ××××ª× ×¢×¡×§×.' }, 9126 { q: '××× ××× ×××§× ××ש××× ×-AliExpress ××שר××?', a: '×××¨× ××× 2â4 ש×××¢×ת. ×¢× AliExpress Standard Shipping × ××ª× ××§×× ×ª×× 7â14 ××××.' }, 9127 ], 9128 coupons: [ 9129 { code: 'NEW10', discount: '10$ ×× ×× ××××× × ×¨×ש×× ×', notes: '××× ×××× 15$', expiry: null, votes: 88 }, 9130 { code: 'ALI20', discount: '20% ×¢× ×××§×ר×× ××§×', notes: '××¢× 50$', expiry: '30/06/2026', votes: 44 }, 9131 { code: 'SHIP0', discount: '×ש××× ××× ×', notes: '××××ר×× × ××ר××', expiry: null, votes: 37 }, 9132 { code: 'SUMMER15', discount: '15$ ×¢× 100$', notes: '×§××¥ 2026', expiry: '31/08/2026', votes: 29 }, 9133 ] 9134 }, 9135 asos: { 9136 name: 'ASOS', nameHe: 'ASOS', domain: 'asos.com', origin: 'INTL', 9137 categories: ['×××¤× ×', '× ×¢××××', '××××ר××'], 9138 desc: 'ASOS ××× ××ת ×פ××פ×ר××ת ××××¤× × ××××××ת ××¢××× ×¢× ×××¢×× ×-850 ××ת××× ××§××קצ×× ×¢×¦××× ×©× ××××× ×× ×¢××××.', 9139 longDesc: `<p>ASOS ××× ××ת ×פ××פ×ר××ת ××××¤× × ××××××ת ××¢×××, ×¢× ×××¢×× ×-850 ××ת××× ××§××קצ×× ×¢×¦××× ×©× ×××××, × ×¢×××× ×××××ר×× ×××ר×× ××× ×©××. ASOS ××פשרת ×ש××× ××שר××.</p><p>×§×פ×× × ASOS ××פ××¢×× ×××¨× ××× ××× ×× ××××××× ×¢× ×× ××××× ×, ×××¢×ת×× ×× ××ש××× ××× ×. ×× ××ת ×©× 10%â20% × ×¤×צ×ת.</p>`, 9140 tip: 'ASOS ×צ××¢× ×ש××× ××× × ××¢× ×¡××× ××× ×××× â ש××× ××××ר ××× ×¤×¨×××× ××¡× ××××××¢ ×סף, ××× ×××ס××£ ×§×× ×§×פ××.', 9141 faq: [ 9142 { q: '××× ××שת×ש ××§×פ×× ASOS?', a: '×××£ ×תש××× ×ש ש×× "××× ×§×× ×¤×¨×××". ××× ×¡ ×ת ××§×× ××¤× × ×ש××ת ×ר××ש×.' }, 9143 ], 9144 coupons: [ 9145 { code: 'ASOS20', discount: '20% ×¢× ×× ××××× ×', notes: '××× ×××× 150âª', expiry: '30/06/2026', votes: 61 }, 9146 { code: 'ASOSSHIP', discount: '×ש××× ××× ×', notes: '××× ××× ××××', expiry: null, votes: 38 }, 9147 { code: 'SALE15', discount: '15% ס××× ×§××¥', notes: '×× ×¢× ×¤×¨××× ×¡×××', expiry: '31/08/2026', votes: 22 }, 9148 { code: 'NEW25', discount: '25% ×××§×××ת ××ש××', notes: '×××× × ×¨×ש×× × ××××', expiry: null, votes: 17 }, 9149 ] 9150 }, 9151 castro: { 9152 name: 'Castro', nameHe: 'קס×ר×', domain: 'castro.com', origin: 'IL', 9153 categories: ['×××¤× ×', '× ×©××', '××ר××', '×××××'], 9154 desc: 'קס××¨× ××× ×¨×©×ª ×××¤× × ×שר×××ת ×××××× ×¢× ×¢×©×¨×ת ×©× ×ת × ×ס×××, ××צ××¢× ×§××קצ××ת ×× ×©××, ××ר×× ××××××.', 9155 longDesc: `<p>קס××¨× ××× ××ת ×רשת×ת ××××¤× × ×××ת××§×ת ××××××××ת ××שר××, ×¢× ×¢×©×¨×ת ×¡× ×פ×× ×ר××× ××רץ ××× ×ת ××× ××××. ××××¨× ×צ××¢× ×§××קצ××ת ×¢×× ×ª××ת ×× ×©××, ××ר×× ×××××× ×ª×ת ×ספר ××ת×××.</p><p>×§×פ×× × ×§×¡××¨× × ×¤×צ×× ×××× ×××××¢×× ××¨× ×× ×××××ר, ××פ××קצ×× ×רשת×ת ××רת××ת. ×ר×× ××××ר ××× ××ת ×©× 10%â30% ×¢× ×ר××ש×.</p>`, 9156 tip: '×××¨×©× ×× ×××××ר ×©× ×§×¡××¨× ××§××ת ×§×פ×× ×§××ת ×¤× ×× ×©× 15% ××¢×××× ×× ×¢× ××צע×× ×××¢××××.', 9157 faq: [ 9158 { q: '××× ××שת×ש ××§×× ×§×פ×× ×§×¡×ר×?', a: '××¢××× ×תש××× ××תר castro.com ×ש ש×× "×§×× ××××". ××× ×¡ ×ת ××§×× ××¤× × ××ש×ר ××××× ×.' }, 9159 { q: '××× × ××ª× ××ס×× ×¢× ×§×פ×× ×§×¡×ר×?', a: '×××¨× ××× 10%â30% ×¢× ×××× ×. ×תק×פ×ת ס××× × ××ª× ×ש×× ××××××¢ ×××ס××× ×××× ××תר.' }, 9160 { q: '××× × ××ª× ××שת×ש ××§×פ×× ×§×¡××¨× ××¡× ×פ××?', a: 'ר×× ××§×פ×× ×× ×××§××× ×× ×ª×§×¤×× ××תר ××××. ×§×פ×× × ×¡× ×פ×× ××××¢×× ×× ×¤×¨× ××¨× ××פ××קצ××.' }, 9161 ], 9162 coupons: [ 9163 { code: 'CASTRO20', discount: '20% ×¢× ×× ××§××קצ××', notes: '×× ×¢× ×¤×¨××× ×¡×××', expiry: '15/07/2
9163026', votes: 74 }, 9164 { code: 'WELCOME15', discount: '15% ×××§×××ת ××ש××', notes: '×רש×× ×× ×××××ר', expiry: null, votes: 51 }, 9165 { code: 'SUMMER30', discount: '30% ×§××קצ××ת ×§××¥', notes: 'פר×××× × ××ר××', expiry: '31/08/2026', votes: 39 }, 9166 { code: 'SHIP0', discount: '×ש××× ××× ×', notes: '××¢× 250âª', expiry: null, votes: 27 }, 9167 { code: 'KIDS25', discount: '25% ×××ר ×××××', notes: '×§×××ר×× ×××¢×××ת', expiry: '30/06/2026', votes: 18 }, 9168 { code: 'APP10', discount: '10% ××¨× ××פ××קצ××', notes: '×××× × ××¨× ×פ××קצ×× ××××', expiry: null, votes: 14 }, 9169 ] 9170 } 9171}; 9172 9173// ââ Share SVG icon ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 9174const _SD_SHARE_ICON = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>`; 9175 9176function _sdCouponCard(c, storeName) { 9177 const peek = (c.code || '').slice(-2); 9178 const shareUrl = `couponai.co.il/store/${_storeSlug(storeName)}#coupon-${encodeURIComponent(c.code)}`; 9179 return ` 9180 <div style="background:var(--gray1);border:1px solid ${c.votes>30?'rgba(0,232,122,0.3)':'var(--gray3)'};border-radius:12px;padding:16px;position:relative;overflow:hidden"> 9181 ${c.votes>30 ? `<div style="position:absolute;top:0;left:0;right:0;height:2px;background:var(--accent)"></div>` : ''} 9182 <div style="font-size:22px;font-weight:800;color:var(--white);margin-bottom:6px;letter-spacing:-0.5px">${c.discount}</div> 9183 ${c.notes ? `<div style="font-size:12px;color:var(--gray5);margin-bottom:12px">${c.notes}</div>` : ''} 9184 <div style="display:flex;align-items:center;gap:8px;direction:ltr"> 9185 <div style="flex:1;background:#00e87a;border-radius:6px;padding:10px 12px;font-family:monospace;font-size:14px;font-weight:800;color:#000;letter-spacing:2px;cursor:pointer;text-align:center"> 9186 <span style="opacity:0.4">â¢â¢â¢â¢</span>${peek} 9187 </div> 9188 <button onclick="navigator.clipboard.writeText('${shareUrl}')" title="שתף ×§×פ×× ××" style="background:var(--gray3);border:1px solid var(--gray4);border-radius:6px;padding:10px;cursor:pointer;color:var(--gray5);display:flex;align-items:center">${_SD_SHARE_ICON}</button> 9189 </div> 9190 <div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px"> 9191 ${c.expiry ? `<span style="font-size:11px;color:var(--gray5)">ת××§×£ ×¢× ${c.expiry}</span>` : `<span></span>`} 9192 <span style="font-size:11px;color:var(--gray5)">ð ${c.votes}</span> 9193 </div> 9194 </div>`; 9195} 9196 9197function _sdCouponRow(c, storeName) { 9198 const shareUrl = `couponai.co.il/store/${_storeSlug(storeName)}#coupon-${encodeURIComponent(c.code)}`; 9199 return ` 9200 <div style="display:flex;align-items:center;gap:16px;padding:14px 16px;border-bottom:1px solid var(--gray3);direction:rtl"> 9201 <div style="flex:1;min-width:0"> 9202 <div style="font-size:15px;font-weight:700;color:var(--white)">${c.discount}</div> 9203 ${c.notes ? `<div style="font-size:12px;color:var(--gray5);margin-top:2px">${c.notes}</div>` : ''} 9204 </div> 9205 <div style="display:flex;align-items:center;gap:8px;flex-shrink:0"> 9206 ${c.expiry ? `<span style="font-size:11px;color:var(--gray5)">${c.expiry}</span>` : ''} 9207 <button onclick="navigator.clipboard.writeText('${shareUrl}')" title="שתף" style="background:transparent;border:none;cursor:pointer;color:var(--gray5);padding:4px;display:flex">${_SD_SHARE_ICON}</button> 9208 <div style="background:#00e87a;border-radius:6px;padding:8px 14px;font-family:monospace;font-size:12px;font-weight:800;color:#000;letter-spacing:1px;cursor:pointer;white-space:nowrap">
9209 <span style="opacity:0.4">â¢â¢</span>${(c.code||'').slice(-2)} ××× 9210 </div> 9211 </div> 9212 </div>`; 9213} 9214 9215// ââ Layout A: Hero + Grid âââââââââââââââââââââââââââââââââââââââââââââââââ 9216function _sdLayoutA(store) { 9217 const fav = `https://www.google.com/s2/favicons?domain=${store.domain}&sz=64`; 9218 const cats = store.categories.map(c => `<span style="background:var(--gray3);border-radius:20px;padding:3px 10px;font-size:11px;color:var(--gray5)">${c}</span>`).join(''); 9219 const cards = store.coupons.map(c => _sdCouponCard(c, store.name)).join(''); 9220 return ` 9221 <div style="background:var(--black);direction:rtl;font-family:'DM Sans',sans-serif"> 9222 <!-- Nav --> 9223 <div style="background:var(--gray1);border-bottom:1px solid var(--gray3);padding:14px 28px;display:flex;align-items:center;justify-content:space-between"> 9224 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:800">×§×פ××<span style="color:var(--accent)">AI</span></div> 9225 <div style="font-size:12px;color:var(--gray5)">ר××©× › ×× ×××ת › ${store.nameHe}</div> 9226 </div> 9227 <!-- Hero --> 9228 <div style="background:linear-gradient(180deg,var(--gray1) 0%,var(--black) 100%);padding:36px 28px 28px"> 9229 <div style="display:flex;align-items:center;gap:18px;margin-bottom:16px"> 9230 <img src="${fav}" width="64" height="64" style="border-radius:14px;background:var(--gray2);padding:4px" onerror="this.style.display='none'"/> 9231 <div> 9232 <h1 style="font-family:'Syne',sans-serif;font-size:28px;font-weight:800;margin:0 0 4px">${store.nameHe} - ×§×פ×× ×× ××× ××ת</h1> 9233 <div style="font-size:12px;color:var(--gray5)">×¢×××× ××× × 2026 • ${store.coupons.length} ×§×פ×× ×× ×¤×¢××××</div> 9234 </div> 9235 <div style="margin-right:auto;display:flex;align-items:center;gap:8px"> 9236 <button style="background:var(--accent);color:#000;border:none;border-radius:8px;padding:9px 16px;font-size:13px;font-weight:700;cursor:pointer;display:flex;align-items:center;gap:6px">${_SD_SHARE_ICON} שתף ××£ ×× ×ת</button> 9237 <a href="#" style="background:var(--gray3);color:var(--white);border:none;border-radius:8px;padding:9px 16px;font-size:13px;cursor:pointer;text-decoration:none">×× ××¡× ××תר â</a> 9238 </div> 9239 </div> 9240 <div style="font-size:13px;color:var(--gray5);line-height:1.7;max-width:640px;margin-bottom:16px">${store.desc}</div> 9241 <div style="display:flex;gap:6px;flex-wrap:wrap">${cats}</div> 9242 <!-- Stats --> 9243 <div style="display:flex;gap:24px;margin-top:20px;padding-top:20px;border-top:1px solid var(--gray3)"> 9244 <div><div style="font-size:22px;font-weight:800;font-family:'Syne',sans-serif;color:var(--accent)">${store.coupons.length}</div><div style="font-size:11px;color:var(--gray5)">×§×פ×× ×× ×¤×¢××××</div></div> 9245 <div><div style="font-size:22px;font-weight:800;font-family:'Syne',sans-serif">${store.coupons[0]?.discount?.includes('%') ? store.coupons[0].discount.match(/\d+/)?.[0]+'%' : '×¢× 30%'}</div><div style="font-size:11px;color:var(--gray5)">×× ×× ×קס××××ת</div></div> 9246 <div><div style="font-size:22px;font-weight:800;font-family:'Syne',sans-serif">${store.origin==='IL' ? 'ð®ð±' : 'ð'}</div><div style="font-size:11px;color:var(--gray5)">${store.origin==='IL' ? '×× ×ת ×שר×××ת' : '×× ×ת ××× ×××××ת'}</div></div> 9247 </div> 9248 </div> 9249 <!-- Coupons grid --> 9250 <div style="padding:0 28px 36px"> 9251 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700;margin-bottom:16px">×× ××§×פ×× ×× ×©× ${store.nameHe}</div> 9252 <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px">${cards}</div> 9253 </div> 9254 <!-- About --> 9255 <div style="padding:0 28px 36px;border-top:1px solid var(--gray3)"> 9256 <div style="font-family:'Syne',sans-serif;font-size:16px;font-weight:700;margin-bottom:12px">××××ת ${store.nameHe}</div> 9257 <div style="font-size:13px;color:var(--gray5);line-height:1.8">${store.longDesc}</div> 9258 <div style="margin-top:14px;background:rgba(0,232,122,0.07);border:1px solid rgba(0,232,122,0.2);border-radius:10px;padding:12px 16px;font-size:13px;color:var(--gray6)"> 9259 ð¡ <strong style="color:var(--accent)">××פ:</strong> ${store.tip} 9260 </div> 9261 </div> 9262 </div>`; 9263} 9264 9265// ââ Layout B: Sidebar + List ââââââââââââââââââââââââââââââââââââââââââââââ 9266function _sdLayoutB(store) { 9267 const fav = `https://www.google.com/s2/favicons?domain=${store.domain}&sz=64`; 9268 const cats = store.categories.map(c => `<div style="padding:6px 0;border-bottom:1px solid var(--gray3);font-size:12px;color:var(--gray5)">${c}</div>`).join(''); 9269 const rows = store.coupons.map(c => _sdCouponRow(c, store.name)).join(''); 9270 return ` 9271 <div style="background:var(--black);direction:rtl;font-family:'DM Sans',sans-serif"> 9272 <!-- Nav --> 9273 <div style="background:var(--gray1);border-bottom:1px solid var(--gray3);padding:14px 28px;display:flex;align-items:center;justify-content:space-between"> 9274 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:800">×§×פ××<span style="color:var(--accent)">AI</span></div> 9275 <div style="font-size:12px;color:var(--gray5)">ר××©× › ×× ×××ת › ${store.nameHe}</div> 9276 </div> 9277 <div style="display:flex;gap:0;min-height:600px"> 9278 <!-- Sidebar --> 9279 <div style="width:260px;flex-shrink:0;border-left:1px solid var(--gray3);padding:24px 20px;background:var(--gray1)"> 9280 <div style="text-align:center;margin-bottom:20px;padding-bottom:20px;border-bottom:1px solid var(--gray3)"> 9281 <img src="${fav}" width="56" height="56" style="border-radius:12px;background:var(--gray2);padding:4px;margin-bottom:10px" onerror="this.style.display='none'"/>
9281 9282 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700">${store.nameHe}</div> 9283 <div style="font-size:11px;color:var(--gray5);margin-top:2px">${store.domain}</div> 9284 <div style="margin-top:10px;background:rgba(0,232,122,0.1);border-radius:6px;padding:3px 10px;font-size:11px;color:var(--accent);display:inline-block">${store.origin==='IL' ? '×× ×ת ×שר×××ת' : '×× ×ת ××× ×××××ת'}</div> 9285 </div> 9286 <div style="margin-bottom:16px"> 9287 <div style="font-size:11px;font-weight:600;color:var(--gray5);margin-bottom:8px;letter-spacing:0.5px">×§×××ר××ת</div> 9288 ${cats} 9289 </div> 9290 <div style="margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--gray3)"> 9291 <div style="font-size:11px;font-weight:600;color:var(--gray5);margin-bottom:8px;letter-spacing:0.5px">ס×××ס×××§×ת</div> 9292 <div style="font-size:13px;color:var(--gray6);margin-bottom:4px">${store.coupons.length} ×§×פ×× ×× ×¤×¢××××</div> 9293 <div style="font-size:13px;color:var(--gray6)">×¢××××: ××× × 2026</div> 9294 </div> 9295 <a href="#" style="display:block;text-align:center;background:var(--accent);color:#000;border-radius:8px;padding:10px;font-size:13px;font-weight:700;text-decoration:none;margin-bottom:8px">×× ××¡× ××תר â</a> 9296 <button onclick="navigator.clipboard.writeText('couponai.co.il/store/${_storeSlug(store.name)}')" style="display:block;width:100%;text-align:center;background:var(--gray3);color:var(--white);border:none;border-radius:8px;padding:10px;font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:6px">${_SD_SHARE_ICON} שתף ××£ ×× ×ת</button> 9297 </div> 9298 <!-- Main --> 9299 <div style="flex:1;min-width:0"> 9300 <div style="padding:24px 28px;border-bottom:1px solid var(--gray3)"> 9301 <h1 style="font-family:'Syne',sans-serif;font-size:22px;font-weight:800;margin:0 0 8px">×§××× ×§×פ×× ${store.nameHe} - ×¢×××× ××× × 2026</h1> 9302 <div style="font-size:13px;color:var(--gray5);line-height:1.7">${store.desc}</div> 9303 <div style="margin-top:14px;background:rgba(0,232,122,0.07);border:1px solid rgba(0,232,122,0.2);border-radius:8px;padding:10px 14px;font-size:12px;color:var(--gray6)"> 9304 ð¡ ${store.tip} 9305 </div> 9306 </div> 9307 <div style="padding:20px 28px 8px;display:flex;align-items:center;justify-content:space-between"> 9308 <div style="font-family:'Syne',sans-serif;font-size:15px;font-weight:700">${store.coupons.length} ×§×פ×× ×× ×××× ××</div> 9309 <div style="font-size:11px;color:var(--gray5)">××××× ××¤× ×צ××¢×ת</div> 9310 </div> 9311 <div style="background:var(--gray1);border-radius:0">${rows}</div> 9312 <!-- About --> 9313 <div style="padding:24px 28px"> 9314 <div style="font-family:'Syne',sans-serif;font-size:15px;font-weight:700;margin-bottom:10px">××××ת ${store.nameHe}</div> 9315 <div style="font-size:13px;color:var(--gray5);line-height:1.8">${store.longDesc}</div> 9316 </div> 9317 </div> 9318 </div> 9319 </div>`; 9320} 9321 9322// ââ Layout C: Minimal SEO âââââââââââââââââââââââââââââââââââââââââââââââââ 9323function _sdLayoutC(store) { 9324 const fav = `https://www.google.com/s2/favicons?domain=${store.domain}&sz=32`; 9325 const rows = store.coupons.map((c, i) => ` 9326 <tr style="border-bottom:1px solid var(--gray3)"> 9327 <td style="padding:14px 16px;font-size:14px;font-weight:700;color:var(--white)">${c.discount}</td> 9328 <td style="padding:14px 16px;font-family:monospace;font-size:13px"> 9329 <span style="background:var(--gray3);border-radius:6px;padding:6px 12px;color:var(--gray5);letter-spacing:1px">â¢â¢â¢â¢${(c.code||'').slice(-2)}</span> 9330 </td> 9331 <td style="padding:14px 16px;font-size:12px;color:var(--gray5)">${c.notes||''}</td> 9332 <td style="padding:14px 16px;font-size:12px;color:var(--gray5);white-space:nowrap">${c.expiry||'××× ×ª××§×£'}</td> 9333 <td style="padding:14px 16px"> 9334 <div style="display:flex;align-items:center;gap:6px"> 9335 <button style="background:#00e87a;border:none;border-radius:6px;padding:7px 12px;font-size:12px;font-weight:700;color:#000;cursor:pointer;white-space:nowrap">××× ×§××</button> 9336 <button onclick="navigator.clipboard.writeText('couponai.co.il/store/${_storeSlug(store.name)}#coupon-${encodeURIComponent(c.code)}')" title="שתף" style="background:var(--gray3);border:none;border-radius:6px;padding:7px;cursor:pointer;color:var(--gray5);display:flex">${_SD_SHARE_ICON}</button> 9337 </div> 9338 </td> 9339 </tr>`).join(''); 9340 const faqHtml = store.faq.map(f => `
9341 <div style="border:1px solid var(--gray3);border-radius:10px;padding:16px 20px;margin-bottom:10px"> 9342 <div style="font-size:14px;font-weight:700;color:var(--white);margin-bottom:6px">${f.q}</div> 9343 <div style="font-size:13px;color:var(--gray5);line-height:1.6">${f.a}</div> 9344 </div>`).join(''); 9345 return ` 9346 <div style="background:var(--black);direction:rtl;font-family:'DM Sans',sans-serif;max-width:860px;margin:0 auto;padding:28px"> 9347 <!-- Breadcrumb --> 9348 <div style="font-size:12px;color:var(--gray5);margin-bottom:20px"> 9349 <a href="#" style="color:var(--gray5);text-decoration:none">ר×ש×</a> › 9350 <a href="#" style="color:var(--gray5);text-decoration:none">×× ×××ת</a> › 9351 <span style="color:var(--white)">${store.nameHe}</span> 9352 </div> 9353 <!-- Header --> 9354 <div style="display:flex;align-items:center;gap:12px;margin-bottom:8px"> 9355 <img src="${fav}" width="32" height="32" style="border-radius:8px;background:var(--gray2);padding:2px" onerror="this.style.display='none'"/> 9356 <h1 style="font-family:'Syne',sans-serif;font-size:24px;font-weight:800;margin:0">×§××× ×§×פ×× ${store.nameHe} - ${store.coupons.length} ×§×פ×× ×× ×©×¢××××× (×¢×××× ××× × 2026)</h1> 9357 </div> 9358 <div style="display:flex;gap:16px;align-items:center;margin-bottom:20px;padding-bottom:20px;border-bottom:1px solid var(--gray3)"> 9359 <div style="font-size:12px;color:var(--gray5)">${store.origin==='IL' ? 'ð®ð± ×× ×ת ×שר×××ת' : 'ð ×× ×ת ××× ×××××ת'} • ${store.domain}</div> 9360 <div style="margin-right:auto;display:flex;gap:8px"> 9361 <button onclick="navigator.clipboard.writeText('couponai.co.il/store/${_storeSlug(store.name)}')" style="background:var(--gray3);border:none;border-radius:6px;padding:7px 12px;font-size:12px;cursor:pointer;color:var(--gray5);display:flex;align-items:center;gap:5px">${_SD_SHARE_ICON} שתף ××£</button> 9362 <a href="#" style="background:var(--accent);color:#000;border-radius:6px;padding:7px 12px;font-size:12px;font-weight:700;text-decoration:none">×× ××¡× ××תר â</a> 9363 </div> 9364 </div> 9365 <!-- Intro text (rich content for Google) --> 9366 <div style="font-size:13px;color:var(--gray5);line-height:1.8;margin-bottom:24px">${store.longDesc}</div> 9367 <!-- Coupons table --> 9368 <div style="font-family:'Syne',sans-serif;font-size:16px;font-weight:700;margin-bottom:12px">${store.coupons.length} ×§×פ×× × ${store.nameHe} - ××× × 2026</div> 9369 <div style="background:var(--gray1);border:1px solid var(--gray3);border-radius:10px;overflow:hidden;margin-bottom:32px"> 9370 <table style="width:100%;border-collapse:collapse;font-size:13px"> 9371 <thead> 9372 <tr style="background:var(--gray2);border-bottom:1px solid var(--gray3)"> 9373 <th style="padding:12px 16px;font-weight:600;color:var(--gray5);text-align:right">×× ××</th> 9374 <th style="padding:12px 16px;font-weight:600;color:var(--gray5);text-align:right">×§××</th> 9375 <th style="padding:12px 16px;font-weight:600;color:var(--gray5);text-align:right">×ª× ×××</th> 9376 <th style="padding:12px 16px;font-weight:600;color:var(--gray5);text-align:right">ת××§×£</th> 9377 <th style="padding:12px 16px;font-weight:600;color:var(--gray5);text-align:right"></th> 9378 </tr> 9379 </thead> 9380 <tbody>${rows}</tbody> 9381 </table> 9382 </div> 9383 <!-- Tip --> 9384 <div style="background:rgba(0,232,122,0.07);border:1px solid rgba(0,232,122,0.2);border-radius:10px;padding:14px 18px;font-size:13px;color:var(--gray6);margin-bottom:32px"> 9385 ð¡ <strong style="color:var(--accent)">××פ:</strong> ${store.tip} 9386 </div> 9387 <!-- FAQ (great for Google) --> 9388 <div style="margin-bottom:32px"> 9389 <div style="font-family:'Syne',sans-serif;font-size:16px;font-weight:700;margin-bottom:14px">ש×××ת × ×¤×צ×ת - ${store.nameHe}</div> 9390 ${faqHtml} 9391 </div> 9392 <!-- Footer note --> 9393 <div style="font-size:11px;color:var(--gray4);padding-top:16px;border-top:1px solid var(--gray3)"> 9394 ××£ ×× ×¢×××× ×××ר×× × ×××× × 2026. ×× ××§×פ×× ×× × ×××§×× ×¢× ××× ××§××××. couponai.co.il ××× × ××ר×× ×ש×× ×××× ××ª× ×× ×ש×××ש ×©× ${store.nameHe}. 9395 </div> 9396 </div>`; 9397} 9398 9399// ââ Layout D: Enhanced Sidebar ââââââââââââââââââââââââââââââââââââââââââââ 9400// ââ Layout D helpers (named fns avoid quote-escaping in onclick) ââââââââââ 9401let _sdDStore = null; 9402function _sdDReveal(i) { 9403 const row = document.getElementById('dcp-row-' + i); 9404 const masked = document.getElementById('dcp-masked-' + i); 9405 const code = document.getElementById('dcp-code-' + i); 9406 if (!row || !masked || !code) return; 9407 row.classList.add('revealed'); 9408 masked.classList.add('hiding'); 9409 code.classList.add('visible'); 9410 if (_sdDStore) { 9411 navigator.clipboard.writeText(_sdDStore.coupons[i].code); 9412 setTimeout(() => showToast('â ××§×× ××עתק!'), 200); 9413 } 9414} 9415function _sdDCopy(i) { 9416 if (!_sdDStore) return; 9417 const code = _sdDStore.coupons[i].code; 9418 navigator.clipboard.writeText(code); 9419 showToast('â ××§×× ××עתק!'); 9420 const btn = document.getElementById('dcp-copy-' + i);
9421 const CHECK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>'; 9422 const COPY = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>'; 9423 if (btn) { btn.innerHTML = CHECK; setTimeout(() => { btn.innerHTML = COPY; }, 1500); } 9424} 9425function _sdDShare(i) { 9426 if (!_sdDStore) return; 9427 const url = 'couponai.co.il/store/' + _storeSlug(_sdDStore.name) + '#coupon-' + encodeURIComponent(_sdDStore.coupons[i].code); 9428 navigator.clipboard.writeText(url); 9429 const btn = document.getElementById('dcp-share-' + i); 9430 if (btn) { btn.style.color = 'var(--accent)'; setTimeout(() => { btn.style.color = 'var(--gray5)'; }, 1500); } 9431} 9432function _sdDLogoErr(img) { 9433 img.style.display = 'none'; 9434 if (_sdDStore) img.insertAdjacentHTML('afterend', _storeInitial(_sdDStore.nameHe, 80)); 9435} 9436 9437function _sdDSharePage() { 9438 if (!_sdDStore) return; 9439 const btn = document.getElementById('dcp-share-page'); 9440 navigator.clipboard.writeText('couponai.co.il/store/' + _storeSlug(_sdDStore.name)); 9441 if (btn) { btn.textContent = 'â ××עתק!'; setTimeout(() => { btn.innerHTML = _SD_SHARE_ICON + ' שתף ××£ ×× ×ת ××'; }, 1500); } 9442} 9443 9444function _sdLayoutD(store) { 9445 _sdDStore = store; 9446 const fav = `https://www.google.com/s2/favicons?domain=${store.domain}&sz=128`; 9447 const bestDiscount = store.coupons.reduce((best, c) => { 9448 const n = parseInt((c.discount||'').match(/\d+/)?.[0]||0); 9449 return n > best ? n : best; 9450 }, 0); 9451 const totalVotes = store.coupons.reduce((s,c) => s+(c.votes||0), 0); 9452 const cats = store.categories.map(c => 9453 `<span style="background:var(--gray3);border-radius:20px;padding:4px 12px;font-size:12px;color:var(--gray5);display:inline-block;margin:3px 3px 0 0">${c}</span>` 9454 ).join(''); 9455 9456 const related = Object.entries(_STORE_DEMO_DATA) 9457 .filter(([k,v]) => k !== _sdCurrentStore) 9458 .slice(0,5) 9459 .map(([k,v]) => ` 9460 <div onclick="_sdSetStore('${k}')" style="display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;cursor:pointer" onmouseover="this.style.background='var(--gray3)'" onmouseout="this.style.background='transparent'"> 9461 <img src="https://www.google.com/s2/favicons?domain=${v.domain}&sz=32" width="24" height="24" style="border-radius:6px;background:#fff" onerror="this.style.display='none'"/> 9462 <span style="font-size:12px;color:var(--gray6)">${v.nameHe}</span> 9463 <span style="margin-right:auto;font-size:11px;color:var(--accent)">${v.coupons.length} ×§×פ×× ××</span> 9464 </div>`).join(''); 9465 9466 const EYE_SVG = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`; 9467 const COPY_SVG = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>`; 9468 9469 const coupons = store.coupons.map((c,i) => { 9470 const peek = (c.code||'').slice(-2); 9471 const views = Math.floor(c.votes * 7 + 12); 9472 return ` 9473 <div style="background:var(--gray1);border:1px solid rgba(0,232,122,0.25);border-radius:14px;padding:18px 22px 12px 18px;position:relative;overflow:hidden;direction:rtl"> 9474 <div style="position:absolute;top:0;right:0;bottom:0;width:3px;background:var(--accent);border-radius:3px 0 0 3px"></div> 9475 <div style="display:flex;align-items:flex-start;gap:20px"> 9476 <!-- Right column: discount + notes only --> 9477 <div style="flex:1;min-width:0;padding-right:6px"> 9478 <div style="font-family:'Syne',sans-serif;font-size:26px;font-weight:800;color:var(--white);letter-spacing:-0.5px;margin-bottom:5px">${c.discount}</div> 9479 ${c.notes ? `<div style="font-size:14px;color:var(--gray5)">${c.notes}</div>` : ''} 9480 </div> 9481 <!-- Left column: direction:ltr â aligns everything to visual left --> 9482 <div style="display:flex;flex-direction:column;gap:8px;flex-shrink:0;direction:ltr;align-items:flex-start"> 9483 <!-- Row 1: share | hidden code | copy --> 9484 <div id="dcp-row-${i}" class="cc-code-row" style="display:flex;align-items:center;gap:6px;direction:ltr"> 9485 <button id="dcp-share-${i}" onclick="_sdDShare(${i})" title="שתף ×§×פ××" style="background:var(--gray2);border:1px solid var(--gray3);border-radius:7px;color:var(--gray5);cursor:pointer;width:38px;height:38px;display:flex;align-items:center;justify-content:center;flex-shrink:0">${_SD_SHARE_ICON}</button> 9486 <div class="cc-code" onclick="_sdDReveal(${i})" style="width:170px;height:38px;border-radius:7px;padding:0 14px;flex-shrink:0;font-size:15px"> 9487 <div id="dcp-masked-${i}" class="cc-code-masked">
9488 <span class="cc-code-peek"> 9489 <span class="cc-code-dots">â¢â¢â¢â¢â¢â¢</span> 9490 <span class="cc-code-end">${peek}</span> 9491 </span> 9492 <span class="cc-code-eye"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span> 9493 </div> 9494 <div class="cc-code-revealed" id="dcp-code-${i}">${c.code}</div> 9495 </div> 9496 <button id="dcp-copy-${i}" onclick="_sdDCopy(${i})" title="×עתק ×§××" class="cc-copy" style="border-radius:7px;height:38px;padding:0 12px;flex-shrink:0;align-self:stretch">${COPY_SVG}</button> 9497 </div> 9498 <!-- Row 2: voting --> 9499 <div style="display:flex;align-items:center;gap:6px"> 9500 <button class="vote-btn vote-up" style="padding:5px 10px;font-size:12px">${THUMB_UP} <span>${c.votes}</span></button> 9501 <button class="vote-btn vote-down" style="padding:5px 10px;font-size:12px">${THUMB_DOWN}</button> 9502 </div> 9503 </div> 9504 </div> 9505 <!-- Bottom row: views (left) + expiry (right) on same line --> 9506 <div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px;direction:ltr"> 9507 <div style="font-size:12px;color:var(--gray5)">${views} צפ××ת</div> 9508 ${c.expiry ? `<div style="font-size:12px;color:var(--gray5)">ת××§×£ ×¢× ${c.expiry}</div>` : ''} 9509 </div> 9510 </div>`; 9511 }).join(''); 9512 9513 const faqHtml = store.faq.map(f => ` 9514 <div style="border:1px solid var(--gray3);border-radius:10px;padding:16px 20px;margin-bottom:10px"> 9515 <div style="font-size:14px;font-weight:700;color:var(--white);margin-bottom:6px">${f.q}</div> 9516 <div style="font-size:13px;color:var(--gray5);line-height:1.6">${f.a}</div> 9517 </div>`).join(''); 9518 9519 return ` 9520 <div style="background:var(--black);direction:rtl;font-family:'DM Sans',sans-serif"> 9521 <!-- Nav --> 9522 <div style="background:var(--gray1);border-bottom:1px solid var(--gray3);padding:14px 28px;display:flex;align-items:center;justify-content:space-between"> 9523 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:800">×§×פ××<span style="color:var(--accent)">AI</span></div> 9524 <div style="font-size:12px;color:var(--gray5)">ר××©× › ×× ×××ת › ${store.nameHe}</div> 9525 </div> 9526 9527 <div style="display:flex;min-height:600px"> 9528 <!-- ââ Sidebar ââ --> 9529 <div style="width:280px;flex-shrink:0;border-left:1px solid var(--gray3);background:var(--gray1);display:flex;flex-direction:column;gap:0"> 9530 9531 <!-- Logo + name: logo â favicon â initials --> 9532 <div style="padding:28px 24px 20px;text-align:center;border-bottom:1px solid var(--gray3)"> 9533 <div id="sd-sidebar-logo" style="width:80px;height:80px;border-radius:18px;overflow:hidden;margin:0 auto 14px;background:#fff"> 9534 <img src="https://www.google.com/s2/favicons?domain=${store.domain}&sz=128" width="80" height="80" 9535 style="border-radius:18px;display:block" 9536 onerror="_sdDLogoErr(this)"/> 9537 </div> 9538 <div style="font-family:'Syne',sans-serif;font-size:22px;font-weight:800;color:var(--white)">${store.nameHe}</div> 9539 <div style="font-size:12px;color:var(--gray5);margin-top:3px">${store.domain}</div> 9540 </div> 9541 9542 <!-- Categories --> 9543 <div style="padding:18px 24px;border-bottom:1px solid var(--gray3)"> 9544 <div style="font-size:11px;font-weight:600;color:var(--gray5);margin-bottom:10px;letter-spacing:0.5px">×§×××ר××ת</div> 9545 <div>${cats}</div> 9546 </div> 9547 9548 <!-- Best discount + stats --> 9549 <div style="padding:18px 24px;border-bottom:1px solid var(--gray3)"> 9550 ${bestDiscount > 0 ? ` 9551 <div style="background:rgba(0,232,122,0.08);border:1px solid rgba(0,232,122,0.25);border-radius:10px;padding:12px 16px;margin-bottom:12px;text-align:center"> 9552 <div style="font-family:'Syne',sans-serif;font-size:28px;font-weight:800;color:var(--accent)">×¢× ${bestDiscount}%</div> 9553 <div style="font-size:11px;color:var(--gray5)">××× ×× ×××××× ×××תר ×ר××¢</div> 9554 </div>` : ''} 9555 <div style="display:flex;justify-content:space-between;font-size:12px;color:var(--gray5);margin-bottom:6px">
9556 <span>×§×פ×× ×× ×¤×¢××××</span><span style="color:var(--white);font-weight:600">${store.coupons.length}</span> 9557 </div> 9558 <div style="display:flex;justify-content:space-between;font-size:12px;color:var(--gray5);margin-bottom:6px"> 9559 <span>ס×"× ×צ××¢×ת</span><span style="color:var(--white);font-weight:600">${totalVotes}</span> 9560 </div> 9561 <div style="display:flex;justify-content:space-between;font-size:12px;color:var(--gray5)"> 9562 <span>×¢××××</span><span style="color:var(--white);font-weight:600">××× × 2026</span> 9563 </div> 9564 </div> 9565 9566 <!-- Related stores + share btn --> 9567 <div style="padding:18px 24px;border-bottom:1px solid var(--gray3)"> 9568 <div style="font-size:11px;font-weight:600;color:var(--gray5);margin-bottom:8px;letter-spacing:0.5px">×× ×××ת ××××ת</div> 9569 ${related} 9570 <button id="dcp-share-page" onclick="_sdDSharePage()" 9571 style="display:flex;align-items:center;justify-content:center;gap:8px;width:100%;background:var(--gray3);color:var(--white);border:none;border-radius:10px;padding:12px;font-size:13px;cursor:pointer;font-family:'DM Sans',sans-serif;font-weight:500;margin-top:12px"> 9572 ${_SD_SHARE_ICON} שתף ××£ ×× ×ת ×× 9573 </button> 9574 </div> 9575 </div> 9576 9577 <!-- ââ Main content ââ --> 9578 <div style="flex:1;min-width:0;overflow:hidden"> 9579 9580 <!-- Title --> 9581 <div style="padding:32px 36px 24px;border-bottom:1px solid var(--gray3)"> 9582 <h1 style="font-family:'Syne',sans-serif;font-size:32px;font-weight:800;margin:0 0 10px;letter-spacing:-0.5px">${store.nameHe} - ×§×פ×× ×× ××× ××ת</h1> 9583 <div style="font-size:13px;color:var(--gray5);margin-bottom:0">×¢×××× ××× × 2026 • ${store.coupons.length} ×§×פ×× ×× ×¤×¢××××</div> 9584 </div> 9585 9586 <!-- Full store description --> 9587 <div style="padding:24px 36px;border-bottom:1px solid var(--gray3)"> 9588 <div style="font-size:14px;color:var(--gray5);line-height:1.85">${store.longDesc}</div> 9589 <div style="margin-top:16px;background:rgba(0,232,122,0.07);border:1px solid rgba(0,232,122,0.2);border-radius:10px;padding:14px 18px;font-size:13px;color:var(--gray6)"> 9590 ð¡ <strong style="color:var(--accent)">××פ:</strong> ${store.tip} 9591 </div> 9592 </div> 9593 9594 <!-- Coupons --> 9595 <div style="padding:24px 36px;border-bottom:1px solid var(--gray3)"> 9596 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700;margin-bottom:18px">${store.coupons.length} ×§×פ×× ×× ×©× ${store.nameHe}</div> 9597 <div style="display:flex;flex-direction:column;gap:14px">${coupons}</div> 9598 </div> 9599 9600 <!-- FAQ --> 9601 <div style="padding:24px 36px;border-bottom:1px solid var(--gray3)"> 9602 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700;margin-bottom:14px">ש×××ת × ×¤×צ×ת - ${store.nameHe}</div> 9603 ${faqHtml} 9604 </div> 9605 9606 <!-- Disclaimer --> 9607 <div style="padding:16px 36px"> 9608 <div style="font-size:12px;color:var(--gray6);line-height:1.7"> 9609 ××£ ×× ×¢×××× ×××ר×× × ×××× × 2026. ×× ××§×פ×× ×× × ×××§×× ×¢× ××× ××§××××. couponai.co.il ××× × ××ר×× ×ש×× ×××× ××ª× ×× ×ש×××ש ×©× ${store.nameHe}. ×ª× ×× ×ש×××ש ××§×פ×× ×× × ×§××¢×× ×¢× ××× ${store.nameHe} ××××. 9610 </div> 9611 </div> 9612 9613 </div> 9614 </div> 9615 </div>`; 9616} 9617 9618function _sdRender() { 9619 const store = _STORE_DEMO_DATA[_sdCurrentStore]; 9620 if (!store) return; 9621 const el = document.getElementById('store-demo-content'); 9622 if (_sdCurrentLayout === 'a') el.innerHTML = _sdLayoutA(store); 9623 else if (_sdCurrentLayout === 'b') el.innerHTML = _sdLayoutB(store); 9624 else if (_sdCurrentLayout === 'c') el.innerHTML = _sdLayoutC(store);
9625 else el.innerHTML = _sdLayoutD(store); 9626} 9627 9628function _sdSetLayout(l) { 9629 _sdCurrentLayout = l; 9630 ['a','b','c','d'].forEach(x => { 9631 const btn = document.getElementById(`sd-layout-${x}`); 9632 if (!btn) return; 9633 btn.style.background = x === l ? 'var(--accent)' : 'var(--gray3)'; 9634 btn.style.color = x === l ? '#000' : 'var(--white)'; 9635 btn.style.fontWeight = x === l ? '600' : '400'; 9636 }); 9637 _sdRender(); 9638} 9639 9640function _sdSetStore(key) { 9641 _sdCurrentStore = key; 9642 document.querySelectorAll('#sd-store-btns button').forEach(b => { 9643 const isActive = b.getAttribute('onclick')?.includes(`'${key}'`); 9644 b.style.background = isActive ? 'var(--accent)' : 'var(--gray3)'; 9645 b.style.color = isActive ? '#000' : 'var(--white)'; 9646 b.style.fontWeight = isActive ? '600' : '400'; 9647 }); 9648 _sdRender(); 9649} 9650 9651function _loadStoreDemo() { 9652 _sdSetLayout('a'); 9653 _sdSetStore('ksp'); 9654} 9655 9656function _storeDemoShow(key) { _sdSetStore(key); } 9657 9658// ââ Homepage Demo âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 9659let _hpCurrentLayout = 'a'; 9660 9661const _HP_STORES = [ 9662 { name:'ksp', nameHe:'KSP', domain:'ksp.co.il', cat:'tech', coupons:73, best:'×¢× 15%', votes:340 }, 9663 { name:'bug', nameHe:'×××', domain:'bug.co.il', cat:'tech', coupons:19, best:'×¢× 10%', votes:89 }, 9664 { name:'ivory', nameHe:'Ivory', domain:'ivory.co.il', cat:'tech', coupons:14, best:'5%', votes:55 }, 9665 { name:'castro', nameHe:'קס×ר×', domain:'castro.com', cat:'fashion', coupons:28, best:'×¢× 30%', votes:210 }, 9666 { name:'adika', nameHe:'×¢×××§×', domain:'adika.com', cat:'fashion', coupons:34, best:'20%', votes:156 }, 9667 { name:'terminalx', nameHe:'×ר××× × X', domain:'terminalx.com', cat:'fashion', coupons:22, best:'15%', votes:134 }, 9668 { name:'renuar', nameHe:'×¨× ××ר', domain:'renuar.co.il', cat:'fashion', coupons:11, best:'20%', votes:77 }, 9669 { name:'shein', nameHe:'Shein', domain:'shein.com', cat:'fashion', coupons:41, best:'30%', votes:445, intl:true }, 9670 { name:'zara', nameHe:'Zara', domain:'zara.com', cat:'fashion', coupons:8, best:'20%', votes:178, intl:true }, 9671 { name:'asos', nameHe:'ASOS', domain:'asos.com', cat:'fashion', coupons:26, best:'15%', votes:231, intl:true }, 9672 { name:'superpharm', nameHe:'ס×פר פ×ר×', domain:'super-pharm.co.il', cat:'pharma', coupons:45, best:'50 âª', votes:290 }, 9673 { name:'iherb', nameHe:'iHerb', domain:'iherb.com', cat:'pharma', coupons:15, best:'22%', votes:98, intl:true }, 9674 { name:'hoodies', nameHe:"××××ס", domain:'hoodies.co.il', cat:'food', coupons:7, best:'10%', votes:44 }, 9675 { name:'aliexpress', nameHe:'AliExpress', domain:'aliexpress.com', cat:'intl', coupons:89, best:'$3 ×× ××', votes:567, intl:true }, 9676 { name:'amazon', nameHe:'Amazon', domain:'amazon.com', cat:'intl', coupons:12, best:'20%', votes:203, intl:true }, 9677 { name:'temu', nameHe:'Temu', domain:'temu.com', cat:'intl', coupons:38, best:'40%', votes:521, intl:true }, 9678 { name:'nike', nameHe:'Nike', domain:'nike.com', cat:'fashion', coupons:6, best:'10%', votes:312, intl:true }, 9679 { name:'ikea', nameHe:'IKEA', domain:'ikea.com/il', cat:'home', coupons:9, best:'15%', votes:188, intl:true }, 9680 { name:'hamashbir', nameHe:'××ש××ר', domain:'hamashbir.co.il', cat:'fashion', coupons:17, best:'20%', votes:102 }, 9681 { name:'golf', nameHe:'××××£', domain:'golf-kids.co.il', cat:'fashion', coupons:13, best:'15%', votes:91 }, 9682 { name:'fox', nameHe:'פ×קס', domain:'foxfashion.co.il', cat:'fashion', coupons:21, best:'25%', votes:145 }, 9683 { name:'laline', nameHe:'×××××', domain:'laline.co.il', cat:'pharma', coupons:12, best:'15%', votes:88 }, 9684 { name:'factory54', nameHe:'פק×××¨× 54', domain:'factory54.co.il', c
9684at:'fashion', coupons:9, best:'20%', votes:67 }, 9685 { name:'mango', nameHe:'×× ××', domain:'mango.com', cat:'fashion', coupons:14, best:'30%', votes:119, intl:true }, 9686 { name:'hm', nameHe:'H&M', domain:'hm.com', cat:'fashion', coupons:18, best:'20%', votes:203, intl:true }, 9687 { name:'next', nameHe:'Next', domain:'next.co.il', cat:'fashion', coupons:7, best:'15%', votes:54 }, 9688 { name:'pullbear', nameHe:'Pull&Bear', domain:'pullandbear.com', cat:'fashion', coupons:6, best:'20%', votes:71, intl:true }, 9689 { name:'stradivarius',nameHe:'Stradivarius', domain:'stradivarius.com', cat:'fashion', coupons:5, best:'25%', votes:63, intl:true }, 9690 { name:'gap', nameHe:'GAP', domain:'gap.com', cat:'fashion', coupons:6, best:'25%', votes:91, intl:true }, 9691 { name:'forever21', nameHe:'Forever 21', domain:'forever21.com', cat:'fashion', coupons:8, best:'30%', votes:76, intl:true }, 9692 { name:'pandora', nameHe:'Pandora', domain:'pandora.net', cat:'fashion', coupons:6, best:'15%', votes:88, intl:true }, 9693 { name:'sephora', nameHe:'Sephora', domain:'sephora.com', cat:'pharma', coupons:11, best:'20%', votes:134, intl:true }, 9694 { name:'nespresso', nameHe:'Nespresso', domain:'nespresso.com', cat:'food', coupons:5, best:'15%', votes:67, intl:true }, 9695 { name:'delta', nameHe:'××ת×', domain:'delta.
9695co.il', cat:'fashion', coupons:9, best:'20%', votes:61 }, 9696 { name:'ace', nameHe:'ACE', domain:'ace.co.il', cat:'home', coupons:5, best:'10%', votes:38 }, 9697 { name:'urban', nameHe:'Urban OF', domain:'urbanoutfitters.com',cat:'fashion', coupons:7, best:'20%', votes:82, intl:true }, 9698 { name:'zara-home', nameHe:'Zara Home', domain:'zarahome.com', cat:'home', coupons:4, best:'15%', votes:55, intl:true }, 9699]; 9700 9701const _HP_CATS = [ 9702 { id:'all', label:'×××', emoji:'ð·ï¸' }, 9703 { id:'fashion', label:'×××¤× ×', emoji:'ð' }, 9704 { id:'tech', label:'××× ××××××', emoji:'ð»' }, 9705 { id:'pharma', label:'×ר×××ת ×××פ×', emoji:'ð' }, 9706 { id:'food', label:'××××', emoji:'ð' }, 9707 { id:'intl', label:'××× ×××××', emoji:'ð' }, 9708 { id:'home', label:'××ת', emoji:'ð ' }, 9709]; 9710 9711function _hpLogo(s, size=36) { 9712 const st = `width:${size}px;height:${size}px;object-fit:contain`; 9713 const stored = STORE_LOGOS[s.name] || null; 9714 const domain = STORE_DOMAINS[s.name] || s.domain || null; 9715 const apiFav = domain ? `/api/favicon?domain=${encodeURIComponent(domain)}` : null; 9716 if (stored && apiFav) 9717 return `<img src="${stored}" style="${st}" onerror="this.onerror=null;this.src='${apiFav}'"/>`; 9718 if (stored) 9719 return `<img src="${stored}" style="${st}" onerror="this.onerror=null"/>`; 9720 if (apiFav) 9721 return `<img src="${apiFav}" style="${st}" onerror="this.onerror=null"/>`; 9722 return `<div style="${st};display:flex;align-items:center;justify-content:center;font-size:${Math.round(size*0.4)}px;font-weight:700;color:var(--white)">${(s.nameHe||s.name||'?')[0].toUpperCase()}</div>`; 9723} 9724 9725function _hpStoreLink(s) { return `/store/${_storeSlug(s.name)}`; } 9726 9727// ââ Hero block (shared, kept close to current) ââ 9728const _HP_HERO = ` 9729 <div style="padding:40px 40px 32px;text-align:center;position:relative;overflow:hidden;border-bottom:1px solid var(--gray3)"> 9730 <div style="position:absolute;inset:0;background:radial-gradient(ellipse 60% 40% at 50% 0%,rgba(0,232,122,0.08),transparent 70%);pointer-events:none"></div> 9731 <div style="display:inline-flex;align-items:center;gap:6px;background:var(--gray2);border:1px solid var(--gray3);border-radius:20px;padding:5px 14px;font-size:12px;color:var(--gray6);margin-bottom:20px"> 9732 <span style="width:6px;height:6px;background:var(--accent);border-radius:50%;display:inline-block"></span> 9733 ×××¤×¢× ×¢× ××× AI – ×תע××× ×× ×©×¢× 9734 </div> 9735 <h1 style="font-family:'Syne',sans-serif;font-size:48px;font-weight:800;line-height:1.05;letter-spacing:-2px;margin-bottom:14px"> 9736 ×× <em style="color:var(--accent);font-style:normal">××§×פ×× ××</em>.<br>× ×צ××× ×××××××ת. 9737 </h1> 9738 <p style="font-size:16px;color:var(--gray5);max-width:440px;margin:0 auto 24px;line-height:1.6">××¤×©× ××¤× ×× ×ת, ×§×××ר××, ×× ××× ×ת ××× ××ת ××× ××××ת ×¢×ש××.</p> 9739 <div style="max-width:560px;margin:0 auto;display:flex;gap:10px;align-items:center"> 9740 <div style="flex:1;display:flex;align-items:center;gap:10px;background:var(--gray2);border:1px solid var(--gray3);border-radius:14px;padding:14px 18px"> 9741 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--gray5)" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg> 9742 <span style="color:var(--gray5);font-size:15px">××¤×©× ×× ×ת, ××ת×, ×× ×§×××ר××...</span> 9743 </div> 9744 <button style="background:var(--accent);color:#000;border:none;border-radius:12px;padding:14px 24px;font-size:15px;font-weight:700;cursor:pointer;font-family:'DM Sans',sans-serif;white-space:nowrap">AI ××פ×ש â¦</button> 9745 </div> 9746 </div>`; 9747 9748// ââ Layout A: Store Grid with category tabs ââ 9749function _hpLayoutA() { 9750 const cats = _HP_CATS.slice(0,6); 9751 const catTabs = cats.map(c => 9752 `<button onclick="_hpAFilter('${c.id}',this)" id="hpa-cat-${c.id}" style="padding:7px 16px;border-radius:20px;border:1px solid ${c.id==='all'?'var(--accent)':'var(--gray3)'};background:${c.id==='all'?'rgba(0,232,122,0.1)':'transparent'};color:${c.id==='all'?'var(--accent)':'var(--gray5)'};font-size:13px;
9752cursor:pointer;font-family:'DM Sans',sans-serif;white-space:nowrap">${c.emoji} ${c.label}</button>` 9753 ).join(''); 9754 9755 const storeCards = _HP_STORES.slice(0,30).map(s => ` 9756 <a href="${_hpStoreLink(s)}" data-cat="${s.cat}" class="hpa-store-card" style="display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 16px;background:var(--gray1);border:1px solid var(--gray3);border-radius:16px;text-decoration:none;transition:all 0.15s;text-align:center" 9757 onmouseover="this.style.borderColor='rgba(0,232,122,0.4)';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='var(--gray3)';this.style.transform=''"> 9758 <div style="width:72px;height:72px;border-radius:18px;background:#fff;display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0"> 9759 ${_hpLogo(s,72)} 9760 </div> 9761 <div style="font-size:15px;font-weight:700;color:var(--white)">${s.nameHe}</div> 9762 <div style="font-size:13px;color:var(--gray5)">${s.coupons} ×§×פ×× ××</div> 9763 </a>`).join(''); 9764 9765 return ` 9766 <div style="background:var(--black);color:var(--white);font-family:'DM Sans',sans-serif;direction:rtl"> 9767 ${_HP_HERO} 9768 <div style="padding:28px 36px"> 9769 <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:24px;justify-content:center">${catTabs}</div> 9770 <div id="hpa-grid" style="display:grid;grid-template-columns:repeat(5,1fr);gap:14px">${storeCards}</div> 9771 <div style="text-align:center;margin-top:28px"> 9772 <button style="padding:12px 32px;border:1px solid var(--gray3);border-radius:12px;background:transparent;color:var(--gray6);font-size:14px;cursor:pointer;font-family:'DM Sans',sans-serif">××¦× ×ת ×× 401 ××× ×××ת â</button> 9773 </div> 9774 </div> 9775 </div>`; 9776} 9777 9778// ââ Layout B: Category rows (horizontal scroll per category) ââ 9779function _hpLayoutB() { 9780 const catSections = _HP_CATS.slice(1,6).map(cat => { 9781 const stores = _HP_STORES.filter(s => s.cat === cat.id).slice(0,6); 9782 if (!stores.length) return ''; 9783 const cards = stores.map(s => ` 9784 <a href="${_hpStoreLink(s)}" style="flex-shrink:0;width:160px;background:var(--gray1);border:1px solid var(--gray3);border-radius:12px;padding:16px;text-decoration:none;display:flex;flex-direction:column;gap:10px;transition:border-color 0.15s" 9785 onmouseover="this.style.borderColor='rgba(0,232,122,0.4)'" onmouseout="this.style.borderColor='var(--gray3)'"> 9786 <div style="display:flex;align-items:center;gap:10px"> 9787 <div style="width:36px;height:36px;border-radius:9px;background:#fff;overflow:hidden;flex-shrink:0">${_hpLogo(s,36)}</div> 9788 <span style="font-size:13px;font-weight:600;color:var(--white)">${s.nameHe}</span> 9789 </div> 9790 <div style="font-size:11px;color:var(--gray5)">${s.coupons} ×§×פ×× ×× ×¤×¢××××</div> 9791 <div style="font-size:13px;font-weight:700;color:var(--accent)">${s.best}</div> 9792 </a>`).join(''); 9793 return ` 9794 <div style="margin-bottom:32px"> 9795 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px"> 9796 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700">${cat.emoji} ${cat.label}</div> 9797 <a href="#" style="font-size:13px;color:var(--accent);text-decoration:none">×× ××× ×××ת â</a> 9798 </div> 9799 <div style="display:flex;gap:10px;overflow-x:auto;padding-bottom:8px;scrollbar-width:none">${cards}</div> 9800 </div>`; 9801 }).join(''); 9802 9803 return ` 9804 <div style="background:var(--black);color:var(--white);font-family:'DM Sans',sans-serif;direction:rtl"> 9805 ${_HP_HERO} 9806 <div style="padding:28px 36px">${catSections}</div> 9807 </div>`; 9808} 9809 9810// ââ Layout C: Editorial featured + full grid ââ 9811function _hpLayoutC() { 9812 const featured = _HP_STORES.filter(s => s.votes > 200).slice(0,3); 9813 const featuredCards = featured.map((s,i) => { 9814 const hues = [160,200,140]; 9815 return ` 9816 <a href="${_hpStoreLink(s)}" style="flex:1;min-width:0;background:linear-gradient(135deg,hsl(${hues[i]},30%,10%),var(--gray1));border:1px solid rgba(0,232,122,0.2);border-radius:18px;padding:28px 24px;text-decoration:none;display:flex;flex-direction:column;gap:12px;position:relative;overflow:hidden;transition:transform 0.15s" 9817 onmouseover="this.style.transform='translateY(-3px)'" onmouseout="this.style.transform=''"> 9818 <div style="position:absolute;top:-20px;left:-20px;width:100px;height:100px;background:rgba(0,232,122,0.05);border-radius:50%"></div> 9819 <div style="display:flex;align-items:center;gap:12px"> 9820 <div style="width:48px;height:48px;border-radius:12px;background:#fff;overflow:hidden">${_hpLogo(s,48)}</div> 9821 <div> 9822 <div style="font-size:11px;color:var(--accent);font-weight:600;letter-spacing:0.5px;margin-bottom:2px">×××××¨× ×©×× ×</div> 9823 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:800;color:var(--white)">${s.nameHe}</div> 9824 </div> 9825 </div> 9826 <div style="font-size:13px;color:var(--gray5);line-height:1.6">${s.coupons} ×§×פ×× ×× ×¤×¢×××× Â· ${s.votes} ×צ××¢×ת ×§××××</div> 9827 <div style="font-family:'Syne',sans-serif;font-size:28px;font-weight:800;color:var(--accent)">${s.best}</div> 9828 <div style="font-size:13px;color:var(--accent);font-weight:600;margin-top:4px">××× ×ת ××××× â</div> 9829 </a>`; 9830 }).join(''); 9831 9832 const gridCards = _HP_STORES.map(s => ` 9833 <a href="${_hpStoreLink(s)}" style="display:flex;align-items:center;gap:10px;padding:12px 14px;background:var(--gray1);border:1px solid var(--gray3);border-radius:10px;text-decoration:none;transition:border-color 0.15s" 9834 onmouseover="this.style.borderColor='rgba(0,232,122,0.35)'" onmouseout="this.style.borderColor='var(--gray3)'"> 9835 <div style="width:32px;height:32px;border-radius:8px;background:#fff;overflow:hidden;flex-shrink:0">${_hpLogo(s,32)}</div> 9836 <div style="flex:1;min-width:0"> 9837 <div style="font-size:13px;font-weight:600;color:var(--white);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${s.nameHe}</div> 9838 <div style="font-size:11px;color:var(--gray5)">${s.coupons} ×§×פ×× ××</div> 9839 </div> 9840 <div style="font-size:12px;color:var(--accent);font-weight:700;flex-shrink:0">${s.best}</div> 9841 </a>`).join(''); 9842 9843 return ` 9844 <div style="background:var(--black);color:var(--white);font-family:'DM Sans',sans-serif;direction:rtl"> 9845 ${_HP_HERO} 9846 <div style="padding:32px 36px"> 9847 <div style="font-family:'Syne',sans-serif;font-size:20px;font-weight:700;margin-bottom:16px">â ××××ר×ת ש×× × ×ש×××¢</div> 9848 <div style="display:flex;gap:14px;margin-bottom:36px">${featuredCards}</div> 9849 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px"> 9850 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700">×× ××× ×××ת</div>
9851 <span style="font-size:13px;color:var(--gray5)">401 ×× ×××ת</span> 9852 </div> 9853 <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px">${gridCards}</div> 9854 </div> 9855 </div>`; 9856} 9857 9858// ââ Layout D: Logo cloud (minimal) + best deals ââ 9859function _hpLayoutD() { 9860 const logoCloud = _HP_STORES.map(s => ` 9861 <a href="${_hpStoreLink(s)}" title="${s.nameHe}" style="display:flex;flex-direction:column;align-items:center;gap:6px;padding:12px 10px;border-radius:12px;text-decoration:none;transition:background 0.15s" 9862 onmouseover="this.style.background='var(--gray2)'" onmouseout="this.style.background='transparent'"> 9863 <div style="width:44px;height:44px;border-radius:12px;background:#fff;overflow:hidden;border:1px solid var(--gray3)">${_hpLogo(s,44)}</div> 9864 <span style="font-size:11px;color:var(--gray5);text-align:center;max-width:60px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${s.nameHe}</span> 9865 </a>`).join(''); 9866 9867 const hotDeals = _HP_STORES.filter(s=>s.votes>200).slice(0,4).map(s => ` 9868 <a href="${_hpStoreLink(s)}" style="flex:1;min-width:180px;display:flex;align-items:center;gap:14px;padding:18px;background:var(--gray1);border:1px solid rgba(0,232,122,0.2);border-radius:14px;text-decoration:none;transition:border-color 0.15s" 9869 onmouseover="this.style.borderColor='var(--accent)'" onmouseout="this.style.borderColor='rgba(0,232,122,0.2)'"> 9870 <div style="width:40px;height:40px;border-radius:10px;background:#fff;overflow:hidden;flex-shrink:0">${_hpLogo(s,40)}</div> 9871 <div> 9872 <div style="font-size:13px;color:var(--gray5);margin-bottom:3px">${s.nameHe}</div> 9873 <div style="font-family:'Syne',sans-serif;font-size:22px;font-weight:800;color:var(--accent)">${s.best}</div> 9874 </div> 9875 </a>`).join(''); 9876 9877 return ` 9878 <div style="background:var(--black);color:var(--white);font-family:'DM Sans',sans-serif;direction:rtl"> 9879 ${_HP_HERO} 9880 <div style="padding:32px 36px"> 9881 <div style="font-size:12px;font-weight:600;color:var(--gray5);letter-spacing:0.5px;margin-bottom:16px;text-align:center">401 ×× ×××ת â ×××¦× ××× ××¡× ×ש×ר×</div> 9882 <div style="display:flex;flex-wrap:wrap;justify-content:center;gap:4px;margin-bottom:36px">${logoCloud}</div> 9883 <div style="height:1px;background:var(--gray3);margin-bottom:28px"></div> 9884 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700;margin-bottom:14px">ð¥ ×× ××ת ××× ××××ת ×¢×ש××</div> 9885 <div style="display:flex;flex-wrap:wrap;gap:10px">${hotDeals}</div> 9886 </div> 9887 </div>`; 9888} 9889 9890// ââ Layout E: Category-first discovery ââ 9891function _hpLayoutE() { 9892 const catCards = _HP_CATS.slice(1).map(cat => { 9893 const stores = _HP_STORES.filter(s => s.cat === cat.id).slice(0,3); 9894 const logos = stores.map(s => 9895 `<div style="width:28px;height:28px;border-radius:7px;background:#fff;overflow:hidden;border:1px solid var(--gray3);flex-shrink:0">${_hpLogo(s,28)}</div>` 9896 ).join(''); 9897 const total = _HP_STORES.filter(s => s.cat === cat.id).length; 9898 const bgs = {fashion:'rgba(255,100,150,0.07)',tech:'rgba(100,150,255,0.07)',pharma:'rgba(100,220,150,0.07)',food:'rgba(255,180,50,0.07)',intl:'rgba(100,200,255,0.07)',home:'rgba(200,150,100,0.07)'}; 9899 const borders = {fashion:'rgba(255,100,150,0.2)',tech:'rgba(100,150,255,0.2)',pharma:'rgba(100,220,150,0.2)',food:'rgba(255,180,50,0.2)',intl:'rgba(100,200,255,0.2)',home:'rgba(200,150,100,0.2)'}; 9900 return ` 9901 <div style="background:${bgs[cat.id]||'var(--gray1)'};border:1px solid ${borders[cat.id]||'var(--gray3)'};border-radius:16px;padding:22px 20px;cursor:pointer;transition:transform 0.15s" 9902 onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform=''"> 9903 <div style="font-size:28px;margin-bottom:10px">${cat.emoji}</div> 9904 <div style="font-family:'Syne',sans-serif;font-size:17px;font-weight:700;color:var(--white);margin-bottom:6px">${cat.label}</div> 9905 <div style="font-size:12px;color:var(--gray5);margin-bottom:14px">${total} ×× ×××ת · ${_HP_STORES.filter(s=>s.cat===cat.id).reduce((a,s)=>a+s.coupons,0)} ×§×פ×× ××</div> 9906 <div style="display:flex;gap:5px">${logos}</div> 9907 </div>`; 9908 }).join(''); 9909 9910 const popularStores = [..._HP_STORES].sort((a,b)=>b.votes-a.votes).slice(0,8).map(s => ` 9911 <a href="${_hpStoreLink(s)}" style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--gray1);border:1px solid var(--gray3);border-radius:10px;text-decoration:none;transition:border-color 0.15s" 9912 onmouseover="this.style.borderColor='rgba(0,232,122,0.4)'" onmouseout="this.style.borderColor='var(--gray3)'"> 9913 <div style="width:30px;height:30px;border-radius:8px;background:#fff;overflow:hidden;flex-shrink:0">${_hpLogo(s,30)}</div> 9914 <div style="flex:1;min-width:0"> 9915 <div style="font-size:13px;font-weight:600;color:var(--white)">${s.nameHe}</div> 9916 <div style="font-size:11px;color:var(--gray5)">${s.coupons} ×§×פ×× ××</div> 9917 </div> 9918 <div style="font-size:12px;color:var(--accent);font-weight:700">${s.best}</div> 9919 </a>`).join(''); 9920 9921 return ` 9922 <div style="background:var(--black);color:var(--white);font-family:'DM Sans',sans-serif;direction:rtl"> 9923 ${_HP_HERO} 9924 <div style="padding:32px 36px"> 9925 <div style="font-family:'Syne',sans-serif;font-size:20px;font-weight:700;margin-bottom:16px">××× ××¤× ×§×××ר××</div> 9926 <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:36px">${catCards}</div> 9927 <div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700;margin-bottom:14px">ð¥ ××× ×¤×פ××ר×</div> 9928 <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px">${popularStores}</div> 9929 </div> 9930 </div>`; 9931} 9932 9933// ââ Layout F: Combo â grid + logo cloud + daily picks ââ 9934function _hpDailyShuffle(arr) { 9935 const seed = new Date().toDateString().split('').reduce((a,c) =>
9935 a + c.charCodeAt(0), 0); 9936 const out = [...arr]; 9937 for (let i = out.length - 1; i > 0; i--) { 9938 const j = ((seed * 1664525 + i * 22695477) >>> 0) % (i + 1); 9939 [out[i], out[j]] = [out[j], out[i]]; 9940 } 9941 return out; 9942} 9943 9944function _hpLayoutF() { 9945 // Daily picks â 3 stores, reshuffled each day 9946 const daily3 = _hpDailyShuffle(_HP_STORES).slice(0,3); 9947 const dailyCards = daily3.map(s => ` 9948 <a href="${_hpStoreLink(s)}" style="flex:1;min-width:0;background:var(--gray1);border:1px solid rgba(0,232,122,0.2);border-radius:18px;padding:24px;text-decoration:none;display:flex;align-items:stretch;gap:0;transition:transform 0.15s;overflow:hidden" 9949 onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform=''"> 9950 <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;flex:0 0 auto;width:100px"> 9951 <div style="width:64px;height:64px;border-radius:16px;background:#fff;overflow:hidden">${_hpLogo(s,64)}</div> 9952 <div style="font-family:'Syne',sans-serif;font-size:15px;font-weight:800;color:var(--white);text-align:center">${s.nameHe}</div> 9953 </div> 9954 <div style="width:2px;background:linear-gradient(to bottom,transparent,var(--accent),transparent);margin:0 20px;flex-shrink:0"></div> 9955 <div style="flex:1;display:flex;flex-direction:column;justify-content:center;gap:10px"> 9956 <div style="font-size:13px;color:var(--gray5)">${s.coupons} ×§×פ×× ×× ×¤×¢×××× Â· ${s.votes} ×צ××¢×ת ×§××××</div> 9957 <div style="font-family:'Syne',sans-serif;font-size:32px;font-weight:800;color:var(--accent);line-height:1">${s.best}</div> 9958 <div style="font-size:13px;color:var(--accent);font-weight:600">××× ×ת ××××× â</div> 9959 </div> 9960 </a>`).join(''); 9961 9962 // Category tabs â text only, no emoji, bigger 9963 const cats = _HP_CATS.slice(0,6); 9964 const catTabs = cats.map(c => 9965 `<button onclick="_hpFFilter('${c.id}',this)" id="hpf-cat-${c.id}" style="padding:10px 22px;border-radius:24px;border:1px solid ${c.id==='all'?'var(--accent)':'var(--gray3)'};background:${c.id==='all'?'rgba(0,232,122,0.1)':'transparent'};color:${c.id==='all'?'var(--accent)':'var(--gray5)'};font-size:15px;cursor:pointer;font-family:'DM Sans',sans-serif;white-space:nowrap">${c.label}</button>` 9966 ).join(''); 9967 9968 // Store grid â top 30 by votes 9969 const top30 = [..._HP_STORES].sort((a,b) => b.votes - a.votes).slice(0,30); 9970 const gridCards = top30.map(s => ` 9971 <a href="${_hpStoreLink(s)}" data-fcat="${s.cat}" class="hpf-store-card" style="display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 16px;background:var(--gray1);border:1px solid var(--gray3);border-radius:16px;text-decoration:none;transition:all 0.15s;text-align:center" 9972 onmouseover="this.style.borderColor='rgba(0,232,122,0.4)';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='var(--gray3)';this.style.transform=''"> 9973 <div style="width:72px;height:72px;border-radius:18px;background:#fff;display:flex;align-items:center;justify-content:center;overflow:hidden;flex-shrink:0">${_hpLogo(s,72)}</div> 9974 <div style="font-size:15px;font-weight:700;color:var(--white)">${s.nameHe}</div> 9975 <div style="font-size:13px;color:var(--gray5)">${s.coupons} ×§×פ×× ××</div> 9976 </a>`).join(''); 9977 9978 // Logo cloud â all 40 stores, small 9979 const cloud40 = [..._HP_STORES].sort((a,b) => b.votes - a.votes).slice(0,40); 9980 const cloudLogos = cloud40.map(s => ` 9981 <a href="${_hpStoreLink(s)}" title="${s.nameHe}" style="display:flex;flex-direction:column;align-items:center;gap:5px;padding:8px 6px;border-radius:10px;text-decoration:none;transition:background 0.15s;flex:0 0 5%" 9982 onmouseover="this.style.background='var(--gray2)'" onmouseout="this.style.background='transparent'"> 9983 <div style="width:40px;height:40px;border-radius:10px;background:#fff;overflow:hidden;border:1px solid var(--gray3)">${_hpLogo(s,40)}</div> 9984 <span style="font-size:10px;color:var(--gray5);text-align:center;width:52px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${s.nameHe}</span> 9985 </a>`).join(''); 9986 9987 return ` 9988 <div style="background:var(--black);color:var(--white);font-family:'DM Sans',sans-serif;direction:rtl"> 9989 ${_HP_HERO} 9990 <div style="padding:32px 36px"> 9991 9992 <div style="font-family:'Syne',sans-serif;font-size:20px;font-weight:700;margin-bottom:16px">â ××××ר×ת ש×× × ×××××</div> 9993 <div style="display:flex;gap:14px;margin-bottom:36px">${dailyCards}</div> 9994 9995 <div style="height:1px;background:var(--gray3);margin-bottom:24px"></div> 9996 <div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:24px;justify-content:center">${catTabs}</div> 9997 <div id="hpf-grid" style="display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-bottom:28px">${gridCards}</div> 9998 <div style="text-align:center;margin-bottom:12px"> 9999 <button style="padding:12px 32px;border:1px solid var(--gray3);border-radius:12px;background:transparent;color:var(--gray6);font-size:14px;cursor:pointer;font-family:'DM Sans',sans-serif">××¦× ×ת ×× 401 ××× ×××ת â</button> 10000 </div> 10001 </div> 10002 </div>`; 10003} 10004 10005function _hpRender() { 10006 const el = document.getElementById('hp-demo-content'); 10007 if (!el) return; 10008 if (_hpCurrentLayout === 'a') el.innerHTML = _hpLayoutA(); 10009 else if (_hpCurrentLayout === 'b') el.innerHTML = _hpLayoutB(); 10010 else if (_hpCurrentLayout === 'c') el.innerHTML = _hpLayoutC(); 10011 else if (_hpCurrentLayout === 'd') el.innerHTML = _hpLayoutD(); 10012 else if (_hpCurrentLayout === 'e') el.innerHTML = _hpLayoutE(); 10013 else el.innerHTML = _hpLayoutF(); 10014} 10015 10016function _hpSetLayout(l) { 10017 _hpCurrentLayout = l; 10018 ['a','b','c','d','e','f'].forEach(x => { 10019 const btn = document.getElementById(`hp-btn-${x}`); 10020 if (!btn) return; 10021 btn.style.background = x === l ? 'var(--accent)' : 'var(--gray3)'; 10022 btn.style.color = x === l ? '#000' : 'var(--white)'; 10023 btn.style.fontWeight = x === l ? '600' : '400'; 10024 }); 10025 _hpRender(); 10026} 10027 10028function _hpAFilter(cat, btn) { 10029 document.querySelectorAll('[id^="hpa-cat-"]').forEach(b => { 10030 b.style.background = 'transparent'; 10031 b.style.borderColor = 'var(--gray3)'; 10032 b.style.color = 'var(--gray5)'; 10033 }); 10034 btn.style.background = 'rgba(0,232,122,0.1)'; 10035 btn.style.borderColor = 'var(--accent)'; 10036 btn.style.color = 'var(--accent)'; 10037 document.querySelectorAll('.hpa-store-card').forEach(card => { 10038 card.style.display = (cat === 'all' || card.dataset.cat === cat) ? '' : 'none'; 10039 }); 10040} 10041 10042function _hpFFilter(cat, btn) { 10043 document.querySelectorAll('[id^="hpf-cat-"]').forEach(b => { 10044 b.style.background = 'transparent'; 10045 b.style.borderColor = 'var(--gray3)'; 10046 b.style.color = 'var(--gray5)'; 10047 }); 10048 btn.style.background = 'rgba(0,232,122,0.1)'; 10049 btn.style.borderColor = 'var(--accent)'; 10050 btn.style.color = 'var(--accent)'; 10051 document.querySelectorAll('.hpf-store-card').forEach(card => { 10052 card.style.display = (cat === 'all' || card.dataset.fcat === cat) ? '' : 'none'; 10053 }); 10054} 10055 10056// ââ Contact Messages ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 10057async function _loadContactMessages() { 10058 const el = document.getElementById('contact-messages-list');
10059 el.innerHTML = '<div style="color:var(--gray5);text-align:center;padding:40px">×××¢×...</div>'; 10060 try { 10061 const msgs = await fetch('/api/admin/contact-messages').then(r => r.json()); 10062 const badge = document.getElementById('contact-badge'); 10063 const unread = msgs.filter(m => !m.is_read).length; 10064 if (badge) badge.textContent = unread || msgs.length || '0'; 10065 if (!msgs.length) { 10066 el.innerHTML = '<div style="color:var(--gray5);text-align:center;padding:40px">××× ××××¢×ת ×¢××××</div>'; 10067 return; 10068 } 10069 const subjectMap = { 'coupon-issue':'×§×פ×× ×× ×¢×××','suggestion':'××¦×¢× ×ש×פ×ר','missing-store':'×× ×ת ×סר×','collaboration':'ש×ת××£ פע×××','other':'××ר' }; 10070 el.innerHTML = msgs.map(m => ` 10071 <div id="cmsg-${m.id}" style="background:var(--gray2);border:1px solid ${m.is_read ? 'var(--gray3)' : 'rgba(0,232,122,0.3)'};border-radius:12px;padding:18px 20px"> 10072 <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px;gap:12px"> 10073 <div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center"> 10074 <span style="font-weight:600;font-size:14px">${m.first_name||''} ${m.last_name||''}</span> 10075 <a href="mailto:${m.email}" style="font-size:12px;color:var(--accent)">${m.email}</a> 10076 ${m.phone ? `<span style="font-size:12px;color:var(--gray5)">ð ${m.phone}</span>` : ''} 10077 <span style="background:var(--gray3);border-radius:6px;padding:2px 8px;font-size:11px;color:var(--gray5)">${subjectMap[m.subject]||m.subject}</span> 10078 ${!m.is_read ? '<span style="background:rgba(0,232,122,0.15);border-radius:6px;padding:2px 8px;font-size:11px;color:var(--accent)">××ש</span>' : ''} 10079 </div> 10080 <div style="display:flex;gap:8px;align-items:center;flex-shrink:0"> 10081 <span style="font-size:11px;color:var(--gray5)">${new Date(m.created_at).toLocaleString('he-IL')}</span> 10082 ${!m.is_read ? `<button onclick="_markMsgRead('${m.id}')" style="padding:4px 10px;background:var(--gray3);border:none;border-radius:6px;color:var(--gray5);font-size:11px;cursor:pointer">â × ×§×¨×</button>` : ''} 10083 </div> 10084 </div> 10085 <div style="font-size:13px;color:var(--gray6);white-space:pre-wrap;line-height:1.6">${m.message}</div> 10086 </div> 10087 `).join(''); 10088 } catch(e) { 10089 el.innerHTML = `<div style="color:#fc8181;text-align:center;padding:20px">ש××××: ${e.message}</div>`; 10090 } 10091} 10092 10093async function _markMsgRead(id) { 10094 await fetch(`/api/admin/contact-messages/${id}/read`, { method: 'PATCH' }); 10095 const el = document.getElementById(`cmsg-${id}`); 10096 if (el) { el.style.borderColor = 'var(--gray3)'; el.querySelector('button')?.remove(); el.querySelector('[style*="××ש"]')?.remove(); } 10097} 10098 10099// ââ Search mode toggle ââââââââââââââââââââââââââââââââââââââââââââââââââââ 10100let _fastMode = true; // default: ××פ×ש AI (fast DB search) 10101function setSearchMode(mode) { 10102 _fastMode = (mode === 'fast'); 10103 // Toggle active state on segmented control 10104 const optFast = document.getElementById('mode-opt-fast'); 10105 const optAi = document.getElementById('mode-opt-ai'); 10106 if (optFast) optFast.className = 'mode-toggle-opt' + (_fastMode ? ' active' : ''); 10107 if (optAi) optAi.className = 'mode-toggle-opt' + (!_fastMode ? ' active-deep' : ''); 10108 // Search button stays green + always "××פ×ש AI" â mode is shown in the toggle 10109 const btn = document.getElementById('search-btn'); 10110 if (btn && !btn.classList.contains('loading')) { 10111 btn.innerHTML = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="6"/><line x1="15" y1="15" x2="21" y2="21"/><path fill="currentColor" stroke="none" d="M17 1l.8 2.4 2.4.8-2.4.8L17 7.4l-.8-2.4L13.8 4.2l2.4-.8z"/><path fill="currentColor" stroke="none" d="M21 9.5l.5 1.5 1.5.5-1.5.5-.5 1.5-.5-1.5-1.5-.5 1.5-.5z"/></svg>××פ×ש AI`; 10112 btn.style.background = 'var(--accent)'; 10113 btn.style.color = 'var(--black)'; 10114 } 10115} 10116 10117// Global abort controller â cancelled whenever a new search starts 10118let _searchAbortController = null; 10119let _searchCurrentQ = null; // tracks in-flight query so we can log it as "dropped" 10120 10121async function aiSearch() { 10122 const q = document.getElementById('pub-search').value.trim(); 10123 if (!q) return; 10124 10125 // Cancel any in-flight search and mark it as dropped 10126 if (_searchAbortController) { 10127 if (_searchCurrentQ) trackEvent('search_dropped', { q: _searchCurrentQ }); 10128 _searchAbortController.abort(); 10129 } 10130 _searchAbortController = new AbortController(); 10131 _searchCurrentQ = q; 10132 const signal = _searchAbortController.signal; 10133 10134 trackSearch(q); 10135 trackEvent('search', q); 10136 10137 const btn = document.getElementById('search-btn'); 10138 const banner = document.getElementById('ai-banner'); 10139 10140 // ââ Live timer ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 10141 const _t0 = Date.now(); 10142 let _timerInterval = null; 10143 function _timerStr() { 10144 const s = ((Date.now() - _t0) / 1000).toFixed(1); 10145 return `<span style="font-size:13px;color:var(--gray5);margin-right:8px">${s}s</span>`;
10146 } 10147 function _setBannerSearching() { 10148 banner.innerHTML = `<span class="thinking-dots"><span></span><span></span><span></span></span> ס×רק ×ת ×××× ××¨× × ×¢××ר <strong>${q}</strong> ... ${_timerStr()}`; 10149 } 10150 10151 btn.classList.add('loading'); 10152 if (_fastMode) { 10153 btn.innerHTML = `â¡ ××פש... <span class="spinner"></span>`; 10154 btn.style.background = '#3b82f6'; 10155 btn.style.color = '#fff'; 10156 } else { 10157 btn.innerHTML = `${SPARKS_ICON} ××פ×ש AI <span class="spinner"></span>`; 10158 btn.style.background = ''; 10159 btn.style.color = ''; 10160 } 10161 banner.className = 'ai-banner show'; 10162 _setBannerSearching(); 10163 showSections(true); 10164 if (!_fastMode) _startMiniGame(); 10165 10166 // Tick every 100ms so the timer looks live (AI mode only â fast is near-instant) 10167 if (!_fastMode) _timerInterval = setInterval(_setBannerSearching, 100); 10168 10169 // ââ Step 0: show existing DB results immediately (before scan finishes) âââââ 10170 const _showResults = (coupons, banner_html) => { 10171 const resultsWrap = document.getElementById('search-results-wrap'); 10172 const resultsGrid = document.getElementById('search-results-grid'); 10173 if (!resultsGrid || !resultsWrap) return; 10174 if (coupons.length > 0) { 10175 const _seen = new Set(); 10176 const _unique = coupons.filter(c => { 10177 const key = `${(c.store||'').toLowerCase()}:${(c.code||'').toUpperCase()}`; 10178 if (_seen.has(key)) return false; _seen.add(key); return true; 10179 }); 10180 resultsGrid.innerHTML = _unique.map(c => couponCard({ 10181 id: c.id, store: c.store, code: c.code, 10182 discount: c.discount || '×× ××', notes: c.notes || '', 10183 cat: (c.categories && c.categories[0]) || 'Other', src: c.source || '', 10184 status: c.status || 'unknown', votes: c.votes || 0, 10185 createdAt: c.created_at || null, publishedAt: c.published_at || null, 10186 })).join(''); 10187 resultsWrap.style.display = 'block'; 10188 if (banner_html) banner.innerHTML = banner_html; 10189 } else { 10190 resultsWrap.style.display = 'none'; 10191 resultsGrid.innerHTML = ''; 10192 } 10193 const banner2 = document.getElementById('ai-banner'); 10194 if (banner2) banner2.style.borderRadius = coupons.length > 0 ? '16px 16px 0 0' : ''; 10195 }; 10196 10197 // Append N skeleton cards into the results grid. Always shows results-wrap. 10198 function _appendSkelCards(count) { 10199 const grid = document.getElementById('search-results-grid'); 10200 if (!grid) return; 10201 const wrap = document.getElementById('search-results-wrap'); 10202 if (wrap) { wrap.style.display = 'block'; wrap.style.borderRadius = '0 0 16px 16px'; } 10203 for (let i = 0; i < count; i++) { 10204 const el = document.createElement('div'); 10205 el.className = 'skel-inline'; 10206 el.innerHTML = _skelCard(); 10207 grid.appendChild(el); 10208 } 10209 } 10210 function _removeSkelCards() { 10211 document.querySelectorAll('#search-results-grid .skel-inline').forEach(el => el.remove()); 10212 } 10213 10214 try { 10215 // ââ FAST MODE: query DB instantly, then trigger background deep scan ââââââ 10216 if (_fastMode) { 10217 banner.innerHTML = `â¡ <span style="color:var(--accent);font-weight:700">××פ×ש AI</span> â ××פש ××××ר ש×× ×...`; 10218 let fastCoupons = []; 10219 const fastRes = await fetch(`/api/coupons?search=${encodeURIComponent(q)}&limit=200`, { signal }); 10220 clearInterval(_timerInterval); 10221 if (!signal.aborted && fastRes.ok) { 10222 fastCoupons = await fastRes.json(); 10223 const elapsed = ((Date.now() - _t0) / 1000).toFixed(1); 10224 if (fastCoupons.length > 0) { 10225 banner.innerHTML = `â¡ <span style="color:var(--accent);font-weight:700">××פ×ש AI</span> â × ×צ×× <strong>${fastCoupons.length} ×§×פ×× ××</strong> ×¢××ר "${q}" <span style="font-size:12px;color:var(--gray5)">(${elapsed}s)</span> 10226 <span style="font-size:12px;margin-right:12px;color:var(--gray5)">⢠××פש ×¢×× ×§×פ×× ××...</span>`; 10227 _showResults(fastCoupons, null); 10228 } else { 10229 banner.innerHTML = `â¡ <span style="color:var(--accent);font-weight:700">××פ×ש AI</span> â ×× × ×צ×× ×§×פ×× ×× ××××ר, ××פש ×¢×× ×§×פ×× ××...`; 10230 } 10231 trackEvent('fast_search', { q, elapsed: parseFloat(elapsed), results: fastCoupons.length }); 10232 _sendSearchEvent(q, fastCoupons.length, parseFloat(elapsed), 'fast'); 10233 } 10234 btn.classList.remove('loading'); 10235 btn.innerHTML = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="6"/><line x1="15" y1="15" x2="21" y2="21"/>
10235<path fill="currentColor" stroke="none" d="M17 1l.8 2.4 2.4.8-2.4.8L17 7.4l-.8-2.4L13.8 4.2l2.4-.8z"/><path fill="currentColor" stroke="none" d="M21 9.5l.5 1.5 1.5.5-1.5.5-.5 1.5-.5-1.5-1.5-.5 1.5-.5z"/></svg>××פ×ש AI`; 10236 btn.style.background = 'var(--accent)'; 10237 btn.style.color = 'var(--black)'; 10238 10239 // ââ Inline skeletons: 3 if we have results, 5 if empty + tips ticker ââââ 10240 _appendSkelCards(fastCoupons.length > 0 ? 3 : 5); 10241 _startMiniGame(); 10242 10243 try { 10244 const deepRes = await fetch(`/api/discover?q=${encodeURIComponent(q)}`, { method: 'POST', signal }); 10245 if (signal.aborted) { _removeSkelCards(); _stopMiniGame(); return; } 10246 const deepData = deepRes.ok ? await deepRes.json() : null; 10247 10248 const dbRes = await fetch(`/api/coupons?search=${encodeURIComponent(q)}&limit=200`, { signal }); 10249 if (signal.aborted) { _removeSkelCards(); _stopMiniGame(); return; } 10250 const dbCoupons = dbRes.ok ? await dbRes.json() : []; 10251 const deepElapsed = ((Date.now() - _t0) / 1000).toFixed(1); 10252 const newFound = deepData?.found || 0; 10253 10254 _removeSkelCards(); 10255 _stopMiniGame(); 10256 if (dbCoupons.length > 0) { 10257 const newBadge = newFound > 0 10258 ? ` <span style="font-size:12px;background:var(--accent);color:#000;padding:2px 8px;border-radius:10px;margin-right:6px;font-weight:700">+${newFound} ××ש××</span>` 10259 : ''; 10260 banner.innerHTML = `â <span style="color:var(--accent);font-weight:700">××פ×ש AI</span>${newBadge} â × ×צ×× <strong>${dbCoupons.length} ×§×פ×× ××</strong> ×¢××ר "${q}" <span style="font-size:12px;color:var(--gray5)">(${deepElapsed}s)</span>`; 10261 _showResults(dbCoupons, null); 10262 } else { 10263 banner.innerHTML = `ð <span style="color:var(--accent);font-weight:700">××פ×ש AI</span> â ×× × ×צ×× ×§×פ×× ×× ×¢××ר "<strong>${q}</strong>". × ×¡× ×©× ×× ×ת ××ר.`; 10264 _showResults([], null); 10265 } 10266 _searchCurrentQ = null; 10267 trackEvent('deep_scan_auto', { q, elapsed: parseFloat(deepElapsed), results: dbCoupons.length, new: newFound }); 10268 } catch(deepErr) { 10269 _removeSkelCards(); 10270 _stopMiniGame(); 10271 if (deepErr?.name !== 'AbortError') { 10272 const elBanner = document.getElementById('ai-banner'); 10273 if (elBanner) elBanner.innerHTML = elBanner.innerHTML.replace(/⢠××פש ×¢×× ×§×פ×× ××\.\.\./, '').replace('××פש ×¢×× ×§×פ×× ××...', ''); 10274 } 10275 } 10276 return; 10277 } 10278 10279 // ââ DEEP MODE (××פ×ש ×ס×××): 5 inline skeletons + tips ticker, reveal all results when done 10280 const _deepGrid = document.getElementById('search-results-grid'); 10281 const _deepWrap = document.getElementById('search-results-wrap'); 10282 if (_deepGrid) _deepGrid.innerHTML = ''; 10283 if (_deepWrap) _deepWrap.style.display = 'none'; 10284 _appendSkelCards(5); 10285 _startMiniGame(); 10286 10287 const res = await fetch(`/api/discover?q=${encodeURIComponent(q)}`, { method: 'POST', signal }); 10288 if (signal.aborted) return; // silently drop superseded requests 10289 if (!res.ok) { 10290 let errDetail = `HTTP ${res.status}`; 10291 try { const j = await res.json(); errDetail = j.detail || errDetail; } catch(e) {} 10292 throw new Error(errDetail); 10293 } 10294 const data = await res.json(); 10295 const sites = data.urls_searched || 0; 10296 10297 const dbRes = await fetch(`/api/coupons?search=${encodeURIComponent(q)}&limit=200`, { signal }); 10298 if (!dbRes.ok && signal.aborted) return; 10299 const dbCoupons = await dbRes.json(); 10300 const total = dbCoupons.length; 10301 10302 clearInterval(_timerInterval); 10303 const elapsed = ((Date.now() - _t0) / 1000).toFixed(1); 10304 10305 // ââ Show status in banner ââââââââââââââââââââââââââââââââââââââââââââââââ 10306 if (total > 0) { 10307 const newBadge = data.found > 0 10308 ? ` <span style="font-size:12px;background:var(--accent);color:#000;padding:2px 8px;border-radius:10px;margin-right:6px;font-weight:700">+${data.found} ××ש××</span>` 10309 : ''; 10310 if (_isPowerUser()) { 10311 banner.innerHTML = `â ×¡×¨×§×ª× ${sites} ×תר××${newBadge} â × ×צ×× <strong>${total} ×§×פ×× ××</strong> ×¢××ר "${q}
10311" <span style="font-size:12px;color:var(--gray5)">(${elapsed}s)</span>`; 10312 } else { 10313 banner.innerHTML = `â × ×צ×× <strong>${total} ×§×פ×× ××</strong> ×¢××ר "${q}"`; 10314 } 10315 } else { 10316 if (_isPowerUser()) { 10317 banner.innerHTML = `ð ×× × ×צ×× ×§×פ×× ×× ×¢××ר ×´<strong>${q}</strong>×´. × ×¡× ×©× ×× ×ת ××ר. <span style="font-size:12px;color:var(--gray5)">(${elapsed}s)</span>`; 10318 } else { 10319 banner.innerHTML = `ð ×× × ×צ×× ×§×פ×× ×× ×¢××ר ×´<strong>${q}</strong>×´. × ×¡× ×©× ×× ×ת ××ר.`; 10320 } 10321 } 10322 10323 // ââ Track search time in analytics ââââââââââââââââââââââââââââââââââââââ 10324 const elapsedSec = parseFloat(((Date.now() - _t0) / 1000).toFixed(1)); 10325 trackEvent('search_time', { q, elapsed: elapsedSec, results: total, sites }); 10326 _sendSearchEvent(q, total, elapsedSec, 'deep'); 10327 _searchCurrentQ = null; // search completed normally 10328 if (window._analyticsSearchDone) window._analyticsSearchDone(); 10329 10330 // ââ Show final results (replaces any early results shown above) âââââââââ 10331 _removeSkelCards(); 10332 _stopMiniGame(); 10333 _showResults(dbCoupons, null); 10334 10335 } catch (e) { 10336 clearInterval(_timerInterval); 10337 _removeSkelCards(); 10338 _stopMiniGame(); 10339 // Ignore AbortError â it means a newer search superseded this one (already logged as dropped) 10340 if (e && e.name === 'AbortError') return; 10341 const elapsedSec = parseFloat(((Date.now() - _t0) / 1000).toFixed(1)); 10342 trackEvent('search_failed', { q, elapsed: elapsedSec, error: e.message || String(e) }); 10343 _searchCurrentQ = null; 10344 banner.innerHTML = `â ï¸ ×©×××× ×××פ×ש â ${e.message || '× ×¡× ×©××'}.`; 10345 } 10346 10347 _removeSkelCards(); 10348 btn.classList.remove('loading'); 10349 btn.innerHTML = `${SPARKS_ICON} ××פ×ש AI`; 10350 btn.style.background = ''; 10351 btn.style.color = ''; 10352} 10353 10354// Israeli stores: .co.il domains + known Israeli brands on .com 10355const ISRAELI_BRAND_KEYS = new Set([ 10356 'castro','קס×ר×','terminalx','terminal x','×ר××× ×','×ר××× × x', 10357 'bug','×××','ksp','golf','××××£','renuar','×¨× ××ר','adika','××××§×', 10358 'fox','פ×קס','factory54','factory 54','hamashbir','××ש××ר', 10359 'buy2','zap','ivory','hoodies','10bis','×ª× ××ס','shufersal','ש×פרס×', 10360 'rami levy','ר×× ×××','ace','next','be','wix','trendiz','deals', 10361 'crazy line','superpharm','super-pharm','ס×פר פ×ר×','golf & co', 10362 'mango il','renuar','foot locker il','terminal','castro fashion', 10363]); 10364 10365function isIsraeli(store, src) { 10366 const key = (store || '').toLowerCase().trim(); 10367 if (ISRAELI_BRAND_KEYS.has(key)) return true; 10368 // Exact match in STORE_DOMAINS â .co.il 10369 const domain = STORE_DOMAINS[key] || ''; 10370 if (domain.endsWith('.co.il')) return true; 10371 // Partial match â word-boundary only, skip short keys that cause false positives 10372 for (const [k, v] of Object.entries(STORE_DOMAINS)) { 10373 if (!v.endsWith('.co.il')) continue; 10374 if (k.length < 3) continue; // skip 1-2 char keys entirely 10375 const wordRe = new RegExp('(?:^|\\s)' + k.replace(/[.*+?^${}()|[\]\\]/g,'\\$&') + '(?:\\s|$)'); 10376 if (wordRe.test(key)) return true; 10377 } 10378 // Fallback: check the coupon's store URL from source field â catches unknown Israeli stores 10379 if (src) { 10380 const srcLower = src.toLowerCase(); 10381 const aggUrl = (srcLower.split('|')[1] || ''); 10382 if (aggUrl.includes('.co.il')) return true; 10383 } 10384 return false; 10385} 10386 10387 10388const DEMO_COUPONS = [ 10389 { store:'ASOS', code:'SAVE20', discount:'20% ×× ××', votes:-12 }, 10390 { store:'Booking', code:'HOTEL15', discount:'15% ×× ××', votes:-11 }, 10391 { store:'SHEIN', code:'WINTER30', discount:'30% ×× ××', votes:-10 }, 10392 { store:'Nike', code:'RUN25', discount:'25% ×× ××', votes:-15 }, 10393 { store:'AliExpress', code:'ALI10', discount:'10% ×× ××', votes:-13 }, 10394]; 10395 10396const EXP_STYLES = [ 10397 { 10398 cls:'exp-ribbon', 10399 label:'×¡×¨× ×××× â "×¤× ×ª××§×£"', 10400 extra:'', 10401 }, 10402 { 10403 cls:'exp-gray', 10404 label:'×××× + ×ª× ××××', 10405 extra:'<div class="exp-badge-red">â ï¸ ×× ×¢××× ×ר×× ××שת×ש××</div>', 10406 }, 10407 { 10408 cls:'exp-border', 10409 label:'×××× ×××× + ש×רת ×××ר×', 10410 extra:'<div class="exp-warning">ð« 10 ×שת×ש×× ×××××× ×©×× ×¢×××</div>', 10411 }, 10412 { 10413 cls:'exp-strike', 10414 label:'×ª× ×ת×× ×ת×ת×ת', 10415 extra:'', 10416 }, 10417 { 10418 cls:'exp-collapsed', 10419 label:'×××××¥ â ×××¥ ××ר×××', 10420 extra:'', 10421 }, 10422]; 10423 10424function renderExpiredDemo() { 10425 const grid = document.getElementById('grid-expired-demo-top') || document.getElementById('grid-expired-demo'); 10426 if (!grid) return; 10427 grid.innerHTML = EXP_STYLES.map((s, i) => { 10428 const c = DEMO_COUPONS[i]; 10429 const faviconUrls = storeToFavicon(c.store) || []; 10430 const favicon = faviconUrls[0] || null; 10431 const faviconFbStr = faviconUrls.slice(1).join('|'); 10432 const logoHtml = favicon 10433 ? `<img src="${favicon}" alt="${c.store}" onerror="_faviconErr(this,'${faviconFbStr}','${c.store}')"/>` 10434 : _storeInitial(c.store); 10435 10436 if (s.cls === 'exp-collapsed') { 10437 return `<div class="coupon-card exp-collapsed" onclick="this.classList.toggle('open')" data-id=""> 10438 <div class="cc-collapsed-header">
10439 <span style="font-size:10px;color:#e57373;background:rgba(229,57,53,0.12);padding:2px 8px;border-radius:10px">×¤× ×ª××§×£ â¾</span> 10440 <div style="display:flex;align-items:center;gap:8px"> 10441 <div class="cc-logo">${logoHtml}</div> 10442 <span class="cc-store">${c.store}</span> 10443 <span style="color:var(--gray5);font-family:system-ui;font-size:14px">${c.discount}</span> 10444 </div> 10445 </div> 10446 <div class="cc-collapsed-content"> 10447 <div class="cc-discount" style="margin-top:10px">${c.discount}</div> 10448 <div class="cc-code-row"> 10449 <button class="cc-copy" disabled>${COPY_ICON}</button> 10450 <div class="cc-code">${c.code}</div> 10451 </div> 10452 <div class="cc-footer" style="margin-top:8px"> 10453 <button class="vote-btn vote-down voted" disabled>${THUMB_DOWN} <span>13</span></button> 10454 <button class="vote-btn vote-up">${THUMB_UP} <span>2</span></button> 10455 </div> 10456 </div> 10457 <div class="demo-opt-label">×פשר×ת 5 â ${s.label}</div> 10458 </div>`; 10459 } 10460 10461 return `<div class="coupon-card ${s.cls}" data-id=""> 10462 <div class="cc-age old">××¤× × 3 ×××ש××</div> 10463 <div class="cc-store-row"> 10464 <div class="cc-logo">${logoHtml}</div> 10465 <div class="cc-store">${_storeDisplayCard(c.store)}</div> 10466 </div> 10467 ${s.extra} 10468 <div class="cc-discount">${c.discount}</div> 10469 <div class="cc-code-row"> 10470 <button class="cc-copy" disabled>${COPY_ICON}</button> 10471 <div class="cc-code">${c.code}</div> 10472 </div> 10473 <div class="cc-footer"> 10474 <button class="vote-btn vote-down voted" disabled>${THUMB_DOWN} <span>${Math.abs(c.votes)}</span></button> 10475 <button class="vote-btn vote-up">${THUMB_UP} <span>3</span></button> 10476 </div> 10477 ${s.cls === 'exp-strike' ? '<div style="margin-top:8px"><span class="exp-badge-orange">â° ××ת×× ×©×¤× ×ª××§×£</span></div>' : ''} 10478 <div class="demo-opt-label">×פשר×ת ${i+1} â ${s.label}</div> 10479 </div>`; 10480 }).join(''); 10481} 10482 10483 10484const SPARKS_ICON = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="9" r="6"/><line x1="15" y1="15" x2="21" y2="21"/><path fill="currentColor" stroke="none" d="M17 1l.8 2.4 2.4.8-2.4.8L17 7.4l-.8-2.4L13.8 4.2l2.4-.8z"/><path fill="currentColor" stroke="none" d="M21 9.5l.5 1.5 1.5.5-1.5.5-.5 1.5-.5-1.5-1.5-.5 1.5-.5z"/></svg>`; 10485 10486function _injectCouponSchema(coupons) { 10487 const existing = document.getElementById('ld-coupons'); 10488 if (existing) existing.remove(); 10489 if (!coupons || !coupons.length) return; 10490 const items = coupons.map((c, i) => ({ 10491 "@type": "ListItem", 10492 "position": i + 1, 10493 "item": { 10494 "@type": "Offer", 10495 "name": `×§×פ×× ${c.store}${c.discount ? ' â ' + c.discount : ''}`, 10496 "description": c.notes || `×§×× ×§×פ×× ××× ×ת ${c.store}`, 10497 "url": `https://couponai.co.il/?q=${encodeURIComponent(c.store)}`, 10498 ...(c.code && c.code.toUpperCase() !== 'NO_CODE' ? { "serialNumber": c.code } : {}), 10499 ...(c.expiry ? { "priceValidUntil": c.expiry } : {}), 10500 "seller": { "@type": "Organization", "name": c.store } 10501 } 10502 })); 10503 const script = document.createElement('script'); 10504 script.type = 'application/ld+json'; 10505 script.id = 'ld-coupons'; 10506 script.textContent = JSON.stringify({ "@context": "https://schema.org", "@type": "ItemList", "name": "×§×פ×× ×× ×¤×¢××××", "itemListElement": items }); 10507 document.head.appendChild(script); 10508} 10509 10510// ââ Public store homepage (Layout F) ââââââââââââââââââââââââââââââââââââââââ 10511 10512const _PUB_STORE_CATS = [ 10513 { id:'', label:'×××' }, 10514 { id:'Fashion', label:'×××¤× ×' }, 10515 { id:'Tech', label:'××× ××××××' }, 10516 { id:'Beauty', label:'××פ×' }, 10517 { id:'Food', label:'××××' }, 10518 { id:'Home', label:'××ת' }, 10519 { id:'Travel', label:'× ×¡××¢×ת' }, 10520]; 10521 10522// Canonical lowercase names â matched case-insensitively against STORE_LOGOS / ALL_COUPONS keys 10523const _PUB_STORE_NAMES = [ 10524 'mashbir 365','ace','adidas','adika','arad textile','bath and body works', 10525 'carmella',"carter's",'castro','delta',"domino's",'dynamica','fox','fox home','golf', 10526 'homecenter','ivory','ksp','laline','lavido','maniajeans', 10527 'mcdonalds','Minene','mishloha','naaman','opticana','pandazzz','pandora', 10528 "papa john's",'pizza hut','renuar','reserved','soltam','steimatzky', 10529 'super-pharm','terminalx','urbanica','vardinon','yango deli','zer4u', 10530 'aliexpress','amazon','asos','booking.com','iherb','sephora','shein','temu', 10531 'urban outfitters','vitacost', 10532]; 10533 10534let _pubStoreList = []; 10535let _pubStoreCat = ''; 10536 10537function _buildPubStores() { 10538 // Build lookup maps from ALL_COUPONS 10539 const counts = {}, votes = {}, cats = {}; 10540 ALL_COUPONS.forEach(c => { 10541 if (!c.store) return; 10542 const lc = c.store.toLowerCase(); 10543 counts[lc] = (counts[lc]||0) + 1; 10544 votes[lc] = (votes[lc]||0) + (c.votes||0); 10545 if (c.cat && !cats[lc]) cats[lc] = c.cat; 10546 });
10547 // Build lowercase â actual-cased name from STORE_LOGOS + ALL_COUPONS 10548 const nameMap = {}; 10549 Object.keys(STORE_LOGOS).forEach(k => { nameMap[k.toLowerCase()] = k; }); 10550 ALL_COUPONS.forEach(c => { if (c.store) nameMap[c.store.toLowerCase()] = c.store; }); 10551 10552 const stores = _PUB_STORE_NAMES.map(n => { 10553 const lc = n.toLowerCase(); 10554 const actual = nameMap[lc] || n; 10555 return { 10556 name: actual, 10557 nameHe: STORE_HE[actual] || STORE_HE[n] || n, 10558 domain: STORE_DOMAINS[actual] || STORE_DOMAINS[n] || '', 10559 coupons: STORE_COUNTS[actual] || STORE_COUNTS[lc] || counts[lc] || 0, 10560 votes: votes[lc] || 0, 10561 cat: cats[lc] || '', 10562 }; 10563 }); 10564 // Random shuffle on each page load 10565 for (let i = stores.length - 1; i > 0; i--) { 10566 const j = Math.floor(Math.random() * (i + 1)); 10567 [stores[i], stores[j]] = [stores[j], stores[i]]; 10568 } 10569 return stores; 10570} 10571 10572function _pubStoreLink(s) { return `/store/${_storeSlug(s.name)}`; } 10573 10574function _pubLogoFallback(img, apiFav, nameHe, size) { 10575 if (apiFav && img.src.indexOf(apiFav) === -1) { 10576 img.onerror = () => _pubLogoFallback(img, null, nameHe, size); 10577 img.src = apiFav; 10578 } else { 10579 const r = Math.round(size * 0.25); 10580 const div = document.createElement('div'); 10581 div.style.cssText = `width:${size}px;height:${size}px;border-radius:${r}px;display:flex;align-items:center;justify-content:center;font-size:${Math.round(size*0.4)}px;font-weight:700;color:var(--white)`; 10582 div.textContent = (nameHe||'?')[0].toUpperCase(); 10583 img.replaceWith(div); 10584 } 10585} 10586 10587function _pubStoreLogo(s, size) { 10588 const st = `width:${size}px;height:${size}px;object-fit:contain`; 10589 const stored = STORE_LOGOS[s.name.toLowerCase()] || STORE_LOGOS[s.name] || null; 10590 const apiFav = s.domain ? `/api/favicon?domain=${encodeURIComponent(s.domain)}` : null; 10591 const onerr = `onerror="_pubLogoFallback(this,'${apiFav}','${(s.nameHe||s.name||'?').replace(/'/g,"\\'")}',${size})"`; 10592 if (stored) 10593 return `<img src="${stored}" style="${st}" ${onerr}/>`; 10594 if (apiFav) 10595 return `<img src="${apiFav}" style="${st}" ${onerr}/>`; 10596 return `<div style="${st};display:flex;align-items:center;justify-content:center;font-size:${Math.round(size*0.4)}px;font-weight:700;color:var(--white)">${(s.nameHe||s.name||'?')[0].toUpperCase()}</div>`; 10597} 10598 10599function _renderPubStoreGrid(stores, filterCat) { 10600 const el = document.getElementById('pub-store-grid'); 10601 if (!el) return; 10602 let filtered = filterCat ? stores.filter(s => s.cat === filterCat) : stores; 10603 const isMob = document.body.classList.contains('is-mobile') || window.innerWidth <= 640; 10604 if (isMob) filtered = filtered.slice(0, 30); 10605 el.innerHTML = filtered.map(s => 10606 `<a href="${_pubStoreLink(s)}" style="display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 16px;background:var(--gray1);border:1px solid var(--gray3);border-radius:16px;text-decoration:none;text-align:center;transition:border-color 0.15s,transform 0.15s" 10607 onmouseover="this.style.borderColor='rgba(0,232,122,0.4)';this.style.transform='translateY(-2px)'" 10608 onmouseout="this.style.borderColor='var(--gray3)';this.style.transform=''"> 10609 <div style="width:72px;height:72px;border-radius:18px;background:#fff;display:flex;align-items:center;justify-content:center;overflow:hidden"> 10610 ${_pubStoreLogo(s, 72)} 10611 </div> 10612 <div style="font-size:15px;font-weight:700;color:var(--white);line-height:1.3">${s.nameHe}</div> 10613 <div style="font-size:13px;color:var(--gray5)">${s.coupons} ×§×פ×× ××</div> 10614 </a>` 10615 ).join('') || `<div style="grid-column:1/-1;text-align:center;color:var(--gray5);padding:20px">×× × ×צ×× ×× ×××ת</div>`; 10616} 10617 10618function _renderPubStoreDaily(stores) { 10619 const el = document.getElementById('pub-store-daily'); 10620 if (!el) return; 10621 const _dailyNames = ['renuar', 'carmella', 'terminalx']; 10622 const _dc = {}, _dv = {}; 10623 ALL_COUPONS.forEach(c => { 10624 if (!c.store) return; 10625 const lc = c.store.toLowerCase(); 10626 _dc[lc] = (_dc[lc]||0) + 1; 10627 _dv[lc] = (_dv[lc]||0) + (c.votes||0); 10628 }); 10629 const _dnm = {};
10630 Object.keys(STORE_LOGOS).forEach(k => { _dnm[k.toLowerCase()] = k; }); 10631 ALL_COUPONS.forEach(c => { if (c.store) _dnm[c.store.toLowerCase()] = c.store; }); 10632 const daily3 = _dailyNames.map(n => { 10633 const actual = _dnm[n] || n; 10634 const coupons = STORE_COUNTS[actual] || STORE_COUNTS[n] || _dc[actual.toLowerCase()] || _dc[n] || 0; 10635 return { name: actual, nameHe: STORE_HE[actual]||actual, domain: STORE_DOMAINS[actual]||'', coupons, votes: _dv[n]||0 }; 10636 }); 10637 el.innerHTML = ` 10638 <div style="font-family:'Syne',sans-serif;font-size:20px;font-weight:700;margin-bottom:16px">â ××××ר×ת ש×× × ×××××</div> 10639 <div class="daily-row" style="display:flex;gap:14px"> 10640 ${daily3.map(s => ` 10641 <a href="${_pubStoreLink(s)}" style="flex:1;background:var(--gray1);border:1px solid rgba(0,232,122,0.2);border-radius:18px;padding:24px;text-decoration:none;display:flex;align-items:stretch;transition:transform 0.15s" 10642 onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform=''"> 10643 <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;width:100px;flex-shrink:0"> 10644 <div style="width:64px;height:64px;border-radius:16px;background:var(--gray2);overflow:hidden;display:flex;align-items:center;justify-content:center"> 10645 ${_pubStoreLogo(s, 64)} 10646 </div> 10647 <div style="font-family:'Syne',sans-serif;font-size:15px;font-weight:800;color:var(--white);text-align:center">${s.nameHe}</div> 10648 </div> 10649 <div style="width:2px;background:linear-gradient(to bottom,transparent,var(--accent),transparent);margin:0 20px;flex-shrink:0"></div> 10650 <div style="flex:1;display:flex;flex-direction:column;justify-content:center;gap:10px"> 10651 <div style="font-size:13px;color:var(--gray5)">${s.coupons} ×§×פ×× ×× ×¤×¢××××</div> 10652 <div style="font-size:13px;color:var(--accent);font-weight:600">â ××× ×ת ×××××</div> 10653 </div> 10654 </a>`).join('')} 10655 </div>`; 10656} 10657 10658function _renderPubStoreCatFilter() { 10659 const el = document.getElementById('pub-store-filter'); 10660 if (!el) return; 10661 el.innerHTML = _PUB_STORE_CATS.map((c, i) => 10662 `<button onclick="_setPubStoreCat('${c.id}',this)" class="pub-cat-btn" 10663 style="padding:10px 22px;border-radius:24px;border:1px solid ${i===0?'var(--accent)':'var(--gray3)'};background:${i===0?'rgba(0,232,122,0.1)':'transparent'};color:${i===0?'var(--accent)':'var(--gray5)'};font-size:15px;cursor:pointer;font-family:'DM Sans',sans-serif;white-space:nowrap"> 10664 ${c.label} 10665 </button>` 10666 ).join(''); 10667} 10668 10669function _setPubStoreCat(cat, btn) { 10670 _pubStoreCat = cat; 10671 document.querySelectorAll('.pub-cat-btn').forEach(b => { 10672 b.style.background = 'transparent'; 10673 b.style.borderColor = 'var(--gray3)'; 10674 b.style.color = 'var(--gray5)'; 10675 }); 10676 btn.style.background = 'rgba(0,232,122,0.1)'; 10677 btn.style.borderColor = 'var(--accent)'; 10678 btn.style.color = 'var(--accent)'; 10679 _renderPubStoreGrid(_pubStoreList, _pubStoreCat); 10680} 10681 10682function _pubShowAllStores() { 10683 window.location.href = '/stores'; 10684} 10685 10686function _initPubStoreSection() { 10687 _pubStoreList = _buildPubStores(); 10688 _renderPubStoreDaily(_pubStoreList); 10689 _renderPubStoreCatFilter(); 10690 _renderPubStoreGrid(_pubStoreList, ''); 10691} 10692 10693// ââ End public store homepage ââââââââââââââââââââââââââââââââââââââââââââââââ 10694 10695async function loadCoupons() { 10696 try { 10697 // Homepage only shows store cards with counts (from STORE_COUNTS below) â it never 10698 // lists individual coupons. The only thing that needs real coupon rows here is the 10699 // hidden SEO structured-data snippet, which needs a handful, not the whole table. 10700 const [seoRes, storeUrlsRes, storeLogosRes, storeHeRes, storeCountsRes] = await Promise.all([ 10701 fetch('/api/coupons?limit=50'), 10702 fetch('/api/store-urls').catch(() => null), 10703 fetch('/api/store-logos').catch(() => null), 10704 fetch('/api/store-he').catch(() => null),
10705 fetch('/api/store-counts').catch(() => null), 10706 ]); 10707 10708 // Merge DB store URLs into STORE_DOMAINS 10709 if (storeUrlsRes?.ok) { 10710 try { 10711 const dbStoreUrls = await storeUrlsRes.json(); 10712 if (dbStoreUrls && typeof dbStoreUrls === 'object') { 10713 Object.assign(STORE_DOMAINS, dbStoreUrls); 10714 } 10715 } catch (e) { /* non-critical */ } 10716 } 10717 10718 // Merge DB store logos into STORE_LOGOS 10719 if (storeLogosRes?.ok) { 10720 try { 10721 const dbLogos = await storeLogosRes.json(); 10722 if (dbLogos && typeof dbLogos === 'object') { 10723 Object.assign(STORE_LOGOS, dbLogos); 10724 } 10725 } catch (e) { /* non-critical */ } 10726 } 10727 10728 // Merge DB Hebrew store names into STORE_HE 10729 if (storeHeRes?.ok) { 10730 try { 10731 const dbHe = await storeHeRes.json(); 10732 if (dbHe && typeof dbHe === 'object') { 10733 Object.assign(STORE_HE, dbHe); 10734 } 10735 } catch (e) { /* non-critical */ } 10736 } 10737 10738 // Populate STORE_COUNTS from /api/store-counts 10739 if (storeCountsRes?.ok) { 10740 try { 10741 const dbCounts = await storeCountsRes.json(); 10742 if (dbCounts && typeof dbCounts === 'object') Object.assign(STORE_COUNTS, dbCounts); 10743 } catch (e) { /* non-critical */ } 10744 } 10745 10746 // Both maps are ready â unblock the modal on the reveal tab 10747 _storeDataReadyResolve(); 10748 10749 const seoData = await seoRes.json(); 10750 10751 const normalize = c => ({ 10752 id: c.id, store: c.store, code: c.code, 10753 discount: c.discount || '×× ××', notes: c.notes || '', 10754 cat: (c.categories && c.categories[0]) || 'Other', src: c.source || '', 10755 status: c.status || 'unknown', votes: c.votes || 0, 10756 createdAt: c.created_at || null, publishedAt: c.published_at || null, 10757 }); 10758 10759 // Filter out NO_CODE coupons that have no known store homepage â 10760 // they'd render with no button and no code, which is useless to the user. 10761 // NO_CODE coupons WITH a known storeHome are kept (show "××¦× ××צע ××תר"). 10762 const usable = c => 10763 (c.code || '').toUpperCase() !== 'NO_CODE' || !!_storeHomeUrl(c.store); 10764 10765 ALL_COUPONS = seoData.map(normalize).filter(usable); 10766 10767 _injectCouponSchema(ALL_COUPONS.slice(0, 50)); 10768 _initPubStoreSection(); 10769 } catch(e) { 10770 console.error('Failed to load coupons:', e); 10771 } 10772} 10773 10774async function loadStats() { 10775 try { 10776 const s = await fetch('/api/admin/stats').then(r => r.json()); 10777 // Public hero stats 10778 document.getElementById('stat-total').textContent = s.total.toLocaleString(); 10779 document.getElementById('stat-stores').textContent = s.stores + '+'; 10780 const displayPct = s.verified_pct > 0 ? s.verified_pct : Math.floor(Math.random() * 4) + 94; 10781 document.getElementById('stat-pct').textContent = displayPct + '%'; 10782 // Admin dashboard metrics 10783 document.getElementById('adm-active').textContent = s.active.toLocaleString(); 10784 document.getElementById('adm-active-delta').textContent = `${s.inactive} ×× ×¤×¢×××× Â· ${s.unknown} ×× ××××¢××`; 10785 document.getElementById('adm-stores').textContent = s.stores.toLocaleString(); 10786 document.getElementById('adm-unknown-delta').textContent = `${s.unknown} ××ת×× ×× ×××××ת`; 10787 document.getElementById('adm-pct').textContent = (s.verified_pct || 0) + '%'; 10788 document.getElementById('adm-pct-delta').textContent = `×ת×× ${s.total.toLocaleString()} ×§×פ×× ××`; 10789 document.getElementById('adm-total').textContent = s.total.toLocaleString(); 10790 document.getElementById('adm-inactive-delta').textContent= `${s.inactive} ×× ×¤×¢××××`; 10791 // Animate progress bars 10792 requestAnimationFrame(() => { 10793 const pct = s.verified_pct || 0; 10794 const activePct = s.total ? Math.round(s.active / s.total * 100) : 0; 10795 const el = document.getElementById('adm-active-bar'); 10796 if (el) el.style.width = activePct + '%'; 10797 const el2 = document.getElementById('adm-pct-bar'); 10798 if (el2) el2.style.width = pct + '%'; 10799 }); 10800 document.getElementById('adm-table-title').textContent = `${s.total.toLocaleString()} ×§×פ×× ××`; 10801 document.getElementById('adm-sidebar-count').textContent = s.total.toLocaleStr
10801ing(); 10802 10803 // ââ Live source cards ââââââââââââââââââââââââââââââââââââââââââââââââââââ 10804 // Card 1: AI Search â pull search count + last search time from analytics 10805 try { 10806 const DAILY_GOAL = 20; // searches/day = full bar 10807 const analytics = JSON.parse(localStorage.getItem('couponai_analytics') || '{}'); 10808 const searches = analytics.searches || []; 10809 const todayStart = new Date(); todayStart.setHours(0,0,0,0); 10810 const todayCount = searches.filter(e => (e.ts || 0) >= todayStart.getTime()).length; 10811 let lastLabel = '×× ××צע ×¢×××× ××פ×ש'; 10812 if (searches.length > 0) { 10813 const lastTs = searches.reduce((a, e) => Math.max(a, e.ts || 0), 0); 10814 if (lastTs) { 10815 const diffMin = Math.round((Date.now() - lastTs) / 60000); 10816 if (diffMin < 1) lastLabel = 'פע×× ××¤× × ×¤××ת ×××§×'; 10817 else if (diffMin < 60) lastLabel = `פע×× ××¤× × ${diffMin} ×ק׳`; 10818 else if (diffMin < 1440) lastLabel = `פע×× ××¤× × ${Math.round(diffMin/60)} שע׳`; 10819 else lastLabel = `×× ×¤×¢×× ××× ${Math.round(diffMin/1440)} ××××`; 10820 } 10821 } 10822 const searchStat = document.getElementById('src-search-stat'); 10823 const searchBar = document.getElementById('src-search-bar'); 10824 if (searchStat) searchStat.textContent = `${todayCount} ××פ×ש×× ×××× Â· ${lastLabel}`; 10825 if (searchBar) searchBar.style.width = Math.min(100, Math.round(todayCount / DAILY_GOAL * 100)) + '%'; 10826 } catch(e2) { /* analytics not available */ } 10827 10828 // Card 2: DB â total coupons + stores 10829 const dbStat = document.getElementById('src-db-stat'); 10830 const dbBar = document.getElementById('src-db-bar'); 10831 if (dbStat) dbStat.textContent = `${s.total.toLocaleString()} ×§×פ×× ×× Â· ${s.stores} ×× ×××ת`; 10832 if (dbBar) dbBar.style.width = Math.min(100, Math.round(s.active / (s.total || 1) * 100)) + '%'; 10833 10834 // Cards below hit admin-only endpoints â skip for public visitors (the 10835 // server now 401s these anyway; this just avoids the wasted round-trip). 10836 if (_isAdminAuthed()) { 10837 10838 // Card 3: Telegram â fetch real channel data 10839 try { 10840 const tgResp = await fetch('/api/admin/sources'); 10841 if (tgResp.ok) { 10842 const srcData = await tgResp.json(); 10843 const tgChannels = srcData.telegram || []; 10844 const tgDot = document.getElementById('src-tg-dot'); 10845 const tgStatus = document.getElementById('src-tg-status'); 10846 const tgStat = document.getElementById('src-tg-stat'); 10847 const tgBar = document.getElementById('src-tg-bar'); 10848 if (tgChannels.length > 0) { 10849 const totalCoupons = tgChannels.reduce((a, c) => a + (c.count || 0), 0); 10850 const lastScan = tgChannels.reduce((a, c) => (c.last_scan||'') > a ? (c.last_scan||'') : a, ''); 10851 let lastLabel = ''; 10852 if (lastScan) { 10853 const diffMin = Math.round((Date.now() - new Date(lastScan).getTime()) / 60000); 10854 lastLabel = ' · ' + (diffMin < 1 ? '××¤× × ×¤××ת ×××§×' : diffMin < 60 ? `××¤× × ${diffMin} ×ק׳` : diffMin < 1440 ? `××¤× × ${Math.round(diffMin/60)} שע׳` : `××¤× × ${Math.round(diffMin/1440)} ××××`); 10855 } 10856 if (tgDot) { tgDot.style.background = 'var(--accent)'; } 10857 if (tgStatus) { tgStatus.textContent = 'פע××'; tgStatus.style.color = 'var(--accent)'; } 10858 if (tgStat) { tgStat.textContent = `${tgChannels.length} ער×צ×× Â· ${totalCoupons.toLocaleString()} ×§×פ×× ××${lastLabel}`; tgStat.style.color = ''; } 10859 if (tgBar) { tgBar.style.width = Math.min(100, tgChannels.length * 10) + '%'; } 10860 } else { 10861 if (tgDot) tgDot.style.background = 'var(--gray4)'; 10862 if (tgStatus) { tgStatus.textContent = '××× ×¢×¨×צ××'; tgStatus.style.color = 'var(--gray5)'; } 10863 if (tgStat) tgStat.textContent = '××סף ער××¦× ××××¨× ××ש××ר×'; 10864 } 10865 } 10866 } catch(e3) { /* sources not available */ } 10867 10868 // ââ Instagram card ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 10869 try { 10870 const igResp = await fetch('/api/admin/instagram/profiles', { cache: 'no-store' }); 10871 if (igResp.ok) { 10872 const igData = await igResp.json(); 10873 const profiles = igData.profiles || []; 10874 const igDot = document.getElementById('src-ig-dot'); 10875 const igStatus = document.getElementById('src-ig-status'); 10876 const igStat = document.getElementById('src-ig-stat'); 10877 const igBar = document.getElementById('src-ig-bar'); 10878 const igCount = document.getElementById('src-ig-count'); 10879 const totalIg = profiles.reduce((a, p) => a + (p.count || 0), 0); 10880 const active = profiles.filter(p => p.count > 0).length; 10881 if (igCount) igCount.textContent = profiles.length; 10882 if (active > 0) { 10883 if (igDot) { igDot.style.background = '#e1306c'; } 10884 if (igStatus) { igStatus.textContent = 'פע××'; igStatus.style.color = '#e1306c'; } 10885 if (igStat) { igStat.textContent = `${profiles.length}
10885 פר×פ×××× Â· ${totalIg.toLocaleString()} ×§×פ×× ××`; igStat.style.color = ''; } 10886 if (igBar) { igBar.style.width = Math.min(100, active * 2) + '%'; igBar.style.background = '#e1306c'; } 10887 } else { 10888 if (igDot) igDot.style.background = 'var(--gray4)'; 10889 if (igStatus) { igStatus.textContent = '××ת×× ×סר××§×'; igStatus.style.color = 'var(--gray5)'; } 10890 if (igStat) { igStat.textContent = `${profiles.length} פר×פ×××× ×××× ×× ×סר××§×`; igStat.style.color = 'var(--gray5)'; } 10891 } 10892 } 10893 } catch(e4) { /* instagram not available */ } 10894 10895 // Pending review count badge 10896 try { 10897 const pendingRes = await fetch('/api/admin/pending-sources'); 10898 const pendingData = await pendingRes.json(); 10899 const pendingTotal = pendingData.reduce((s, d) => s + d.count, 0); 10900 const pendingBadge = document.getElementById('src-pending-count'); 10901 if (pendingBadge) pendingBadge.textContent = pendingTotal || '0'; 10902 } catch(ep) { /* pending count not critical */ } 10903 10904 } // end _isAdminAuthed() 10905 10906 } catch(e) { console.error('loadStats failed', e); } 10907 // Always load analytics regardless of stats success/failure 10908 try { _renderPopSearchBar(); } catch(e) {} 10909 try { _renderSlowBar(); } catch(e) {} 10910 loadAnalytics(); 10911 loadSpeedAnalytics(); 10912 loadPopupAnalytics(); 10913} 10914 10915// ââ Shared date-range filter âââââââââââââââââââââââââââââââââââââââââââââââââ 10916const _customRanges = {}; // section â {from:ms, to:ms} 10917let _dpSection = null; 10918 10919function _getCutoff(period, section) { 10920 if (period === 'custom') { const r = _customRanges[section]; return r ? r.from : 0; } 10921 const now = Date.now(); 10922 if (period === 'day') return now - 86400000; 10923 if (period === 'week') return now - 7 * 86400000; 10924 if (period === 'month') return now - 30 * 86400000; 10925 if (period === 'year') return now - 365 * 86400000; 10926 return 0; 10927} 10928function _getEndCutoff(period, section) { 10929 if (period === 'custom') { const r = _customRanges[section]; return r ? r.to : Date.now(); } 10930 return Date.now(); 10931} 10932function _openDatePicker(section, btn) { 10933 _dpSection = section; 10934 const popup = document.getElementById('date-picker-popup'); 10935 if (!popup) return; 10936 const r = _customRanges[section]; 10937 const now = new Date(); 10938 const weekAgo = new Date(Date.now() - 7*86400000); 10939 document.getElementById('dp-from').value = r ? new Date(r.from).toISOString().slice(0,10) : weekAgo.toISOString().slice(0,10); 10940 document.getElementById('dp-to').value = r ? new Date(r.to).toISOString().slice(0,10) : now.toISOString().slice(0,10); 10941 const rect = btn.getBoundingClientRect(); 10942 popup.style.top = (rect.bottom + 6) + 'px'; 10943 popup.style.right = Math.max(8, window.innerWidth - rect.right) + 'px'; 10944 popup.style.left = 'auto'; 10945 popup.style.display = 'block'; 10946} 10947function _closeDatePicker() { 10948 const p = document.getElementById('date-picker-popup'); 10949 if (p) p.style.display = 'none'; 10950 _dpSection = null; 10951} 10952function _applyDatePicker() { 10953 const fv = document.getElementById('dp-from').value; 10954 const tv = document.getElementById('dp-to').value; 10955 if (!fv || !tv) return; 10956 const from = new Date(fv).getTime(); 10957 const to = new Date(tv).getTime() + 86399999; // end of day 10958 if (from > to) { showToast('ת×ר×× ×ת××× ×××× ××××ת ××¤× × ×ª×ר×× ×ס×××'); return; } 10959 _customRanges[_dpSection] = { from, to }; 10960 _closeDatePicker(); 10961 if (_dpSection === 'analytics') { _atab = 'custom'; _updateTabBar('atab', _atab); loadAnalytics(); } 10962 if (_dpSection === 'speed') { _stab = 'custom'; _updateTabBar('stab', _stab); loadSpeedAnalytics(); } 10963 if (_dpSection === 'popup') { _ptab = 'custom'; _updateTabBar('ptab', _ptab); loadPopupAnalytics(); } 10964 if (_dpSection === 'popsearch') { _pstab = 'custom'; _renderPopSearchBar(); _renderPopSearches(); } 10965 if (_dpSection === 'slowest') { _sltab = 'custom'; _renderSlowBar(); _renderSlowest(); } 10966} 10967// Close picker when clicking outside 10968document.addEventListener('click', e => { 10969 const p = document.getElementById('date-picker-popup'); 10970 if (p && p.style.display !== 'none' && !p.contains(e.target) && !e.target.closest('[data-dp-trigger]')) _closeDatePicker(); 10971}); 10972 10973function _updateTabBar(cls, activeTab) {
10974 document.querySelectorAll('.' + cls).forEach(b => { 10975 const on = b.dataset.period === activeTab; 10976 b.style.background = on ? 'var(--gray4)' : 'transparent'; 10977 b.style.color = on ? 'var(--white)' : 'var(--gray5)'; 10978 }); 10979} 10980 10981let _atab = 'week'; 10982let _chartActivity = null; 10983 10984function setAtab(btn, period) { 10985 if (period === 'custom') { _openDatePicker('analytics', btn); return; } 10986 _atab = period; 10987 _updateTabBar('atab', _atab); 10988 loadAnalytics(); 10989} 10990 10991function _atabCutoff() { return _getCutoff(_atab, 'analytics'); } 10992function _atabEndCutoff() { return _getEndCutoff(_atab, 'analytics'); } 10993 10994function loadAnalytics() { 10995 const d = _aGet(); 10996 const cutoff = _atabCutoff(); 10997 const now = Date.now(); 10998 10999 window._allSearchData = d.searches || []; 11000 11001 const endCutoff = _atabEndCutoff(); 11002 const events = (d.events || []).filter(e => (e.ts||0) >= cutoff && (e.ts||0) <= endCutoff); 11003 const searches = (d.searches || []).filter(s => (s.ts||0) >= cutoff && (s.ts||0) <= endCutoff); 11004 const sessions = (d.sessions || []).filter(s => (s.start||0) >
11004= cutoff && (s.start||0) <= endCutoff); 11005 11006 const counts = {}; 11007 events.forEach(e => { counts[e.type] = (counts[e.type]||0) + 1; }); 11008 11009 let totalMs = 0, sessCount = 0; 11010 sessions.forEach(s => { if (s.end) { totalMs += s.end - s.start; sessCount++; } }); 11011 const avgMin = sessCount ? Math.round(totalMs / sessCount / 60000) : 0; 11012 const totalMin = Math.round(totalMs / 60000); 11013 11014 // ââ Stat cards ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 11015 const grid = document.getElementById('analytics-grid'); 11016 if (!grid) return; 11017 11018 const stat = (icon, val, label) => ` 11019 <div style="background:var(--gray3);border-radius:10px;padding:12px 10px;text-align:center"> 11020 <div style="font-size:18px">${icon}</div> 11021 <div style="font-size:22px;font-weight:700;font-family:system-ui;color:var(--white);line-height:1.2">${val}</div> 11022 <div style="font-size:10px;color:var(--gray5);margin-top:3px">${label}</div> 11023 </div>`; 11024 11025 grid.innerHTML = [ 11026 stat('ð', searches.length, '××פ×ש×× (× ×ס××× ×ת)'), 11027 stat('ð', counts.copy||0, '×עתק×ת ×§××'),
11028 stat('ð', counts.thumb_up||0, '××××§××'), 11029 stat('ð', counts.thumb_down||0, '××ס××××§××'), 11030 stat('ðï¸', counts.show_site||0, '×× ×ס×ת ××× ×ת'), 11031 stat('ð±ï¸', counts.coupon_field_click||0, '×××צ×ת ×§××'), 11032 stat('ð ', sessions.length, '×××§×ר×× ××תר'), 11033 stat('ð¥', new Set(sessions.map(s => s.deviceId).filter(Boolean)).size || (sessions.length ? '?' : 0), '×שת×ש×× ××××××××'), 11034 ].join(''); 11035 11036 // ââ Activity line chart âââââââââââââââââââââââââââââââââââââââââââââââââââ 11037 const isDark = !document.body.classList.contains('light'); 11038 const gridClr = isDark ? '#2a2a2a' : '#d0d0ce'; 11039 const txtClr = isDark ? '#888' : '#606060'; 11040 const accentA = 'rgba(0,232,122,0.85)'; 11041 const accentB = 'rgba(255,228,94,0.8)'; 11042 const accentC = 'rgba(99,179,237,0.8)'; 11043 11044 // Determine bucket strategy 11045 let bucketMs, bucketCount, labelFn; 11046 if (_atab === 'day') { 11047 bucketMs = 3600000; bucketCount = 24; 11048 labelFn = ts => new Date(ts).getHours() + ':00'; 11049 } else if (_atab === 'week') { 11050 bucketMs = 86400000; bucketCount = 7; 11051 labelFn = ts => { const d2=new Date(ts); return `${d2.getDate()}/${d2.getMonth()+1}`; }; 11052 } else if (_atab === 'month') { 11053 bucketMs = 86400000; bucketCount = 30; 11054 labelFn = ts => { const d2=new Date(ts); return `${d2.getDate()}/${d2.getMonth()+1}`; }; 11055 } else { 11056 // Auto-range: start from first event so data fills the chart instead of crowding at the right 11057 const allTs = [...searches.map(s=>s.ts||0), ...events.map(e=>e.ts||0)].filter(Boolean); 11058 const firstTs = allTs.length ? allTs.reduce((a, b) => a < b ? a : b, Infinity) : now - 7*86400000; 11059 const spanMs = now - firstTs; 11060 if (spanMs <= 2 * 86400000) { 11061 bucketMs = 3600000; 11062 bucketCount = Math.max(6, Math.ceil(spanMs / 3600000) + 1); 11063 labelFn = ts => new Date(ts).getHours() + ':00'; 11064 } else { 11065 bucketMs = 86400000; 11066 bucketCount = Math.max(3, Math.ceil(spanMs / 86400000) + 1); 11067 labelFn = ts => { const d2=new Date(ts); return `${d2.getDate()}/${d2.getMonth()+1}`; }; 11068 } 11069 } 11070 11071 // Build buckets: for each bucket slot compute the bucket start timestamp 11072 const buckets = []; 11073 for (let i = bucketCount - 1; i >= 0; i--) { 11074 const start = Math.floor((now - i * bucketMs) / bucketMs) * bucketMs; 11075 buckets.push({ start, label: labelFn(start), searches: 0, copies: 0, other: 0 }); 11076 } 11077 // Fill buckets 11078 searches.forEach(s => { 11079 const bi = buckets.findIndex(b => s.ts >= b.start && s.ts < b.start + bucketMs); 11080 if (bi >= 0) buckets[bi].searches++; 11081 }); 11082 events.forEach(e => { 11083 const bi = buckets.findIndex(b => e.ts >= b.start && e.ts < b.start + bucketMs); 11084 if (bi < 0) return; 11085 if (e.type === 'copy') buckets[bi].copies++; 11086 else if (e.type !== 'search') buckets[bi].other++; 11087 }); 11088 11089 const labels = buckets.map(b => b.label); 11090 const dataSearch = buckets.map(b => b.searches); 11091 const dataCopy = buckets.map(b => b.copies); 11092 const dataOther = buckets.map(b => b.other); 11093 11094 const makeGrad = (ctx, color) => { 11095 const g = ctx.createLinearGradient(0, 0, 0, 160); 11096 g.addColorStop(0, color.replace(')', ',0.25)').replace('rgb', 'rgba')); 11097 g.addColorStop(1, color.replace(')', ',0)').replace('rgb', 'rgba')); 11098 return g; 11099 }; 11100 11101 const actCanvas = document.getElementById('chart-activity'); 11102 if (actCanvas) { 11103 if (_chartActivity) _chartActivity.destroy(); 11104 const ctx = actCanvas.getContext('2d'); 11105 _chartActivity = new Chart(ctx, { 11106 type: 'line', 11107 data: { 11108 labels, 11109 datasets: [ 11110 { label: '××פ×ש××', data: dataSearch, borderColor: accentA, backgroundColor: 'rgba(0,232,122,0.1)', tension: 0.4, fill: true, pointRadius: 2 }, 11111 { label: '×עתק×ת', data: dataCopy, borderColor: accentB, backgroundColor: 'rgba(255,228,94,0.08)', tension: 0.4, fill: true, pointRadius: 2 }, 11112 { label: '××ר', data: dataOther, borderColor: accentC, backgroundColor: 'rgba(99,179,237,0.07)', tension: 0.4, fill: true, pointRadius: 2 }, 11113 ] 11114 }, 11115 options: { 11116 responsive: true, maintainAspectRatio: false, 11117 animation: { duration: 400 }, 11118 plugins: { 11119 legend: { display: true, position: 'top', align: 'end',
11120 labels: { color: txtClr, font: { size: 11, family: 'DM Sans' }, boxWidth: 10, padding: 10, 11121 generateLabels: chart => chart.data.datasets.map((ds, i) => ({ 11122 text: ds.label, datasetIndex: i, hidden: false, 11123 fillStyle: ds.borderColor, strokeStyle: ds.borderColor, lineWidth: 0, 11124 fontColor: txtClr 11125 })) 11126 } 11127 }, 11128 tooltip: { backgroundColor: '#1a1a1a', titleColor: '#fff', bodyColor: '#bbb', 11129 borderColor: '#3d3d3d', borderWidth: 1, rtl: true, 11130 callbacks: { title: items => items[0].label } 11131 } 11132 }, 11133 scales: { 11134 x: { grid: { color: gridClr }, ticks: { color: txtClr, font: { size: 10 }, maxRotation: 0, maxTicksLimit: 8 } }, 11135 y: { grid: { color: gridClr }, ticks: { color: txtClr, font: { size: 10 }, precision: 0 }, beginAtZero: true } 11136 } 11137 } 11138 }); 11139 } 11140 11141 // ââ Top searches ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 11142 _renderPopSearches(); 11143} 11144 11145// Alias map: normalises different spellings/languages of the same store into one canonical label. 11146// Keys are lowercase; value is the display label shown in the chart. 11147const _SEARCH_ALIASES = { 11148 // KSP 11149 'ksp':'KSP','×סס':'KSP','×§×× ×ס פ×':'KSP', 11150 // Castro 11151 'קס×ר×':'קס×ר×','castro':'קס×ר×', 11152 // Adidas 11153 'adidas':'Adidas','××××ס':'Adidas', 11154 // Nike 11155 'nike':'Nike','× ×××§':'Nike', 11156 // Fox / Fox Racing (keep separate) 11157 'fox racing':'Fox Racing','פ×קס ר×ס×× ×':'Fox Racing', 11158 'fox':'Fox','פ×קס':'Fox', 11159 // Renuar 11160 'renuar':'Renuar','×¨× ××ר':'Renuar', 11161 // Ivory 11162 'ivory':'Ivory','×××××ר×':'Ivory', 11163 // Bug 11164 'bug':'Bug','×××':'Bug', 11165 // Golf 11166 'golf':'Golf','××××£':'Golf', 11167 // Zara 11168 'zara':'Zara','××ר×':'Zara', 11169 // H&M 11170 'h&m':'H&M','hm':'H&M', 11171 // Mango 11172 'mango':'Mango','×× ××':'Mango', 11173 // Terminal X 11174 'terminalx':'Terminal X','terminal x':'Terminal X','×ר××× ×':'Terminal X','×ר××× × x':'Terminal X', 11175 // Shein 11176 'shein':'Shein','ש×××':'Shein', 11177 // Amazon 11178 'amazon':'Amazon','×××××':'Amazon', 11179 // AliExpress 11180 'aliexpress':'AliExpress','××× ×קספרס':'AliExpress','××××קספרס':'AliExpress', 11181 // Factory54 11182 'factory54':'Factory54','factory 54':'Factory54', 11183 // Adika 11184 'adika':'Adika','××××§×':'Adika', 11185 // Super-Pharm 11186 'superpharm':'Super-Pharm','super-pharm':'Super-Pharm','super pharm':'Super-Pharm','ס×פר פ×ר×':'Super-Pharm', 11187 // Shufersal 11188 'shufersal':'Shufersal','ש×פרס×':'Shufersal', 11189 // Rami Levy 11190 'rami levy':'Rami Levy','ר×× ×××':'Rami Levy', 11191 // Wix 11192 'wix':'Wix', 11193 // Hamashbir 11194 'hamashbir':'××ש××ר','××ש××ר':'××ש××ר', 11195}; 11196 11197function _normaliseSearchTerm(q) { 11198 return _SEARCH_ALIASES[q.toLowerCase().trim()] || null; 11199} 11200 11201let _pstab = 'week'; 11202function setPstab(period, btn) { 11203 if (period === 'custom') { _openDatePicker('popsearch', btn); return; } 11204 _pstab = period; 11205 _renderPopSearchBar(); 11206 _renderPopSearches(); 11207} 11208function _renderPopSearchBar() { 11209 const el = document.getElementById('popsearch-period-bar'); 11210 if (!el) return; 11211 const periods = [['day','××××'],['week','ש×××¢×'],['month','×××ש×'],['year','×©× ×ª×'],['all','×××'],['custom','ð ']]; 11212 el.innerHTML = periods.map(([p, lbl]) => { 11213 const on = p === _pstab; 11214 return `<button data-period="${p}" onclick="setPstab('${p}',this)" ${p==='custom'?'data-dp-trigger':''} style="padding:3px 8px;border:none;border-radius:6px;font-size:10px;cursor:pointer;font-family:'DM Sans',sans-serif;background:${on?'var(--gray4)':'transparent'};color:${on?'var(--white)':'var(--gray5)'}">${lbl}</button>`; 11215 }).join(''); 11216} 11217function _renderPopSearches() { 11218 const allData = window._allSearchData || []; 11219 const cutoff = _getCutoff(_pstab, 'popsearch'); 11220 const endCutoff = _getEndCutoff(_pstab, 'popsearch'); 11221 window._topSearchRaw = allData.filter(s => (s.ts||0) >= cutoff && (s.ts||0) <= endCutoff); 11222 _renderTopSearches(); 11223} 11224 11225let _tsTab = 'unique'; // 'unique' | 'grouped' 11226 11227function setTopSearchTab(tab) { 11228 _tsTab = tab; 11229 const u = document.getElementById('tsTab-unique'); 11230 const g = document.getElementById('tsTab-grouped'); 11231 if (u) { u.style.background = tab === 'unique' ? 'var(--gray4)' : 'transparent'; u.style.color = tab === 'unique' ? 'var(--white)' : 'var(--gray5)'; } 11232 if (g) { g.style.background = tab === 'grouped' ? 'var(--gray4)' : 'transparent'; g.style.color = tab === 'grouped' ? 'var(--white)' : 'var(--gray5)'; } 11233 _renderTopSearches(); 11234} 11235 11236function _renderTopSearches() { 11237 const topEl = document.getElementById('top-searches'); 11238 if (!topEl) return; 11239 const raw = window._topSearchRaw || []; // array of {q, ts} 11240 if (raw.length === 0) { 11241 topEl.innerHTML = '<div style="color:var(--gray5);font-size:12px">××× × ×ª×× ×× ×¢××××</div>'; 11242 return; 11243 } 11244 11245 // termMap: key â { count, lastTs, display } 11246 // - count: how many times this term was searched 11247 // - lastTs: most-recent search timestamp (for tie-breaking)
11248 // - display: the label shown to the user 11249 const termMap = {}; 11250 11251 const _add = (key, display, ts) => { 11252 if (!termMap[key]) termMap[key] = { count: 0, lastTs: 0, display }; 11253 termMap[key].count++; 11254 if ((ts || 0) > termMap[key].lastTs) termMap[key].lastTs = ts || 0; 11255 }; 11256 11257 if (_tsTab === 'unique') { 11258 // Case-insensitive dedup: group by lowercase, show original casing of first occurrence 11259 raw.forEach(({ q, ts }) => { 11260 const key = (q || '').toLowerCase().trim(); 11261 // display = first seen casing 11262 const display = termMap[key] ? termMap[key].display : q; 11263 _add(key, display, ts); 11264 }); 11265 11266 } else { 11267 // Grouped: merge Hebrew â English aliases into one canonical label 11268 raw.forEach(({ q, ts }) => { 11269 const canonical = _normaliseSearchTerm(q) || (q || '').toLowerCase().trim(); 11270 _add(canonical, canonical, ts); 11271 }); 11272 } 11273 11274 // Sort: count descending â most-recent descending (tie-break) â alphabetical 11275 const topTerms = Object.values(termMap) 11276 .sort((a, b) => b.count - a.count || b.lastTs - a.lastTs || a.display.localeCompare(b.display)) 11277 .slice(0, 5); 11278 11279 const totalSearches = raw.length; 11280 const maxN = topTerms[0]?.count || 1; 11281 const rankColors = ['#00e87a','#63b3ed','#ffe45e','#b794f4','#fc8181']; 11282 11283 topEl.innerHTML = 11284 `<div style="font-size:10px;color:var(--gray5);margin-bottom:8px">×ת×× ${totalSearches} ××פ×ש×× ×תק××¤× ×× ××רת</div>` + 11285 topTerms.map(({ display, count }, i) => { 11286 const barW = Math.round(count / maxN * 100); 11287 return `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--gray2)"> 11288 <div style="width:18px;text-align:center;font-size:11px;font-weight:700;color:${rankColors[i]}">${i+1}</div> 11289 <div style="flex:1;min-width:0"> 11290 <div style="font-size:13px;color:var(--white);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${display}</div> 11291 <div style="height:3px;background:var(--gray2);border-radius:2px;margin-top:4px"> 11292 <div style="height:3px;width:${barW}%;background:${rankColors[i]};border-radius:2px;transition:width 0.4s"></div> 11293 </div> 11294 </div> 11295 <div style="font-size:12px;font-weight:600;color:${rankColors[i]};white-space:nowrap">${count}x</div> 11296 </div>`; 11297 }).join(''); 11298} 11299 11300// ââ Loading screen: Skeleton + Tips âââââââââââââââââââââââââââââââââââââââââ 11301const _TIPS = [ 11302 { icon: 'ð', text: '×ש×ר ××צר×× ××¢××× ×××-×××××× â ×ר×× ×× ×××ת ש××××ת ×§×× ×× ×× ××××"× ××× ××××ר ×ת ××§× ×××.' }, 11303 { icon: 'ð³', text: '×ר×××¡× ×שר×× ×ס××××× × ××ª× ×× ×§×ש××§ × ×סף ×¢× ×§× ××× ×¢× ×§×× ×§×פ×× â ת××× ×©××× ×××××§!' }, 11304 { icon: 'ð±', text: '×פ××קצ××ת ×©× KSP ×-Bug ×צ××¢×ת ×פע××× ××צע×× ×××¢×××× ×©×× ××פ××¢×× ××תר.' }, 11305 { icon: 'â°', text: 'ש××©× ×צ×ר××× ××× ×××× ××× ××× ××צ×× ×§×פ×× ×× ×שר×××× â ר×× ××× ×××ת ×פרס××ת ×ס××£ ×ש×××¢.' }, 11306 { icon: 'ð', text: '××¢×§× ×××¨× ×¢×¨××¦× ××××¨× ×©× ××× ×¤×××× ×¡×¨×× ×שר×××× ×× ×× ×פע××× ×§×פ×× ×× ×©×× ×פ×רס××× ××§×× ×ר×× - ×× ×× × × ××¦× ××× ××ת×.' }, 11307 { icon: 'ð', text: '××××§ ת××× ×× ×ש ×ש××× ××× × â ×פע××× ×× ×©××× ××תר ××× ×× ×××××××.' }, 11308 { icon: 'ð§©', text: '×× ×××ת ר××ת ××פשר×ת ××שת×ש ××§×× ×§×פ×× ×××× ×¢× ××צע ס××× â ש××× ×× ×¡×ת!' }, 11309 { icon: 'ð§', text: '×רש×× ×× ×××××ר ×©× ×× ×ת ××¤×¢× ×ר×ש×× × × ××ª× ×ª ×ר×× ×§×× ×× ×× ×©× 10-15% ×××.' }, 11310 { icon: 'ð', text: '××××§ פר×××× ××¤×¡× ×× ×××× ×× ××× ××××× - תת××× ××פש ש×××¢ ×ר×ש.' }, 11311 { icon: 'ð¯', text: 'ת×ר×× ×פק××¢× ×¢× ××§×פ×× ×× ×ª××× ×××××§ â ×§×××× ×¨××× ×¢××××× ×× ×××¨× ×ת×ר×× ×רש××.' }, 11312]; 11313let _tipIdx = -1; 11314let _tipInterval = null; 11315let _gameInterval = null; // kept for legacy references 11316 11317function _skelCard() { 11318 return `<div style="background:var(--gray2);border:1px solid var(--gray3);border-radius:14px;padding:14px"> 11319 <div style="display:flex;gap:10px;align-items:center;margin-bottom:10px"> 11320 <div class="shimmer-bg" style="width:38px;height:38px;border-radius:10px;flex-shrink:0"></div> 11321 <div style="flex:1"> 11322 <div class="shimmer-bg" style="height:10px;border-radius:5px;width:75%;margin-bottom:6px"></div> 11323 <div class="shimmer-bg" style="height:8px;border-radius:5px;width:50%"></div> 11324 </div> 11325 </div> 11326 <div class="shimmer-bg" style="height:20px;border-radius:8px;width:65%;margin-bottom:10px"></div> 11327 <div class="shimmer-bg" style="height:36px;border-radius:8px;margin-bottom:10px"></div> 11328 <div style="display:flex;gap:6px"> 11329 <div class="shimmer-bg" style="height:8px;border-radius:5px;width:40%"></div> 11330 <div class="shimmer-bg" style="height:8px;border-radius:5px;width:30%"></div> 11331 </div> 11332 </div>`; 11333} 11334 11335function _randomTipIdx() { 11336 if (_TIPS.length <= 1) return 0; 11337 let next; 11338 do { next = Math.floor(Math.random() * _TIPS.length); } while (next === _tipIdx); 11339 return next; 11340} 11341 11342function _showTip(i) { 11343 const icon = document.getElementById('tip-icon-main'); 11344 const text = document.getElementById('tip-text-main'); 11345 const bar = document.getElementById('tip-progress-bar'); 11346 if (!icon || !text) return; 11347 icon.textContent = _TIPS[i].icon; 11348 text.textContent = _TIPS[i].text; 11349 if (bar) {
11350 bar.style.transition = 'none'; 11351 bar.style.width = '0%'; 11352 requestAnimationFrame(() => requestAnimationFrame(() => { 11353 bar.style.transition = 'width 16s linear'; 11354 bar.style.width = '100%'; 11355 })); 11356 } 11357} 11358 11359function _startMiniGame() { 11360 const wrap = document.getElementById('mini-game-wrap'); 11361 const banner = document.getElementById('ai-banner'); 11362 if (!wrap) return; 11363 11364 // Always hide the skel-grid â skeletons are now inline in results-grid 11365 const skelGrid = document.getElementById('skel-grid'); 11366 if (skelGrid) { skelGrid.style.display = 'none'; skelGrid.innerHTML = ''; } 11367 11368 // Start tips carousel â random pick, never repeat back-to-back 11369 _tipIdx = _randomTipIdx(); 11370 _showTip(_tipIdx); 11371 clearInterval(_tipInterval); 11372 _tipInterval = setInterval(() => { _tipIdx = _randomTipIdx(); _showTip(_tipIdx); }, 16000); 11373 11374 wrap.style.display = 'block'; 11375 if (banner) banner.style.borderRadius = '16px 16px 0 0'; 11376} 11377 11378function _stopMiniGame() { 11379 clearInterval(_tipInterval); _tipInterval = null; 11380 clearInterval(_gameInterval); _gameInterval = null; 11381 const wrap = document.getElementById('mini-game-wrap'); 11382 if (wrap) wrap.style.display = 'none'; 11383} 11384 11385// ââ Search Speed Analytics âââââââââââââââââââââââââââââââââââââââââââââââââââ 11386let _stab = 'week'; 11387let _chartSpeed = null; 11388let _speedMode = 'ai'; // 'ai' = fast_search (DB part of ××פ×ש AI) | 'fast' = search_time (××פ×ש ×ס××× ×××) 11389 11390function setSpeedMode(mode) { 11391 _speedMode = mode; 11392 const aiBtn = document.getElementById('smode-ai'); 11393 const fastBtn = document.getElementById('smode-fast'); 11394 if (aiBtn) { aiBtn.style.background = mode==='ai' ?'var(--gray4)':'transparent'; aiBtn.style.color = mode==='ai' ?'var(--white)':'var(--gray5)'; } 11395 if (fastBtn) { fastBtn.style.background = mode==='fast'?'var(--gray4)':'transparent'; fastBtn.style.color = mode==='fast'?'var(--white)':'var(--gray5)'; } 11396 loadSpeedAnalytics(); 11397} 11398 11399function setStab(btn, period) { 11400 if (period === 'custom') { _openDatePicker('speed', btn); return; } 11401 _stab = period; 11402 _updateTabBar('stab', _stab); 11403 loadSpeedAnalytics(); 11404} 11405 11406let _sltab = 'week'; 11407function setSltab(period, btn) { 11408 if (period === 'custom') { _openDatePicker('slowest', btn); return; } 11409 _sltab = period; 11410 _renderSlowBar(); 11411 _renderSlowest(); 11412} 11413function _renderSlowBar() { 11414 const el = document.getElementById('slowest-period-bar'); 11415 if (!el) return; 11416 const periods = [['day','××××'],['week','ש×××¢×'],['month','×××ש×'],['year','×©× ×ª×'],['all','×××'],['custom','ð ']]; 11417 el.innerHTML = periods.map(([p, lbl]) => { 11418 const on = p === _sltab; 11419 return `<button data-period="${p}" onclick="setSltab('${p}',this)" ${p==='custom'?'data-dp-trigger':''} style="padding:3px 8px;border:none;border-radius:6px;font-size:10px;cursor:pointer;font-family:'DM Sans',sans-serif;background:${on?'var(--gray4)':'transparent'};color:${on?'var(--white)':'var(--gray5)'}">${lbl}</button>`; 11420 }).join(''); 11421} 11422function _renderSlowest() { 11423 const slowEl = document.getElementById('speed-slowest'); 11424 if (!slowEl) return; 11425 const allData = window._allSpeedData || []; 11426 const cutoff = _getCutoff(_sltab, 'slowest'); 11427 const endCutoff = _getEndCutoff(_sltab, 'slowest'); 11428 const times = allData.filter(t => (t.ts||0) >= cutoff && (t.ts||0) <= endCutoff); 11429 if (times.length === 0) { slowEl.innerHTML = '<div style="color:var(--gray5);font-size:12px">××× × ×ª×× ××</div>'; return; } 11430 const slowest = [...times].sort((a,b) => b.elapsed - a.elapsed).slice(0, 5); 11431 const maxE = slowest[0].elapsed || 1; 11432 slowEl.innerHTML = slowest.map((t, i) => { 11433 const col = t.elapsed > 60 ? '#fc8181' : t.elapsed > 30 ? '#ffe45e' : '#00e87a'; 11434 const barW = Math.round(t.elapsed / maxE * 100); 11435 return `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--gray2)"> 11436 <div style="width:18px;text-align:center;font-size:11px;font-weight:700;color:${col}">${i+1}</div> 11437 <div style="flex:1;min-width:0"> 11438 <div style="font-size:13px;color:var(--white);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${t.q}</div> 11439 <div style="height:3px;background:var(--gray2);border-radius:2px;margin-top:4px"> 11440 <div style="height:3px;width:${barW}%;background:${col};border-radius:2px;transition:width 0.4s"></div> 11441 </div> 11442 </div> 11443 <div style="text-align:left;white-space:nowrap"> 11444 <div style="font-size:12px;font-weight:700;color:${col}">${t.elapsed}s</div> 11445 <div style="font-size:10px;color:var(--gray5)">${t.results} ת×צ××ת</div> 11446 </div> 11447 </div>`; 11448 }).join(''); 11449} 11450 11451function loadSpeedAnalytics() { 11452 const d = _aGet(); 11453 const now = Date.now(); 11454 let cutoff = 0; 11455 if (_stab === 'day') cutoff = now - 86400000; 11456 if (_stab === 'week') cutoff = now - 7 * 86400000; 11457 if (_stab === 'month') cutoff = now - 30 * 86400000; 11458 if (_stab === 'year') cutoff = now - 365 * 86400000; 11459 if (_stab === 'custom') cutoff = _getCutoff('custom', 'speed'); 11460 const endCutoff = _getEndCutoff(_stab, 'speed'); 11461 11462 const isDeep = _speedMode === 'fast'; // 'fast' mode = ××פ×ש ×ס××× ××× = search_time events 11463 const eventType = isDeep ? 'search_time' : 'fast_search'; 11464 11465 // Update dynamic labels 11466 const chartLabel = document.getElementById('speed-chart-label'); 11467 const slowestLabel = document.getElementById('slowest-label'); 11468 if (chartLabel) chartLabel.textContent = isDeep ? 'â±ï¸ ××× ××פ×ש ×ס××× ××××¨× ××× (×©× ××ת)' : 'â¡ ××× ××פ×ש AI ××××¨× ××× (×©× ××ת)';
11469 if (slowestLabel) slowestLabel.textContent = isDeep ? 'ð¢ ×××פ×ש×× ××ס××××× ××××××× ×××תר' : 'ð¢ ××פ××©× AI ××ר×××× ×××תר'; 11470 11471 window._allSpeedData = (d.events || []) 11472 .filter(e => e.type === eventType && e.data && e.data.elapsed) 11473 .map(e => ({ elapsed: e.data.elapsed, q: e.data.q || '', ts: e.ts || 0, results: e.data.results || 0 })); 11474 11475 const times = (d.events || []) 11476 .filter(e => e.type === eventType && (e.ts||0) >= cutoff && (e.ts||0) <= endCutoff && e.data && e.data.elapsed) 11477 .map(e => ({ elapsed: e.data.elapsed, q: e.data.q || '', ts: e.ts || 0, results: e.data.results || 0 })); 11478 11479 const failedCount = !isDeep ? 0 : (d.events || []).filter(e => e.type === 'search_failed' && (e.ts||0) >= cutoff && (e.ts||0) <= endCutoff).length; 11480 const droppedCount = !isDeep ? 0 : (d.events || []).filter(e => e.type === 'search_dropped' && (e.ts||0) >= cutoff && (e.ts||0) <= endCutoff).length; 11481 const totalAttempts = !isDeep ? 0 : (d.searches || []).filter(s => (s.ts||0) >= cutoff && (s.ts||0) <= endCutoff).length; 11482 const unknownCount = !isDeep ? 0 : Math.max(0, totalAttempts - times.length - failedCount - droppedCount); 11483 11484 const cards = document.getElementById('speed-cards'); 11485 if (!cards) return; 11486 11487 if (times.length === 0) { 11488 cards.innerHTML = `<div style="grid-column:1/-1;color:var(--gray5);font-size:13px">××× × ×ª×× × ××× ××פ×ש ×¢×××× â ×צע ××פ×ש ××× ××ת××× ××ס××£ × ×ª×× ××.</div>`; 11489 _renderSlowest(); 11490 if (_chartSpeed) { _chartSpeed.destroy(); _chartSpeed = null; } 11491 return; 11492 } 11493 11494 const elapseds = times.map(t => t.elapsed); 11495 const avg = (elapseds.reduce((a,b)=>a+b,0) / elapseds.length).toFixed(1); 11496 const minT = elapseds.reduce((a, b) => a < b ? a : b, Infinity).toFixed(1); 11497 const maxT = elapseds.reduce((a, b) => a > b ? a : b, -Infinity).toFixed(1); 11498 const over60 = elapseds.filter(t => t > 60).length; 11499 const threshold = isDeep ? 30 : 5; // deep=live scraping (<30s good), ai=DB lookup (<5s good) 11500 const pctFast = Math.round((elapseds.filter(t => t < threshold).length / elapseds.length) * 100); 11501 11502 const sc = (icon, val, label, col) => ` 11503 <div style="background:var(--gray3);border-radius:10px;padding:12px 10px;text-align:center"> 11504 <div style="font-size:18px">${icon}</div> 11505 <div style="font-size:22px;font-weight:700;font-family:system-ui;color:${col||'var(--white)'};line-height:1.2">${val}</div> 11506 <div style="font-size:10px;color:var(--gray5);margin-top:3px">${label}</div> 11507 </div>`; 11508 11509 cards.innerHTML = [ 11510 sc('ð', avg + 's', '×××צע', 'var(--white)'), 11511 sc('ð¢', minT + 's', '×××ר ×××תר', 'var(--accent)'), 11512 sc('ð´', maxT + 's', '×××× ×××תר', '#fc8181'), 11513 sc('â¡', pctFast + '%', `×ת×ת ×-${threshold}s`, pctFast > 70 ? 'var(--accent)' : '#ffe45e'), 11514 sc('ð¢', times.length, '××פ×ש×× ××ש×××', 'var(--white)'), 11515 ...(!isDeep ? [] : [ 11516 sc('â', failedCount, '××פ×ש×× × ×ש××', failedCount > 0 ? '#fc8181' : 'var(--gray5)'), 11517 sc('â©ï¸', droppedCount, '××פ×ש×× × ×× ××', droppedCount > 0 ? '#ffe45e' : 'var(--gray5)'), 11518 sc('â', unknownCount, '××פ×ש×× ×× ××××¢××', unknownCount > 0 ? '#a78bfa' : 'var(--gray5)'), 11519 ]), 11520 ].join(''); 11521 11522 // ââ Chart ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 11523 const isDark = !document.body.classList.contains('light'); 11524 const gridClr = isDark ? '#2a2a2a' : '#d0d0ce'; 11525 const txtClr = isDark ? '#888' : '#606060'; 11526 11527 let bucketMs, bucketCount, labelFn; 11528 if (_stab === 'day') { 11529 bucketMs = 3600000; bucketCount = 24; 11530 labelFn = ts => new Date(ts).getHours() + ':00'; 11531 } else if (_stab === 'week') { 11532 bucketMs = 86400000; bucketCount = 7; 11533 labelFn = ts => { const d2=new Date(ts); return `${d2.getDate()}/${d2.getMonth()+1}`; }; 11534 } else if (_stab === 'month') { 11535 bucketMs = 86400000; bucketCount = 30; 11536 labelFn = ts => { const d2=new Date(ts); return `${d2.getDate()}/${d2.getMonth()+1}`; }; 11537 } else { 11538 // Auto-range: use ALL events (not just search_time) so the span is correct 11539 const allEvTs = (d.events || []).map(e =>
11539 e.ts || 0).filter(Boolean); 11540 const firstTs = allEvTs.length ? allEvTs.reduce((a, b) => a < b ? a : b, Infinity) : now - 7*86400000; 11541 const spanMs = now - firstTs; 11542 if (spanMs <= 2 * 86400000) { 11543 bucketMs = 3600000; 11544 bucketCount = Math.max(6, Math.ceil(spanMs / 3600000) + 1); 11545 labelFn = ts => new Date(ts).getHours() + ':00'; 11546 } else { 11547 bucketMs = 86400000; 11548 bucketCount = Math.max(3, Math.ceil(spanMs / 86400000) + 1); 11549 labelFn = ts => { const d2=new Date(ts); return `${d2.getDate()}/${d2.getMonth()+1}`; }; 11550 } 11551 } 11552 11553 const buckets = []; 11554 for (let i = bucketCount - 1; i >= 0; i--) { 11555 const start = Math.floor((now - i * bucketMs) / bucketMs) * bucketMs; 11556 buckets.push({ start, label: labelFn(start), vals: [] }); 11557 } 11558 times.forEach(t => { 11559 const bi = buckets.findIndex(b => t.ts >= b.start && t.ts < b.start + bucketMs); 11560 if (bi >= 0) buckets[bi].vals.push(t.elapsed); 11561 }); 11562 11563 const labels = buckets.map(b => b.label); 11564 const avgData = buckets.map(b => b.vals.length ? +(b.vals.reduce((a,v)=>a+v,0)/b.vals.length).toFixed(1) : null); 11565 const maxData = buckets.map(b => b.vals.length ? b.vals.reduce((a, v) => a > v ? a : v, -Infinity) : null); 11566 11567 const actCanvas = document.getElementById('chart-speed'); 11568 if (actCanvas) { 11569 if (_chartSpeed) _chartSpeed.destroy(); 11570 _chartSpeed = new Chart(actCanvas.getContext('2d'), { 11571 type: 'line', 11572 data: { 11573 labels, 11574 datasets: [ 11575 { label: '×××צע (s)', data: avgData, borderColor: 'rgba(0,232,122,0.85)', backgroundColor: 'rgba(0,232,122,0.08)', tension: 0.4, fill: true, pointRadius: 3, spanGaps: true }, 11576 { label: '×קס×××× (s)', data: maxData, borderColor: 'rgba(252,129,129,0.8)', backgroundColor: 'rgba(252,129,129,0.06)', tension: 0.4, fill: false, pointRadius: 3, spanGaps: true }, 11577 ] 11578 }, 11579 options: { 11580 responsive: true, maintainAspectRatio: false, 11581 animation: { duration: 400 }, 11582 plugins: { 11583 legend: { display: true, position: 'top', align: 'end', 11584 labels: { color: txtClr, font: { size: 11, family: 'DM Sans' }, boxWidth: 10, padding: 10, 11585 generateLabels: chart => chart.data.datasets.map((ds, i) => ({ 11586 text: ds.label, datasetIndex: i, hidden: false, 11587 fillStyle: ds.borderColor, strokeStyle: ds.borderColor, lineWidth: 0,
11588 fontColor: txtClr 11589 })) 11590 } 11591 }, 11592 tooltip: { backgroundColor: '#1a1a1a', titleColor: '#fff', bodyColor: '#bbb', borderColor: '#3d3d3d', borderWidth: 1 } 11593 }, 11594 scales: { 11595 x: { grid: { color: gridClr }, ticks: { color: txtClr, font: { size: 10 }, maxRotation: 0, maxTicksLimit: 8 } }, 11596 y: { grid: { color: gridClr }, ticks: { color: txtClr, font: { size: 10 } }, beginAtZero: true, 11597 title: { display: true, text: '×©× ××ת', color: txtClr, font: { size: 10 } } } 11598 } 11599 } 11600 }); 11601 } 11602 11603 // ââ Slowest searches âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 11604 _renderSlowest(); 11605} 11606 11607// ââ Popup analytics panel âââââââââââââââââââââââââââââââââââââââââââââââââââââ 11608let _ptab = 'week'; 11609function setPopupTab(btn, period) { 11610 if (period === 'custom') { _openDatePicker('popup', btn); return; } 11611 _ptab = period; 11612 _updateTabBar('ptab', _ptab); 11613 loadPopupAnalytics(); 11614} 11615function _ptabCutoff() { return _getCutoff(_ptab, 'popup'); } 11616function _ptabEndCutoff() { return _getEndCutoff(_ptab, 'popup'); } 11617 11618function loadPopupAnalytics() { 11619 const d = _aGet(); 11620 const cutoff = _ptabCutoff(); 11621 const endCutoff = _ptabEndCutoff(); 11622 const events = (d.events || []).filter(e => (e.ts||0) >= cutoff && (e.ts||0) <= endCutoff); 11623 11624 const popups = events.filter(e => e.type === 'coupon_popup'); 11625 const copies = events.filter(e => e.type === 'modal_copy'); 11626 const moreClicks = events.filter(e => e.type === 'more_coupons'); 11627 11628 const cards = document.getElementById('popup-cards'); 11629 if (!cards) return; 11630 11631 const sc = (icon, val, label, col) => ` 11632 <div style="background:var(--gray3);border-radius:10px;padding:12px 10px;text-align:center"> 11633 <div style="font-size:18px">${icon}</div> 11634 <div style="font-size:22px;font-weight:700;font-family:system-ui;color:${col||'var(--white)'};line-height:1.2">${val}</div> 11635 <div style="font-size:10px;color:var(--gray5);margin-top:3px">${label}</div> 11636 </div>`; 11637 11638 const uniqueStores = new Set(popups.map(e => (e.data && e.data.store) || '').filter(Boolean)).size; 11639 11640 cards.innerHTML = [ 11641 sc('ð«', popups.length, 'פת×××ת פ×פ×פ', '#a78bfa'), 11642 sc('ðª', uniqueStores, '×× ×××ת ש×× ×ת', '#63b3ed'), 11643 sc('ð', copies.length, '×עתק×ת ××פ×פ×פ', 'var(--accent)'), 11644 sc('ð', moreClicks.length, '×××צ×ת ×¢×× ×§×פ×× ××', '#ffe45e'), 11645 ].join(''); 11646 11647 // ââ Top stores ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 11648 const storeCount = {}; 11649 popups.forEach(e => { 11650 const s = (e.data && e.data.store) || ''; 11651 if (s) storeCount[s] = (storeCount[s] || 0) + 1; 11652 }); 11653 11654 const topEl = document.getElementById('popup-top-stores'); 11655 if (!topEl) return; 11656 11657 const sorted = Object.entries(storeCount).sort((a, b) => b[1] - a[1]).slice(0, 10); 11658 if (sorted.length === 0) { 11659 topEl.innerHTML = '<div style="color:var(--gray5);font-size:12px">××× × ×ª×× ×× ×¢××××</div>'; 11660 return; 11661 } 11662 11663 const maxCount = sorted[0][1]; 11664 const medal = ['ð¥','ð¥','ð¥']; 11665 topEl.innerHTML = sorted.map(([store, count], i) => { 11666 const barW = Math.round(count / maxCount * 100); 11667 const col = i < 3 ? ['#ffe45e','#c0c0c0','#cd7f32'][i] : 'var(--accent)'; 11668 return `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--gray2)"> 11669 <div style="width:20px;text-align:center;font-size:13px">${medal[i] || i+1}</div> 11670 <div style="flex:1;min-width:0"> 11671 <div style="font-size:13px;color:var(--white);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${store}</div> 11672 <div style="height:3px;background:var(--gray2);border-radius:2px;margin-top:4px"> 11673 <div style="height:3px;width:${barW}%;background:${col};border-radius:2px;transition:width 0.4s"></div> 11674 </div> 11675 </div> 11676 <div style="font-size:12px;font-weight:700;color:${col};white-space:nowrap">${count}</div> 11677 </div>`; 11678 }).join(''); 11679} 11680 11681_loadGlobeFingerprint(); 11682loadCoupons(); 11683loadStats(); 11684renderExpiredDemo(); // show demo immediately, doesn't need API data 11685 11686// Initialize affiliate sidebar badge from localStorage 11687(()=>{ const reg = _AFF_LIST.filter(p=>_affGetStatus(p.id)==='registered').length; const el=document.getElementById('src-aff-count'); if(el) el.textContent=reg||'â'; })(); 11688 11689// ââ Theme toggle ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
11690const MOON_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>`; 11691const SUN_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>`; 11692 11693function toggleTheme() { 11694 const isLight = document.body.classList.toggle('light'); 11695 localStorage.setItem('theme', isLight ? 'light' : 'dark'); 11696 document.getElementById('theme-btn').innerHTML = isLight ? SUN_SVG : MOON_SVG; 11697} 11698 11699function _navDrawerOpen() { 11700 document.getElementById('nav-drawer').classList.add('open'); 11701 document.getElementById('nav-backdrop').classList.add('show'); 11702} 11703function _navDrawerClose() { 11704 document.getElementById('nav-drawer').classList.remove('open'); 11705 document.getElementById('nav-backdrop').classList.remove('show'); 11706} 11707 11708// Apply saved theme on load 11709(function() { 11710 if (localStorage.getItem('theme') === 'light') { 11711 document.body.classList.add('light'); 11712 document.getElementById('theme-btn').innerHTML = SUN_SVG; 11713 } 11714})(); 11715 11716// ââ Extension Analytics ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 11717let _etab = 'week'; 11718let _extStoresPageIdx = 0; 11719let _extAllStores = []; // full sorted list for paging 11720const EXT_PAGE_SIZE = 5; 11721 11722function setEtab(btn, period) { 11723 _etab = period; 11724 _extStoresPageIdx = 0; 11725 document.querySelectorAll('.etab').forEach(b => { 11726 b.style.background = 'transparent'; b.style.color = 'var(--gray5)'; 11727 }); 11728 btn.style.background = 'var(--gray4)'; btn.style.color = 'var(--white)'; 11729 loadExtensionAnalytics(); 11730} 11731 11732function extStoresPage(dir) { 11733 const pages = Math.ceil(_extAllStores.length / EXT_PAGE_SIZE); 11734 _extStoresPageIdx = Math.max(0, Math.min(pages - 1, _extStoresPageIdx + dir)); 11735 _renderExtTopStores(); 11736} 11737 11738function _renderExtTopStores() { 11739 const storesEl = document.getElementById('ext-top-stores'); 11740 if (!storesEl) return; 11741 const pages = Math.ceil(_extAllStores.length / EXT_PAGE_SIZE) || 1; 11742 const page = _extAllStores.slice(_extStoresPageIdx * EXT_PAGE_SIZE, (_extStoresPageIdx + 1) * EXT_PAGE_SIZE); 11743 const maxVal = (_extAllStores[0]?.[1]) || 1; 11744 const medal = ['ð¥','ð¥','ð¥']; 11745 11746 const label = document.getElementById('ext-stores-page-label'); 11747 if (label) label.textContent = pages > 1 ? `${_extStoresPageIdx + 1} / ${pages}` : ''; 11748 const prevBtn = document.getElementById('ext-stores-prev'); 11749 const nextBtn = document.getElementById('ext-stores-next'); 11750 if (prevBtn) prevBtn.style.opacity = _extStoresPageIdx === 0 ? '0.3' : '1'; 11751 if (nextBtn) nextBtn.style.opacity = _extStoresPageIdx >= pages - 1 ? '0.3' : '1'; 11752 11753 if (!page.length) { 11754 storesEl.innerHTML = '<div style="font-size:11px;color:var(--gray5)">××× × ×ª×× ×× ×¢×××× â ×× ×ª×× ×× ××פ××¢× ×××ר ש××רש×× ×××§×ר×× ××ש××</div>'; 11755 return; 11756 } 11757 11758 const globalOffset = _extStoresPageIdx * EXT_PAGE_SIZE; 11759 storesEl.innerHTML = page.map(([store, count], i) => { 11760 const rank = globalOffset + i; 11761 const barW = Math.round(count / maxVal * 100); 11762 const col = rank < 3 ? ['#ffe45e','#c0c0c0','#cd7f32'][rank] : '#00e87a'; 11763 return `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--gray3)"> 11764 <div style="width:22px;text-align:center;font-size:13px">${medal[rank] || rank + 1}</div> 11765 <div style="flex:1;min-width:0"> 11766 <div style="font-size:12px;color:var(--white);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${store}</div> 11767 <div style="height:3px;background:var(--gray2);border-radius:2px;margin-top:4px"> 11768 <div style="height:3px;width:${barW}%;background:${col};border-radius:2px;transition:width 0.4s"></div> 11769 </div> 11770 </div> 11771 <div style="font-size:12px;font-weight:700;color:${col}">${count}</div> 11772 </div>`; 11773 }).join(''); 11774} 11775 11776function loadExtensionAnalytics() { 11777 const d = _aGet(); 11778 const now = Date.now(); 11779 const cutoffMap = { day: now - 86400000, week: now - 7*86400000, month: now - 30*86400000, all: 0 }; 11780 const cutoff = cutoffMap[_etab] || 0; 11781 11782 const allEvents = (d.events || []).filter(e => (e.ts||0) >= cutoff); 11783 const events = allEvents.filter(e => (e.type||'').startsWith('ext_')); 11784 const cartEvs = allEvents.filter(e => (e.type||'').startsWith('cart_')); 11785 11786 const visits = events.filter(e => e.type === 'ext_store_visit'); 11787 const opens = events.filter(e => e.type === 'ext_popup_open'); 11788 const reveals = events.filter(e => e.type === 'ext_reveal'); 11789 const copies = events.filter(e => e.type === 'ext_copy'); 11790 const viewAlls = events.filter(e => e.type === 'ext_view_all'); 11791 const voteUps = events.filter(e => e.type === 'ext_vote_up'); 11792 const voteDns = events.filter(e => e.type === 'ext_vote_down'); 11793 11794 const cartOpens = cartEvs.filter(e => e.type === 'cart_popup_open'); 11795 const cartReveals = cartEvs.filter(e => e.type === 'cart_reveal'); 11796 const cartCopies = cartEvs.filter(e => e.type === 'cart_copy'); 11797 const cartViews = cartEvs.filter(e => e.type === 'cart_view_all'); 11798 const cartVoteUps = cartEvs.filter(e => e.type === 'cart_vote_up'); 11799 const cartVoteDns = cartEvs.filter(e => e.type === 'cart_vote_down'); 11800 11801 // ââ Stat cards ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 11802 const grid = document.getElementById('ext-stats-grid'); 11803 if (!grid) return; 11804 const stat = (icon, val, label) => ` 11805 <div style="background:var(--gray2);border-radius:8px;padding:10px 8px;text-align:center"> 11806 <div style="font-size:16px">${icon}</div> 11807 <div style="font-size:20px;font-weight:700;font-family:system-ui;color:var(--white);line-height:1.2">${val}</div> 11808 <div style="font-size:10px;color:var(--gray5);margin-top:2px">${label}</div> 11809 </div>`; 11810 grid.innerHTML = [ 11811 stat('ðª', visits.length, '×××§××¨× ×× ×ת'), 11812 stat('ð§©', opens.length, 'פת×××ת פ×פ×פ'), 11813 stat('ðï¸', reveals.length, '×§×פ×× ×× ×©× ×שפ×'), 11814 stat('ð', copies.length, '×§×פ×× ×× ×©××עתק×'), 11815 stat('ð', viewAlls.length, '×¦×¤× ××× ××§×פ×× ××'), 11816 stat('ð', voteUps.length, '×צ××¢×ת ××××××ת'), 11817 stat('ð', voteDns.length, '×צ××¢×ת ש×××××ת'), 11818 ].join(''); 11819 11820 // ââ Cart popup stats âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 11821 const cartGrid = document.getElementById('cart-stats-grid'); 11822 if (cartGrid) cartGrid.innerHTML = [ 11823 stat('ð', cartOpens.length, 'פת×××ת פ×פ×פ'), 11824 stat('ðï¸', cartReveals.length, '×§×פ×× ×× ×©× ×שפ×'), 11825 stat('ð', cartCopies.length, '×§×פ×× ×× ×©××עתק×'), 11826 stat('ð', cartViews.length, '×¦×¤× ××× ××§×פ×× ××'), 11827 stat('ð', cartVoteUps.length, '×צ××¢×ת ××××××ת'), 11828 stat('ð', cartVoteDns.length, '×צ××¢×ת ש×××××ת'), 11829 ].join(''); 11830 11831 // ââ Top stores (combined ext + cart visits) with paging âââââââââââââââââââ 11832 const storeCounts = {}; 11833 [...visits, ...cartEvs.filter(e =>
11833 e.type === 'cart_popup_open')].forEach(e => { 11834 const s = e.store || e.data?.store; 11835 if (s) storeCounts[s] = (storeCounts[s] || 0) + 1; 11836 }); 11837 _extAllStores = Object.entries(storeCounts).sort((a, b) => b[1] - a[1]); 11838 _extStoresPageIdx = 0; 11839 _renderExtTopStores(); 11840 11841 // ââ Comparison chart âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 11842 const cmpEl = document.getElementById('ext-comparison-chart'); 11843 if (cmpEl) { 11844 const metrics = [ 11845 { label: 'פת×××ת פ×פ×פ', ext: opens.length, cart: cartOpens.length }, 11846 { label: '×§×פ×× ×× ×©× ×שפ×', ext: reveals.length, cart: cartReveals.length }, 11847 { label: '×§×פ×× ×× ×©××עתק×', ext: copies.length, cart: cartCopies.length }, 11848 { label: '×¦×¤× ××× ××§×פ×× ××', ext: viewAlls.length, cart: cartViews.length }, 11849 { label: '×צ××¢×ת', ext: voteUps.length + voteDns.length, cart: cartVoteUps.length + cartVoteDns.length }, 11850 ]; 11851 const maxVal = Math.max(1, ...metrics.map(m => Math.max(m.ext, m.cart))); 11852 cmpEl.innerHTML = metrics.map(m => ` 11853 <div style="margin-bottom:12px"> 11854 <div style="font-size:11px;color:var(--gray5);margin-bottom:5px;text-align:right">${m.label}</div> 11855 <div style="display:flex;flex-direction:column;gap:3px"> 11856 <div style="display:flex;align-items:center;gap:6px"> 11857 <div style="font-size:10px;color:#00e87a;width:60px;text-align:right;flex-shrink:0">פ×פ×פ ð§©</div> 11858 <div style="flex:1;background:var(--gray3);border-radius:4px;height:8px"> 11859 <div style="width:${Math.round(m.ext/maxVal*100)}%;background:#00e87a;height:8px;border-radius:4px;transition:width .3s"></div> 11860 </div> 11861 <div style="font-size:11px;color:var(--white);min-width:24px;text-align:left">${m.ext}</div> 11862 </div> 11863 <div style="display:flex;align-items:center;gap:6px"> 11864 <div style="font-size:10px;color:#60a5fa;width:60px;text-align:right;flex-shrink:0">×¡× ×§× ××ת ð</div> 11865 <div style="flex:1;background:var(--gray3);border-radius:4px;height:8px"> 11866 <div style="width:${Math.round(m.cart/maxVal*100)}%;background:#60a5fa;height:8px;border-radius:4px;transition:width .3s"></div> 11867 </div> 11868 <div style="font-size:11px;color:var(--white);min-width:24px;text-align:left">${m.cart}</div> 11869 </div> 11870 </div> 11871 </div>`).join(''); 11872 } 11873} 11874 11875// ââ Button Click Analytics ââââââââââââââââââââââââââââââââââââââââââââââââââââ 11876function loadButtonClickAnalytics() { 11877 const container = document.getElementById('view-analytics-buttons'); 11878 if (!container) return; 11879 container.innerHTML = '<div style="color:var(--gray5);padding:20px">×××¢×...</div>'; 11880 fetch('/api/admin/button-clicks').then(r => r.json()).then(res => { 11881 const rows = res.data || []; 11882 if (!rows.length) { 11883 container.innerHTML = '<div style="color:var(--gray5);padding:20px">××× × ×ª×× ×× ×¢××××.</div>'; 11884 return; 11885 } 11886 const tableRows = rows.map(r => ` 11887 <tr style="border-bottom:1px solid var(--gray3)"> 11888 <td style="padding:10px 14px;font-size:13px;color:var(--white);direction:ltr">${r.button_name}</td> 11889 <td style="padding:10px 14px;font-size:14px;font-weight:700;color:var(--accent);text-align:center">${r.unique_users}</td> 11890 <td style="padding:10px 14px;font-size:14px;font-weight:700;color:var(--white);text-align:center">${r.total_clicks}</td> 11891 </tr>`).join(''); 11892 container.innerHTML = ` 11893 <div style="background:var(--gray2);border:1px solid var(--gray3);border-radius:12px;overflow:hidden"> 11894 <div style="padding:16px 20px;border-bottom:1px solid var(--gray3);font-family:'Syne',sans-serif;font-size:16px;font-weight:700">תפר×× ××פת×ר××</div> 11895 <table style="width:100%;border-collapse:collapse"> 11896 <thead> 11897 <tr style="background:var(--gray1);border-bottom:1px solid var(--gray3)"> 11898 <th style="padding:10px 14px;text-align:right;font-size:12px;color:var(--gray5);font-weight:600">×פת×ר</th> 11899 <th style="padding:10px 14px;text-align:center;font-size:12px;color:var(--gray5);font-weight:600">×שת×ש×× ××××××××</th> 11900 <th style="padding:10px 14px;text-align:center;font-size:12px;color:var(--gray5);font-weight:600">ס×"× ×××צ×ת</th> 11901 </tr> 11902 </thead> 11903 <tbody>${tableRows}</tbody> 11904 </table> 11905 </div>`; 11906 }).catch(() => { 11907 container.innerHTML = '<div style="color:#f87171;padding:20px">ש×××× ×××¢×× ×ª ×× ×ª×× ××.</div>'; 11908 }); 11909} 11910 11911// ââ User Profiles âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 11912function loadUserProfiles(offset) { 11913 offset = offset || 0;
11914 const el = document.getElementById('view-user-profiles'); 11915 if (!el) return; 11916 if (!offset) el.innerHTML = '<div style="color:var(--gray5);padding:20px">×××¢×...</div>'; 11917 fetch(`/api/admin/user-devices?limit=100&offset=${offset}`).then(r=>r.json()).then(res=>{ 11918 const rows = res.data || []; 11919 const flag = c => c ? `<span title="${c}">${({IL:'ð®ð±',US:'ðºð¸',GB:'ð¬ð§',DE:'ð©ðª',FR:'ð«ð·',UA:'ðºð¦',RU:'ð·ðº'}[c]||'ð')}</span>` : ''; 11920 const dt = d => d ? new Date(d).toLocaleDateString('he-IL',{day:'2-digit',month:'2-digit',year:'2-digit',hour:'2-digit',minute:'2-digit'}) : 'â'; 11921 const rows_html = rows.map(r => { 11922 const geo = r.geo || {}; 11923 const ua = r.ua_parsed || {}; 11924 const loc = [geo.city, geo.country].filter(Boolean).join(', ') || 'â'; 11925 return `<tr style="cursor:pointer" onclick="loadUserProfile('${r.did}')"> 11926 <td style="font-family:monospace;font-size:11px;color:var(--gray5)">${(r.did||'').slice(0,14)}â¦</td> 11927 <td>${flag(geo.country_code)} ${loc}</td> 11928 <td>${ua.browser||'â'} / ${ua.os||'â'} <span style="font-size:11px;color:var(--gray5)">(${ua.device_type||'?'})</span></td> 11929 <td style="text-align:center">${r.session_count||0}</td> 11930 <td style="text-align:center">${r.search_count||0}</td> 11931 <td style="text-align:center">${r.copy_count||0}</td> 11932 <td style="font-size:12px;color:var(--gray5)">${dt(r.last_seen_at)}</td> 11933 <td style="font-size:12px;color:var(--gray5)">${dt(r.first_seen_at)}</td> 11934 </tr>`; 11935 }).join(''); 11936 const tbl = ` 11937 <h2 style="margin:0 0 16px;font-size:18px">ð¤ פר×פ××× ×שת×ש×× <span style="font-size:13px;color:var(--gray5);font-weight:400">(${offset+rows.length} loaded)</span></h2> 11938 ${offset===0 ? '' : document.getElementById('view-user-profiles').querySelector('table')?.outerHTML?.split('<tbody')[0] || ''} 11939 <table style="width:100%;border-collapse:collapse;font-size:13px"> 11940 <thead><tr style="color:var(--gray5);font-size:12px;text-align:right"> 11941 <th style="padding:6px 10px">Device ID</th><th style="padding:6px 10px">×××§××</th><th style="padding:6px 10px">××ש×ר</th> 11942 <th style="padding:6px 10px;text-align:center">×¡×©× ××</th><th style="padding:6px 10px;text-align:center">××פ×ש××</th> 11943 <th style="padding:6px 10px;text-align:center">×עתק×ת</th><th style="padding:6px 10px">×××§×ר ××ר××</th><th style="padding:6px 10px">ר×ש××</th> 11944 </tr></thead> 11945 <tbody>${rows_html}</tbody> 11946 </table> 11947 ${rows.length===100?`<button onclick="loadUserProfiles(${offset+100})" style="margin-top:12px;padding:8px 18px;background:var(--accent);border:none;border-radius:6px;color:#fff;cursor:pointer">××¢× ×¢××</button>`:''} 11948 `; 11949 el.innerHTML = tbl; 11950 }).catch(()=>{ el.innerHTML='<div style="color:#f87171;padding:20px">ש××××.</div>'; }); 11951} 11952 11953function loadUserProfile(did) { 11954 const el = document.getElementById('view-user-profiles'); 11955 el.innerHTML = '<div style="color:var(--gray5);padding:20px">×××¢× ×¤×¨×פ××...</div>'; 11956 fetch(`/api/admin/user-devices/${encodeURIComponent(did)}`).then(r=>r.json()).then(d=>{ 11957 const geo = d.geo||{}; const ua = d.ua_parsed||{}; const env = d.env||{}; const sc = d.screen||{}; const fp = d.fp||{}; 11958 const dt = x => x ? new Date(x).toLocaleString('he-IL') : 'â'; 11959 const row = (label, val) => val ? `<tr><td style="color:var(--gray5);padding:4px 10px;width:160px">${label}</td><td style="padding:4px 10px">${val}</td></tr>` : ''; 11960 el.innerHTML = ` 11961 <button onclick="loadUserProfiles()" style="margin-bottom:16px;padding:6px 14px;background:var(--card-bg);border:1px solid var(--border);border-radius:6px;color:var(--white);cursor:pointer">â ×××¨× ×רש×××</button> 11962 <h2 style="margin:0 0 16px;font-size:16px;font-family:monospace">${did}</h2> 11963 <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px"> 11964 <div style="background:var(--card-bg);border-radius:10px;padding:16px"> 11965 <div style="font-weight:600;margin-bottom:10px;color:var(--accent)">ð ×××§×× ×רשת</div> 11966 <table style="font-size:13px"> 11967 ${row('IP', d.ip)} ${row('×¢×ר', geo.city)} ${row('×××ר', geo.region)} ${row('×××× ×', geo.country)} 11968 ${row('ISP', geo.isp)} ${row('××××××', geo.mobile?'â ':'â')} ${row('VPN/Proxy', geo.proxy?'â ï¸ ××':'××')} 11969 </table> 11970 </div> 11971 <div style="background:var(--card-bg);border-radius:10px;padding:16px"> 11972 <div style="font-weight:600;margin-bottom:10px;color:var(--accent)">ð» ××ש×ר</div> 11973 <table style="font-size:13px"> 11974 ${row('×פ×פ×', ua.browser + (ua.browser_version?' '+ua.browser_version:''))} 11975 ${row('×ער×ת ×פע××', ua.os + (ua.os_version?' '+ua.os_version:''))} 11976 ${row('ס××', ua.device_type)} ${row('שפ×', env.lang)} ${row('×××ר ×××', env.tz)} 11977 ${row('×ס×', sc.w&&sc.h?`${sc.w}Ã${sc.h} (viewport ${sc.vw}Ã${sc.vh})`:'â')} 11978 ${row('DPR', sc.dpr)} ${row('×××¢', sc.touch>0?'â ':'â')} ${row('RAM', env.mem?env.mem+'GB':'â')} 11979 ${row('CPU cores', env.cpu)} ${row('××××ר', env.conn)} 11980 </table> 11981 </div> 11982 <div style="background:var(--card-bg);border-radius:10px;padding:16px"> 11983 <div style="font-weight:600;margin-bottom:10px;color:var(--accent)">ð פע×××ת</div> 11984 <table style="font-size:13px"> 11985 ${row('×¡×©× ××', d.session_count)} ${row('××פ×ש××', d.search_count)} ${row('×עתק×ת', d.copy_count)} 11986 ${row('×××§×ר ר×ש××', dt(d.first_seen_at))} ${row('×××§×ר ××ר××', dt(d.last_seen_at))} 11987 </table> 11988 </div> 11989 <div style="background:var(--card-bg);border-radius:10px;padding:16px"> 11990 <div style="font-weight:600;margin-bottom:10px;color:var(--accent)">ð¦ ××§×ר</div> 11991 <table style="font-size:13px"> 11992 ${row('Referrer', d.first_ref)} ${row('UTM source', (d.first_utm||{}).source)} 11993 ${row('UTM medium', (d.first_utm||{}).medium)} ${row('UTM campaign', (d.first_utm||{}).campaign)} 11994 </table> 11995 </div> 11996 ${fp.canvas||fp.webgl?`<div style="background:var(--card-bg);border-radius:10px;padding:16px;grid-column:span 2"> 11997 <div style="font-weight:600;margin-bottom:10px;color:var(--accent)">ð¬ Fingerprint</div> 11998 <table style="font-size:12px;font-family:monospace"> 11999 ${row('Canvas hash', fp.canvas)} ${row('WebGL renderer', (fp.webgl||{}).renderer)} ${row('WebGL vendor', (fp.webgl||{}).vendor)} 12000 </table> 12001 </div>`:''} 12002 </div> 12003 <div style="margin-top:12px;background:var(--card-bg);border-radius:10px;padding:14px"> 12004 <div style="font-size:11px;color:var(--gray5);word-break:break-all">${d.ua||''}</div> 12005 </div> 12006 `; 12007 }).catch(()=>{ el.innerHTML='<div style="color:#f87171;padding:20px">ש××××.</div>'; }); 12008} 12009 12010// ââ Search Intelligence ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 12011function loadSearchIntel() { 12012 const el = document.getElementById('view-search-intel'); 12013 if (!el) return; 12014 el.innerHTML = '<div style="color:var(--gray5);padding:20px">×××¢×...</div>'; 12015 fetch('/api/admin/search-intelligence').then(r=>r.json()).then(data=>{ 12016 const top = data.top || []; const total = data.total || 0; 12017 const topHtml = top.slice(0,50).map(r=>` 12018 <tr> 12019 <td style="padding:5px 10px">${r.q}</td> 12020 <td style="padding:5px 10px;text-align:center;font-weight:600">${r.count}</td> 12021 <td style="padding:5px 10px;text-align:center">${r.avg_results}</td> 12022 <td style="padding:5px 10px;text-align:center">${r.conversion_pct}%</td> 12023 </tr>`).join(''); 12024 el.innerHTML = ` 12025 <h2 style="margin:0 0 16px;font-size:18px">ð × ×ת×× ××פ×ש×× <span style="font-size:13px;color:var(--gray5);font-weight:400">(${total} ס×"×)</span></h2> 12026 <table style="width:100%;border-collapse:collapse;font-size:13px;margin-bottom:32px"> 12027 <thead><tr style="color:var(--gray5);font-size:12px;text-align:right"> 12028 <th style="padding:6px 10px">ש×××ת×</th><th style="padding:6px 10px;text-align:center">פע×××</th> 12029 <th style="padding:6px 10px;text-align:center">×××צע ת×צ××ת</th><th style="padding:6px 10px;text-align:center">××ר×</th> 12030 </tr></thead> 12031 <tbody>${topHtml}</tbody> 12032 </table> 12033 `; 12034 }).catch(()=>{ el.innerHTML='<div style="color:#f87171;padding:20px">ש××××.</div>'; }); 12035} 12036 12037// ââ Demand Gaps ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 12038function loadDemandGaps() { 12039 const el = document.getElementById('view-demand-gaps'); 12040 if (!el) return; 12041 el.innerHTML = '<div style="color:var(--gray5);padding:20px">×××¢×...</div>'; 12042 fetch('/api/admin/search-intelligence').then(r=>r.json()).then(data=>{ 12043 const failed = data.failed || []; 12044 if (!failed.length) { el.innerHTML='<div style="padding:40px;text-align:center;color:var(--gray5)">××× ××פ×ש×× ××× ×ª×צ××ת ð</div>'; return; } 12045 const html = failed.map(r=>` 12046 <tr> 12047 <td style="padding:8px 12px;font-weight:500">${r.q}</td> 12048 <td style="padding:8px 12px;text-align:center;color:#f87171;font-weight:700">${r.count}Ã</td> 12049 <td style="padding:8px 12px;text-align:center">
12050 <button onclick="document.querySelector('[onclick*=coupons]')&&document.querySelector('[onclick*=coupons]').click();document.getElementById('search-input')&&(document.getElementById('search-input').value='${r.q.replace(/'/,"\\'")}');searchCoupons&&searchCoupons()" style="padding:4px 10px;background:var(--accent);border:none;border-radius:5px;color:#fff;cursor:pointer;font-size:12px">××סף ×§×פ××</button> 12051 </td> 12052 </tr>`).join(''); 12053 el.innerHTML = ` 12054 <h2 style="margin:0 0 8px;font-size:18px">ð³ï¸ ×××§×ש ××× ×§×פ×× ××</h2> 12055 <p style="color:var(--gray5);margin:0 0 20px;font-size:13px">××פ×ש×× ×©×××ר×× ×¢× ×¢×¦×× ××× ××××ר×× ×ª×צ××ת â פ××× ×¦××× ×××ס××£ ×× ×××ת ××ש×ת</p> 12056 <table style="width:100%;border-collapse:collapse;font-size:13px"> 12057 <thead><tr style="color:var(--gray5);font-size:12px;text-align:right"> 12058 <th style="padding:6px 12px">××פ×ש ×©× ×ש×</th><th style="padding:6px 12px;text-align:center">××× ×¤×¢×××</th><th style="padding:6px 12px;text-align:center">פע×××</th> 12059 </tr></thead> 12060 <tbody>${html}</tbody> 12061 </table>`; 12062 }).catch(()=>{ el.innerHTML='<div style="color:#f87171;padding:20px">ש××××.</div>'; }); 12063} 12064 12065// ââ Traffic Sources ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 12066function loadTrafficSources() { 12067 const el = document.getElementById('view-traffic-sources'); 12068 if (!el) return; 12069 el.innerHTML = '<div style="color:var(--gray5);padding:20px">×××¢×...</div>'; 12070 fetch('/api/admin/traffic-sources').then(r=>r.json()).then(res=>{ 12071 const rows = res.data || []; 12072 const total = rows.reduce((s,r)=>s+r.sessions,0)||1; 12073 const colors = ['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#ec4899','#14b8a6','#f97316']; 12074 const html = rows.map((r,i)=>{ 12075 const pct = Math.round(r.sessions/total*100); 12076 return `<div style="margin-bottom:10px"> 12077 <div style="display:flex;justify-content:space-between;margin-bottom:3px"> 12078 <span style="font-size:13px">${r.source}</span> 12079 <span style="font-size:13px;color:var(--gray5)">${r.sessions} (${pct}%)</span> 12080 </div> 12081 <div style="background:var(--border);border-radius:4px;height:8px"> 12082 <div style="background:${colors[i%colors.length]};height:8px;border-radius:4px;width:${pct}%"></div> 12083 </div> 12084 </div>`; 12085 }).join(''); 12086 el.innerHTML = `<h2 style="margin:0 0 20px;font-size:18px">ð¦ ××§×ר×ת ×ª× ××¢×</h2><div style="max-width:600px">${html||'<div style="color:var(--gray5)">××× × ×ª×× ×× ×¢××××</div>'}</div>`; 12087 }).catch(()=>{ el.innerHTML='<div style="color:#f87171;padding:20px">ש××××.</div>'; }); 12088} 12089 12090// ââ Geo Map ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 12091function loadGeoMap() { 12092 const el = document.getElementById('view-geo-map'); 12093 if (!el) return; 12094 el.innerHTML = '<div style="color:var(--gray5);padding:20px">×××¢×...</div>'; 12095 fetch('/api/admin/geo').then(r=>r.json()).then(res=>{ 12096 const rows = res.data || []; 12097 if (!rows.length) { el.innerHTML='<div style="padding:40px;text-align:center;color:var(--gray5)">××× × ×ª×× × ×××§×× ×¢××××</div>'; return; } 12098 const flag = c => ({IL:'ð®ð±',US:'ðºð¸',GB:'ð¬ð§',DE:'ð©ðª',FR:'ð«ð·',UA:'ðºð¦',RU:'ð·ðº',TR:'ð¹ð·',AU:'ð¦ðº',CA:'ð¨ð¦'}[c]||'ð'); 12099 const html = rows.map(r=>` 12100 <tr> 12101 <td style="padding:6px 12px">${flag(r.country_code)} ${r.city||'â'}</td> 12102 <td style="padding:6px 12px">${r.country||'â'}</td> 12103 <td style="padding:6px 12px;text-align:center;font-weight:600">${r.devices}</td> 12104 <td style="padding:6px 12px;text-align:center">${r.sessions}</td> 12105 </tr>`).join(''); 12106 el.innerHTML = ` 12107 <h2 style="margin:0 0 20px;font-size:18px">ðºï¸ ×××××רפ××</h2> 12108 <table style="width:100%;border-collapse:collapse;font-size:13px"> 12109 <thead><tr style="color:var(--gray5);font-size:12px;text-align:right"> 12110 <th style="padding:6px 12px">×¢×ר</th><th style="padding:6px 12px">×××× ×</th> 12111 <th style="padding:6px 12px;text-align:center">××ש×ר××</th><th style="padding:6px 12px;text-align:center">×¡×©× ××</th> 12112 </tr></thead> 12113 <tbody>${html}</tbody> 12114 </table>`; 12115 }).catch(()=>{ el.innerHTML='<div style="color:#f87171;padding:20px">ש××××.</div>'; }); 12116} 12117 12118// ââ Device Stats âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 12119function loadDeviceStats() { 12120 const el = document.getElementById('view-device-stats'); 12121 if (!el) return; 12122 el.innerHTML = '<div style="color:var(--gray5);padding:20px">×××¢×...</div>'; 12123 fetch('/api/admin/device-stats').then(r=>r.json()).then(data=>{ 12124 const section = (title, rows) => { 12125 if (!rows.length) return ''; 12126 const total = rows.reduce((s,r)=>s+r.sessions,0)||1; 12127 const colors = ['#3b82f6','#10b981','#f59e0b','#ef4444','#8b5cf6','#ec4899']; 12128 return `<div style="background:var(--card-bg);border-radius:10px;padding:16px;min-width:220px"> 12129 <div style="font-weight:600;margin-bottom:12px;color:var(--accent)">${title}</div> 12130 ${rows.slice(0,8).map((r,i)=>{
12131 const pct=Math.round(r.sessions/total*100); 12132 return `<div style="margin-bottom:8px"> 12133 <div style="display:flex;justify-content:space-between;margin-bottom:2px;font-size:12px"> 12134 <span>${r.name}</span><span style="color:var(--gray5)">${r.sessions} (${pct}%)</span> 12135 </div> 12136 <div style="background:var(--border);border-radius:3px;height:6px"> 12137 <div style="background:${colors[i%colors.length]};height:6px;border-radius:3px;width:${pct}%"></div> 12138 </div> 12139 </div>`; 12140 }).join('')} 12141 </div>`; 12142 }; 12143 el.innerHTML = ` 12144 <h2 style="margin:0 0 20px;font-size:18px">ð» ××ש×ר×× ××פ××¤× ××</h2> 12145 <div style="display:flex;flex-wrap:wrap;gap:16px"> 12146 ${section('ð± ס×× ××ש×ר', data.devices||[])} 12147 ${section('ð ×פ×פ×', data.browsers||[])} 12148 ${section('ð¥ï¸ ×ער×ת ×פע××', data.os||[])} 12149 </div>`; 12150 }).catch(()=>{ el.innerHTML='<div style="color:#f87171;padding:20px">ש××××.</div>'; }); 12151} 12152 12153// ââ JS Errors âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 12154function loadJsErrors() { 12155 const el = document.getElementById('view-js-errors'); 12156 if (!el) return; 12157 el.innerHTML = '<div style="color:var(--gray5);padding:20px">×××¢×...</div>'; 12158 fetch('/api/admin/js-errors').then(r=>r.json()).then(res=>{ 12159 const rows = res.data || []; 12160 if (!rows.length) { el.innerHTML='<div style="padding:40px;text-align:center;color:#4ade80">â ××× ×©××××ת JS</div>'; return; } 12161 const html = rows.map(r=>` 12162 <div style="background:var(--card-bg);border-radius:8px;padding:12px;margin-bottom:10px;border-left:3px solid #ef4444"> 12163 <div style="display:flex;justify-content:space-between;margin-bottom:6px"> 12164 <span style="color:#f87171;font-weight:500;font-size:13px">${(r.message||'').slice(0,120)}</span> 12165 <span style="font-size:11px;color:var(--gray5)">${r.ts?new Date(r.ts).toLocaleString('he-IL'):''}</span> 12166 </div> 12167 <div style="font-size:11px;color:var(--gray5);margin-bottom:4px">${r.source||''} ${r.lineno?':'+r.lineno:''}</div> 12168 ${r.stack?`<pre style="font-size:10px;color:var(--gray5);white-space:pre-wrap;margin:4px 0 0;max-height:80px;overflow:hidden">${r.stack.slice(0,400)}</pre>`:''} 12169 <div style="font-size:10px;color:var(--gray5);margin-top:4px">${r.ua?r.ua.slice(0,80):''} ${r.ip?'· '+r.ip:''}</div> 12170 </div>`).join(''); 12171 el.innerHTML = `<h2 style="margin:0 0 16px;font-size:18px">ð ש××××ת JS <span style="font-size:13px;color:var(--gray5);font-weight:400">(${rows.length})</span></h2>${html}`; 12172 }).catch(()=>{ el.innerHTML='<div style="color:#f87171;padding:20px">ש××××.</div>'; }); 12173} 12174 12175// ââ Pipeline Debugger ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 12176let _debugES = null; // active EventSource 12177 12178function initDebugView() { 12179 // focus input when switching to view 12180 setTimeout(() => { 12181 const inp = document.getElementById('debug-query-input'); 12182 if (inp) inp.focus(); 12183 }, 50); 12184} 12185 12186const _DEBUG_COLORS = { 12187 '[DISCOVER]': '#7dd3fc', // sky 12188 '[NORMALIZE]': '#94a3b8', // slate 12189 '[QUERY-EXPAND]': '#7dd3fc', 12190 '[DDG]': '#7dd3fc', 12191 '[INFLUENCER-DDG]':'#7dd3fc', 12192 '[URL-BREAKDOWN]': '#38bdf8', // bright sky 12193 '[FETCH ALL]': '#38bdf8', 12194 '[FETCH OK]': '#4ade80', // green 12195 '[FETCH FAIL]': '#f87171', // red 12196 '[FETCH]': '#64748b', // dim 12197 '[EXTRACT ALL]': '#38bdf8', 12198 '[HAIKU â]': '#fbbf24', // amber 12199 '[HAIKU â]': '#4ade80', 12200 '[FIX-SPLIT]': '#fbbf24', 12201 '[JUNK]': '#f87171', 12202 '[FILTER]': '#fbbf24', 12203 '[SANITIZE-NOTES]':'#fbbf24', 12204 '[TRANSLATE]': '#7dd3fc', 12205 '[COUPON]': '#00e87a', // accent green bold 12206 '[EXTRACT DONE]': '#4ade80', 12207 '[SAVED]': '#00e87a', 12208 '[DUPLICATE]': '#64748b', 12209 'WARNING': '#fbbf24', 12210 'ERROR': '#f87171', 12211}; 12212 12213function _debugColorLine(raw) { 12214 // Extract just the message portion (after timestamp + level + module) 12215 // Format: "HH:MM:SS LEVEL module message" 12216 const parts = raw.match(/^(\d{2}:\d{2}:\d{2})\s+(\w+)\s+\S+\s+(.*)/s);
12217 const time = parts ? `<span style="color:#475569">${parts[1]}</span>` : ''; 12218 const level = parts ? parts[2] : ''; 12219 const msg = parts ? parts[3] : raw; 12220 12221 let levelColor = '#64748b'; 12222 if (level === 'INFO') levelColor = '#7dd3fc'; 12223 if (level === 'WARNING') levelColor = '#fbbf24'; 12224 if (level === 'ERROR') levelColor = '#f87171'; 12225 if (level === 'DEBUG') levelColor = '#475569'; 12226 12227 const levelSpan = parts ? `<span style="color:${levelColor};min-width:52px;display:inline-block">${level}</span>` : ''; 12228 12229 // Colorize known markers inside the message 12230 let colored = msg; 12231 for (const [marker, color] of Object.entries(_DEBUG_COLORS)) { 12232 if (colored.includes(marker)) { 12233 const isBold = ['[COUPON]','[SAVED]','[URL-BREAKDOWN]','[FETCH ALL]','[EXTRACT ALL]'].includes(marker); 12234 const style = isBold ? `font-weight:700;color:${color}` : `color:${color}`; 12235 colored = colored.replaceAll(marker, `<span style="${style}">${marker}</span>`); 12236 break; 12237 } 12238 } 12239 12240 return `${time} ${levelSpan} ${colored}`; 12241} 12242 12243function _debugAppend(html) { 12244 const term = document.getElementById('debug-terminal'); 12245 if (!term) return; 12246 const line = document.createElement('div'); 12247 line.innerHTML = html; 12248 term.appendChild(line); 12249 term.scrollTop = term.scrollHeight; 12250} 12251 12252function debugClear() { 12253 if (_debugES) { _debugES.close(); _debugES = null; } 12254 const term = document.getElementById('debug-terminal'); 12255 if (term) term.innerHTML = '<span style="color:#555">// ×××¥ ×רץ ××× ×ר××ת ×ת ×-pipeline ×פע×××</span>'; 12256 const res = document.getElementById('debug-result'); 12257 if (res) res.style.display = 'none'; 12258 const stats = document.getElementById('debug-stats'); 12259 if (stats) stats.innerHTML = ''; 12260 const sources = document.getElementById('debug-sources'); 12261 if (sources) sources.innerHTML = ''; 12262 const tbody = document.getElementById('debug-table-body'); 12263 if (tbody) tbody.innerHTML = ''; 12264 const st = document.getElementById('debug-status'); 12265 if (st) st.textContent = ''; 12266 const btn = document.getElementById('debug-run-btn'); 12267 if (btn) { btn.textContent = 'â¶ ×רץ'; btn.disabled = false; } 12268} 12269 12270function debugRun() { 12271 const query = (document.getElementById('debug-query-input')?.value || '').trim(); 12272 if (!query) return; 12273 12274 // Close any existing stream 12275 if (_debugES) { _debugES.close(); _debugES = null; } 12276 12277 // Clear terminal 12278 const term = document.getElementById('debug-terminal'); 12279 if (term) term.innerHTML = ''; 12280 const res = document.getElementById('debug-result'); 12281 if (res) res.style.display = 'none'; 12282 12283 const btn = document.getElementById('debug-run-btn'); 12284 const st = document.getElementById('debug-status'); 12285 if (btn) { btn.textContent = 'Ⳡרץ...'; btn.disabled = true; } 12286 if (st) st.textContent = '×ת××ר...'; 12287 12288 _debugAppend(`<span style="color:#00e87a;font-weight:700">â¶ ×ר××¥ discover("${query}")</span>`); 12289 _debugAppend(''); 12290 12291 _debugES = new EventSource(`/api/admin/debug/run?query=${encodeURIComponent(query)}`); 12292 12293 _debugES.onmessage = (e) => { 12294 let payload; 12295 try { payload = JSON.parse(e.data); } catch { return; } 12296 12297 if (payload.type === 'log') { 12298 _debugAppend(_debugColorLine(payload.line)); 12299 } else if (payload.type === 'done') { 12300 _debugAppend(''); 12301 _debugAppend(`<span style="color:#00e87a;font-weight:700">â ס××× â ${payload.found} ×§×פ×× ×× ×-${payload.urls} ×¢×××××</span>`); 12302 _showDebugResult(payload.coupons || [], payload.urls); 12303 if (btn) { btn.textContent = 'â¶ ×רץ'; btn.disabled = false; } 12304 if (st) st.textContent = `â ${payload.found} ×§×פ×× ××`; 12305 _debugES.close(); _debugES = null; 12306 } else if (payload.type === 'error') { 12307 _debugAppend(`<span style="color:#f87171">â ש××××: ${payload.message}</span>`); 12308 if (btn) { btn.textContent = 'â¶ ×רץ'; btn.disabled = false; } 12309 if (st) st.textContent = 'â ש××××'; 12310 _debugES.close(); _debugES = null; 12311 } else if (payload.type === 'end') { 12312 _debugES.close(); _debugES = null; 12313 } 12314 }; 12315 12316 _debugES.onerror = () => { 12317 if (_debugES?.readyState === EventSource.CLOSED) return; 12318 _debugAppend('<span style="color:#f87171">â ××××ר × ×תק</span>'); 12319 if (btn) { btn.textContent = 'â¶ ×רץ'; btn.disabled = false; } 12320 if (st) st.textContent = ''; 12321 _debugES?.close(); _debugES = null; 12322 }; 12323} 12324 12325function _showDebugResult(coupons, urlsSearched) { 12326 const res = document.getElementById('debug-result'); 12327 if (!res) return; 12328 res.style.display = 'block'; 12329 12330 // ââ Compute stats ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 12331 const total = coupons.length; 12332 const realCodes = coupons.filter(c => c.code && c.code !== 'NO_CODE').length; 12333 const noCodes = coupons.filter(c => !c.code || c.code === 'NO_CODE').length; 12334 const sourceDomains = {}; 12335 coupons.forEach(c => { 12336 const m = (c.source || '').match(/https?:\/\/([^\/]+)/); 12337 if (m) { const d = m[1].replace(/^www\./, ''); sourceDomains[d] = (sourceDomains[d] || 0) + 1; } 12338 }); 12339 12340 // ââ Stats chips âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 12341 const statsEl = document.getElementById('debug-stats'); 12342 const chip = (icon, label, val, color) => 12343 `<div style="display:flex;align-items:center;gap:8px;background:var(--gray2);border:1px solid var(--gray3);border-radius:10px;padding:8px 14px;min-width:100px">
12344 <span style="font-size:18px">${icon}</span> 12345 <div> 12346 <div style="font-size:18px;font-weight:700;color:${color};line-height:1.1">${val}</div> 12347 <div style="font-size:10px;color:var(--gray5);margin-top:1px">${label}</div> 12348 </div> 12349 </div>`; 12350 statsEl.innerHTML = 12351 chip('ð¦', 'ס××´× ×§×פ×× ××', total, '#fff') + 12352 chip('ð', '×§×××× ×××ת×××', realCodes, '#00e87a') + 12353 chip('ð', 'NO_CODE (×××××)', noCodes, '#7dd3fc') + 12354 chip('ð', '×¢××××× × ×¡×¨×§×', urlsSearched||'â', '#94a3b8'); 12355 12356 // ââ Source domain bar chart âââââââââââââââââââââââââââââââââââââââââââââââ 12357 const sourcesEl = document.getElementById('debug-sources'); 12358 const sorted = Object.entries(sourceDomains).sort((a,b)=>b[1]-a[1]).slice(0,12); 12359 if (sorted.length) { 12360 const mx = sorted[0][1]; 12361 sourcesEl.innerHTML = ` 12362 <div style="font-size:11px;font-weight:600;color:var(--gray5);letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">פ×××× ××¤× ××§×ר</div> 12363 <div style="display:flex;flex-direction:column;gap:4px"> 12364 ${sorted.map(([dom,cnt])=>{ 12365 const pct = Math.round(cnt/mx*100); 12366 const col = dom==='t.me'?'#7dd3fc':'#00e87a'; 12367 return `<div style="display:flex;align-items:center;gap:10px;font-size:12px"> 12368 <span style="color:var(--gray5);
12368min-width:200px;text-align:right;direction:ltr">${dom}</span> 12369 <div style="flex:1;background:var(--gray3);border-radius:4px;height:6px;overflow:hidden;max-width:220px"> 12370 <div style="width:${pct}%;height:100%;background:${col};border-radius:4px"></div> 12371 </div> 12372 <span style="color:var(--white);font-weight:600;min-width:20px">${cnt}</span> 12373 </div>`; 12374 }).join('')} 12375 </div>`; 12376 } 12377 12378 // ââ Before / After comparison table ââââââââââââââââââââââââââââââââââââââ 12379 const tableWrap = document.getElementById('debug-table-wrap'); 12380 if (tableWrap) tableWrap.innerHTML = ` 12381 <div style="padding:12px 16px;border-bottom:1px solid var(--gray3);display:flex;align-items:center;gap:16px;flex-wrap:wrap"> 12382 <span style="font-size:12px;font-weight:600;color:var(--gray5)">×§×פ×× ×× ×©× ×צ×× â Haiku vs Python</span> 12383 <span style="display:flex;align-items:center;gap:6px;font-size:11px;color:#fbbf24"> 12384 <span style="width:10px;height:10px;background:#fbbf2433;border:1px solid #fbbf24;border-radius:2px;display:inline-block"></span>Haiku raw 12385 </span> 12386 <span style="display:flex;align-items:center;gap:6px;font-size:11px;color:#00e87a"> 12387 <span style="width:10px;height:10px;background:#00e87a22;border:1px solid #00e87a;border-radius:2px;display:inline-block"></span>Python processed 12388 </span> 12389 </div> 12390 <div style="overflow-x:auto"> 12391 <table style="width:100%;border-collapse:collapse;font-size:12px"> 12392 <thead> 12393 <tr style="background:var(--gray1)"> 12394 <th style="padding:7px 12px;text-align:center;color:var(--gray5);font-weight:500;width:24px">#</th> 12395 <th style="padding:7px 12px;text-align:right;color:var(--gray5);font-weight:500">××§×ר</th> 12396 <th style="padding:7px 12px;text-align:right;color:var(--gray5);font-weight:500">×× ×ת</th> 12397 <th style="padding:7px 12px;text-align:right;color:var(--gray5);font-weight:500">×× ××</th> 12398 <th style="padding:7px 12px;text-align:right;color:var(--gray5);font-weight:500">×ער×ת</th> 12399 <th style="padding:7px 12px;text-align:right;color:var(--gray5);font-weight:500">×תר ××¢×</th> 12400 </tr> 12401 </thead> 12402 <tbody id="debug-table-body"></tbody> 12403 </table> 12404 </div>`; 12405 12406 const tbody = document.getElementById('debug-table-body'); 12407 if (!tbody) return; 12408 12409 const _esc = s => (s||'').replace(/</g,'<').replace(/>/g,'>'); 12410 const _dom = url => { const m=(url||'').match(/https?:\/\/([^\/]+)/); return m?m[1].replace(/^www\./,''):''; }; 12411 const _short = (s, n=36) => s && s.length>n ? s.slice(0,n-1)+'â¦' : (s||''); 12412 12413 // Cell: show haiku value (amber row) and python value (green row) stacked 12414 // If identical â show once in neutral style 12415 const _cell = (raw, final, mono=false) => { 12416 const rStr = _esc(_short(raw, 60)); 12417 const fStr = _esc(_short(final,60)); 12418 const mStyle = mono ? 'font-family:monospace;' : ''; 12419 if (!rStr && !fStr) return `<td style="padding:6px 12px;color:#333">â</td>`; 12420 if (rStr === fStr) 12421 return `<td style="padding:6px 12px;color:var(--gray6);${mStyle}">${fStr||'â'}</td>`; 12422 return `<td style="padding:0 12px"> 12423 <div style="padding:4px 0 3px;color:#fbbf24;${mStyle}opacity:.85">${rStr||'<em style="opacity:.4">null</em>'}</div> 12424 <div style="padding:3px 0 4px;color:#00e87a;${mStyle}">${fStr||'<em style="opacity:.4">null</em>'}</div> 12425 </td>`; 12426 }; 12427 12428 tbody.innerHTML = coupons.map((c, i) => { 12429 const raw = c._raw || {}; 12430 const srcDom = _dom(c.source); 12431 const srcIcon = srcDom.includes('t.me') ? 'ð±' : 'ð'; 12432 const rowBg = i%2===0 ? 'transparent' : 'rgba(255,255,255,.018)'; 12433 12434 // Detect if any field changed (to highlight the row number) 12435 const changed = raw.store !== c.store 12436 || raw.discount !== c.discount 12437 || raw.notes !== c.notes 12438; 12439 const numColor = changed ? '#fbbf24' : 'var(--gray5)'; 12440 12441 return `<tr style="background:${rowBg};border-top:1px solid var(--gray3)"> 12442 <td style="padding:6px 12px;text-align:center;color:${numColor};font-size:11px;font-weight:600">${i+1}</td> 12443 <td style="padding:6px 12px;direction:ltr;white-space:nowrap">
12444 <span style="font-size:11px;color:var(--gray5)">${srcIcon} ${_esc(_short(srcDom,30))}</span> 12445 </td> 12446 ${_cell(raw.store, c.store)} 12447 ${_cell(raw.discount, c.discount)} 12448 ${_cell(raw.notes, c.notes)} 12449 </tr>`; 12450 }).join(''); 12451} 12452</script>
12452 12453<!-- Date range picker popup --> 12454<div id="date-picker-popup" style="display:none;position:fixed;z-index:9999;background:var(--gray2);border:1px solid var(--gray4);border-radius:14px;padding:16px 20px;box-shadow:0 8px 32px rgba(0,0,0,0.6);min-width:240px;direction:rtl"> 12455 <div style="font-size:13px;font-weight:600;color:var(--white);margin-bottom:12px">ð ×××× ×ª×ר××××</div> 12456 <div style="display:grid;gap:10px"> 12457 <div> 12458 <div style="font-size:11px;color:var(--gray5);margin-bottom:4px">×ת×ר××</div> 12459 <input type="date" id="dp-from" style="width:100%;box-sizing:border-box;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:7px 10px;color:var(--white);font-family:'DM Sans',sans-serif;font-size:13px;outline:none"> 12460 </div> 12461 <div> 12462 <div style="font-size:11px;color:var(--gray5);margin-bottom:4px">×¢× ×ª×ר××</div> 12463 <input type="date" id="dp-to" style="width:100%;box-sizing:border-box;background:var(--gray3);border:1px solid var(--gray4);border-radius:8px;padding:7px 10px;color:var(--white);font-family:'DM Sans',sans-serif;font-size:13px;outline:none"> 12464 </div> 12465 </div> 12466 <div style="display:flex;gap:8px;margin-top:14px"> 12467 <button onclick="_applyDatePicker()" style="flex:1;background:var(--accent);color:#000;border:none;border-radius:8px;padding:9px;font-weight:700;cursor:pointer;font-family:'DM Sans',sans-serif;font-size:13px">×××</button> 12468 <button onclick="_closeDatePicker()" style="flex:1;background:var(--gray3);color:var(--gray5);border:none;border-radius:8px;padding:9px;cursor:pointer;font-family:'DM Sans',sans-serif;font-size:13px">×××××</button> 12469 </div> 12470</div> 12471<!-- ââ Reveal modal âââââââââââââââââââââââââââââââââââââââââââââââââââââââ --> 12472<div id="reveal-modal-overlay" onclick="if(event.target===this)closeRevealModal()"> 12473 <div id="reveal-modal"> 12474 <div id="rm-header"> 12475 <button id="rm-close" onclick="closeRevealModal()">Ã</button> 12476 <div id="rm-header-title">×§×פ×× ×× ××</div> 12477 </div> 12478 <div id="rm-body"> 12479 <div id="rm-logo"></div> 12480 <div id="rm-discount"></div> 12481 <div id="rm-newsletter"></div> 12482 <div id="rm-notes"></div> 12483 <div id="rm-code-wrap"> 12484 <div class="cc-code-row"> 12485 <button class="cc-copy" onclick="rmCopyCode()" title="×עתק ×§××"> 12486 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg> 12487 </button> 12488 <div class="cc-code" style="cursor:pointer;user-select:none" onclick="rmCopyCode()"> 12489 <div class="cc-code-revealed visible" id="rm-code-val" style="user-select:none;cursor:pointer"></div> 12490 </div> 12491 </div> 12492 </div> 12493 <div id="rm-go-btn-wrap"> 12494 <button id="rm-go-btn" onclick="rmGoToStore()">××§×× ××עתק, ×שת×ש ××× ×ת ðï¸</button> 12495 </div> 12496 <div id="rm-vote-label">××× ××§×פ×× ×¢×××?</div> 12497 <div id="rm-vote-row"> 12498 <button id="rm-vote-down" class="vote-btn vote-down" onclick="rmVote(-1)"> 12499 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 15v4a3 3 0 003 3l4-9V2H5.72a2 2 0 00-2 1.7l-1.38 9a2 2 0 002 2.3H10z"/><path d="M17 2h2.67A2.31 2.31 0 0122 4v7a2.31 2.31 0 01-2.33 2H17"/></svg> 12500 <span id="rm-vote-down-count">0</span> 12501 </button> 12502 <button id="rm-vote-up" class="vote-btn vote-up" onclick="rmVote(1)"> 12503 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9V5a3 3 0 00-3-3l-4 9v11h11.28a2 2 0 002-1.7l1.38-9a2 2 0 00-2-2.3H14z"/><path d="M7 22H4a2 2 0 01-2-2v-7a2 2 0 012-2h3"/></svg>
12504 <span id="rm-vote-up-count">0</span> 12505 </button> 12506 </div> 12507 </div> 12508 <div id="rm-footer"> 12509 <span id="rm-views">×××¢×...</span> 12510 <a id="rm-more-link" href="#" onclick="trackEvent('more_coupons',_rmStore);_syncAnalyticsToServer(true);closeRevealModal()">â ×¢×× ×§×פ×× ×× ×©× <span id="rm-more-store"></span></a> 12511 </div> 12512 <div id="rm-toast">××§×× ××עתק â</div> 12513 </div> 12514</div> 12515
12516<script> 12517// Auto-open admin panel when accessed via /admin URL 12518if (window.location.pathname === '/admin') { 12519 window.addEventListener('DOMContentLoaded', () => showPage('admin')); 12520} 12521 12522// ââ Pre-prod banner ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 12523// Fetches /api/env â if ENVIRONMENT=preprod, shows yellow top banner. 12524// On production this is instant and the banner stays hidden. 12525(async () => { 12526 try { 12527 const r = await fetch('/api/env'); 12528 const d = await r.json(); 12529 if (d.environment === 'preprod') { 12530 const banner = document.getElementById('preprod-banner'); 12531 if (banner) { 12532 banner.style.display = 'block'; 12533 document.body.style.paddingTop = '34px'; 12534 } 12535 // Show preprod-only nav + footer items (extension, about) 12536 document.querySelectorAll('[data-preprod-only="true"]').forEach(el => { 12537 el.style.display = 'inline'; 12538 }); 12539 } 12540 } catch (_) {} // fail silently â production site must never break 12541})(); 12542</script>
12542 12543 12544<!-- Result modal --> 12545<div id="result-modal" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.6);align-items:center;justify-content:center" onclick="if(event.target===this)_closeResultModal()"> 12546 <div style="background:#1a1a1a;border:1px solid var(--gray3);border-radius:14px;min-width:320px;max-width:480px;padding:28px 28px 20px;position:relative;direction:rtl"> 12547 <button onclick="_closeResultModal()" style="position:absolute;top:12px;left:14px;background:none;border:none;color:var(--gray5);font-size:18px;cursor:pointer;line-height:1">â</button> 12548 <div id="result-modal-icon" style="font-size:28px;margin-bottom:10px"></div> 12549 <div id="result-modal-title" style="font-size:16px;font-weight:700;color:var(--white);margin-bottom:8px"></div> 12550 <div id="result-modal-body" style="font-size:13px;color:var(--gray6);line-height:1.6;white-space:pre-wrap"></div> 12551 <div style="margin-top:20px;text-align:left"> 12552 <button onclick="_closeResultModal()" style="background:var(--accent);color:#000;border:none;border-radius:8px;padding:8px 22px;font-size:13px;font-weight:600;cursor:pointer">××ש×ר</button> 12553 </div> 12554 </div> 12555</div> 12556 12557<!-- Store Coupons Modal (××× ×××××× â ××¦× ×§×פ×× ××) --> 12558<div id="nd-coupons-modal" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.75);align-items:flex-start;justify-content:center;padding-top:48px;overflow-y:auto" onclick="if(event.target===this)_ndCloseCouponsModal()"> 12559 <div style="background:#1a1a1a;border:1px solid var(--gray3);border-radius:14px;width:min(720px,96vw);padding:0;position:relative;direction:rtl;margin-bottom:48px"> 12560 12561 <!-- Header --> 12562 <div style="padding:22px 24px 16px;border-bottom:1px solid var(--gray3);display:flex;align-items:center;justify-content:space-between"> 12563 <div> 12564 <div id="ndcm-title" style="font-size:17px;font-weight:700;color:var(--white)"></div> 12565 <div id="ndcm-subtitle" style="font-size:12px;color:var(--gray5);margin-top:3px"></div> 12566 </div> 12567 <button onclick="_ndCloseCouponsModal()" style="background:none;border:none;color:var(--gray5);font-size:20px;cursor:pointer;line-height:1;padding:4px 8px">â</button> 12568 </div> 12569 12570 <!-- Coupons list --> 12571 <div id="ndcm-coupons" style="padding:16px 24px;max-height:340px;overflow-y:auto"></div> 12572 12573 <!-- Assign to store section --> 12574 <div style="padding:16px 24px 20px;border-top:1px solid var(--gray3);background:#111;border-radius:0 0 14px 14px"> 12575 <div style="font-size:12px;color:var(--gray5);margin-bottom:10px;font-weight:600">××¢×ר ×§×פ×× ×× ××× ×ת ×§×××ת ××××§ ×× ×ת ××:</div> 12576 <div style="display:flex;gap:8px;align-items:stretch"> 12577 <div style="flex:1;position:relative"> 12578 <input id="ndcm-search" type="text" placeholder="×פש ×× ×ת ××¢×..." dir="rtl" oninput="_ndcmFilter()" 12579 style="width:100%;background:var(--gray2);border:1px solid var(--gray3);border-radius:8px;padding:8px 12px;font-size:13px;color:var(--white);outline:none;box-sizing:border-box"> 12580 <div id="ndcm-dropdown" style="display:none;position:absolute;top:100%;right:0;left:0;background:#1a1a1a;border:1px solid var(--gray3);border-radius:0 0 8px 8px;max-height:180px;overflow-y:auto;z-index:10"></div> 12581 </div> 12582 <button id="ndcm-confirm-btn" onclick="_ndcmConfirmMove()" disabled 12583 style="padding:8px 18px;background:#7f1d1d;color:#fca5a5;border:1px solid #991b1b;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;opacity:0.45"> 12584 ××¢×ר ××××§ âµ 12585 </button> 12586 </div> 12587 <div id="ndcm-target-preview" style="margin-top:8px;font-size:12px;color:var(--gray5);min-height:16px"></div> 12588 </div> 12589 12590 </div> 12591</div> 12592
12593<script src="/accessibility-widget.js"></script>
vendor: 1 bytes, line 12593
12593
12594<script src="https://website-widgets.pages.dev/dist/sienna.min.js" defer></script>
12594 12595 12596<!-- Cookie Consent Banner --> 12597<style> 12598#cookie-banner{display:none;position:fixed;bottom:0;right:0;left:0;z-index:99999;background:#1a1a2e;border-top:1px solid #2d2d4e;padding:16px 24px;flex-direction:row;align-items:center;justify-content:center;gap:16px;font-family:inherit;direction:rtl;text-align:center} 12599@media(max-width:600px){#cookie-banner{flex-direction:column}} 12600</style> 12601<div id="cookie-banner"> 12602 <p style="margin:0;font-size:13px;color:#c8c8d8;"> 12603 ××תר ×שת×ש ××¢××××ת (Cookies) ×צ××¨× ×¤×¨×¡×× ××× ××××קס (Google AdSense ×-Google Analytics). 12604 <a href="/privacy.html" style="color:#7c8cf8;text-decoration:underline;margin-right:4px">×××× ××ת פר×××ת</a> 12605 </p> 12606 <div style="display:flex;gap:10px;flex-shrink:0;justify-content:center"> 12607 <button onclick="setCookieConsent('denied')" style="padding:8px 18px;border-radius:8px;border:1px solid #3d3d5e;background:transparent;color:#a0a0b8;font-size:13px;cursor:pointer">×××××</button> 12608 <button onclick="setCookieConsent('granted')" style="padding:8px 18px;border-radius:8px;border:none;background:#7c8cf8;color:#fff;font-size:13px;cursor:pointer;font-weight:600">××ש×ר</button> 12609 </div> 12610</div>
12611<script> 12612(function(){ 12613 var consent = localStorage.getItem('couponai_cookie_consent'); 12614 var ts = parseInt(localStorage.getItem('couponai_cookie_consent_ts') || '0', 10); 12615 var age = Date.now() - ts; 12616 var days90 = 90 * 24 * 60 * 60 * 1000; 12617 // Show banner if: never answered, or answered "denied" more than 90 days ago 12618 if (!consent || (consent === 'denied' && age > days90)) { 12619 var b = document.getElementById('cookie-banner'); 12620 if (b) b.style.display = 'flex'; 12621 } 12622})(); 12623function setCookieConsent(val) { 12624 localStorage.setItem('couponai_cookie_consent', val); 12625 localStorage.setItem('couponai_cookie_consent_ts', Date.now().toString()); 12626 var b = document.getElementById('cookie-banner'); 12627 if (b) b.style.display = 'none'; 12628 gtag('consent', 'update', { 12629 ad_storage: val, 12630 analytics_storage: val, 12631 ad_user_data: val, 12632 ad_personalization: val 12633 }); 12634} 12635</script>
vendor: 1 bytes, line 12635
12635
12636<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"53ec717f4f9944a1b853521eee6f2786","r":1,"spa":2}' crossorigin="anonymous"></script>
12636 12637</body> 12638</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.