Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width,initial-scale=1.0"/> | |
| <title>EmailBlast Pro</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Clash+Display:wght@400;500;600;700&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| :root{ | |
| --bg:#06060a; | |
| --s1:#0d0d14; | |
| --s2:#12121c; | |
| --border:#1c1c2b; | |
| --border2:#252538; | |
| --y:#d4f542; | |
| --b:#42c8f5; | |
| --o:#f57842; | |
| --g:#42f59a; | |
| --r:#f54242; | |
| --text:#ddddf0; | |
| --muted:#4a4a6a; | |
| --muted2:#6a6a8a; | |
| } | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| html{scroll-behavior:smooth} | |
| body{ | |
| background:var(--bg); | |
| color:var(--text); | |
| font-family:'JetBrains Mono',monospace; | |
| min-height:100vh; | |
| font-size:14px; | |
| } | |
| /* scanline overlay */ | |
| body::after{ | |
| content:''; | |
| position:fixed;inset:0; | |
| background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.03) 2px,rgba(0,0,0,0.03) 4px); | |
| pointer-events:none;z-index:999; | |
| } | |
| /* dot grid */ | |
| body::before{ | |
| content:''; | |
| position:fixed;inset:0; | |
| background-image:radial-gradient(circle,rgba(212,245,66,0.06) 1px,transparent 1px); | |
| background-size:28px 28px; | |
| pointer-events:none;z-index:0; | |
| } | |
| .wrap{ | |
| position:relative;z-index:1; | |
| max-width:1000px; | |
| margin:0 auto; | |
| padding:0 20px 100px; | |
| } | |
| /* ββ NAV ββ */ | |
| nav{ | |
| display:flex; | |
| align-items:center; | |
| justify-content:space-between; | |
| padding:24px 0 20px; | |
| border-bottom:1px solid var(--border); | |
| margin-bottom:52px; | |
| } | |
| .wordmark{ | |
| font-family:'Clash Display',sans-serif; | |
| font-size:22px; | |
| font-weight:700; | |
| letter-spacing:-0.04em; | |
| color:#fff; | |
| } | |
| .wordmark em{color:var(--y);font-style:normal} | |
| .nav-right{display:flex;align-items:center;gap:16px} | |
| .badge{ | |
| display:inline-flex;align-items:center;gap:7px; | |
| border:1px solid var(--border2); | |
| border-radius:6px; | |
| padding:5px 12px; | |
| font-size:11px; | |
| color:var(--muted2); | |
| letter-spacing:0.05em; | |
| } | |
| .badge .dot{ | |
| width:6px;height:6px;border-radius:50%; | |
| background:var(--g); | |
| animation:blink 2s infinite; | |
| } | |
| @keyframes blink{0%,100%{opacity:1}50%{opacity:.3}} | |
| /* ββ HERO ββ */ | |
| .hero{ | |
| margin-bottom:56px; | |
| display:grid; | |
| grid-template-columns:1fr auto; | |
| gap:24px; | |
| align-items:start; | |
| } | |
| .hero-left h1{ | |
| font-family:'Clash Display',sans-serif; | |
| font-size:clamp(2.4rem,5vw,4rem); | |
| font-weight:700; | |
| letter-spacing:-0.04em; | |
| line-height:1.05; | |
| color:#fff; | |
| margin-bottom:16px; | |
| } | |
| .hero-left h1 .hi{color:var(--y)} | |
| .hero-left p{ | |
| color:var(--muted2); | |
| font-size:13px; | |
| line-height:1.7; | |
| max-width:480px; | |
| } | |
| .hero-stats{ | |
| display:flex; | |
| flex-direction:column; | |
| gap:10px; | |
| } | |
| .stat-box{ | |
| background:var(--s1); | |
| border:1px solid var(--border); | |
| border-radius:10px; | |
| padding:14px 20px; | |
| text-align:right; | |
| min-width:140px; | |
| } | |
| .stat-box .n{ | |
| font-family:'Clash Display',sans-serif; | |
| font-size:24px;font-weight:700; | |
| color:var(--y); | |
| line-height:1; | |
| } | |
| .stat-box .l{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em;margin-top:4px} | |
| /* ββ FEATURE STRIP ββ */ | |
| .features{ | |
| display:flex;flex-wrap:wrap;gap:8px; | |
| margin-bottom:52px; | |
| } | |
| .feat{ | |
| border:1px solid var(--border); | |
| border-radius:6px; | |
| padding:6px 14px; | |
| font-size:11px; | |
| color:var(--muted2); | |
| letter-spacing:0.04em; | |
| background:var(--s1); | |
| transition:border-color .2s,color .2s; | |
| } | |
| .feat:hover{border-color:var(--y);color:var(--y)} | |
| .feat b{color:var(--y);font-weight:500;margin-right:4px} | |
| /* ββ SECTION HEADER ββ */ | |
| .sh{ | |
| font-family:'Clash Display',sans-serif; | |
| font-size:11px;font-weight:600; | |
| letter-spacing:0.12em;text-transform:uppercase; | |
| color:var(--y); | |
| margin-bottom:16px; | |
| display:flex;align-items:center;gap:12px; | |
| } | |
| .sh::after{content:'';flex:1;height:1px;background:var(--border)} | |
| section{margin-bottom:52px} | |
| /* ββ CARD ββ */ | |
| .card{ | |
| background:var(--s1); | |
| border:1px solid var(--border); | |
| border-radius:14px; | |
| padding:28px; | |
| } | |
| /* ββ TABS ββ */ | |
| .tabs{ | |
| display:flex;gap:4px; | |
| background:var(--s2); | |
| border:1px solid var(--border); | |
| border-radius:10px; | |
| padding:4px; | |
| margin-bottom:28px; | |
| width:fit-content; | |
| } | |
| .tab{ | |
| padding:8px 18px; | |
| border-radius:7px; | |
| font-size:11px;font-weight:500; | |
| letter-spacing:0.06em;text-transform:uppercase; | |
| cursor:pointer;color:var(--muted2); | |
| transition:all .2s;border:none;background:transparent; | |
| font-family:'JetBrains Mono',monospace; | |
| } | |
| .tab.active{background:var(--y);color:#06060a;font-weight:700} | |
| /* ββ FORM ββ */ | |
| .fg{display:grid;grid-template-columns:1fr 1fr;gap:14px} | |
| .fg .full{grid-column:1/-1} | |
| label{ | |
| display:block; | |
| font-size:10px;letter-spacing:0.08em;text-transform:uppercase; | |
| color:var(--muted);margin-bottom:6px; | |
| } | |
| input,select,textarea{ | |
| width:100%; | |
| background:var(--bg); | |
| border:1px solid var(--border); | |
| border-radius:8px; | |
| color:var(--text); | |
| font-family:'JetBrains Mono',monospace; | |
| font-size:13px; | |
| padding:10px 13px; | |
| outline:none; | |
| transition:border-color .18s; | |
| appearance:none; | |
| } | |
| textarea{resize:vertical;min-height:72px} | |
| input:focus,select:focus,textarea:focus{border-color:var(--y)} | |
| input[type=range]{padding:6px 0;cursor:pointer} | |
| input[type=file]::file-selector-button{ | |
| background:var(--border2);border:none;color:var(--text); | |
| padding:6px 12px;border-radius:5px; | |
| font-family:'JetBrains Mono',monospace;font-size:11px; | |
| cursor:pointer;margin-right:10px; | |
| } | |
| .hint{font-size:10px;color:var(--muted);margin-top:5px;letter-spacing:0.03em} | |
| /* warmup toggle */ | |
| .toggle-row{display:flex;align-items:center;gap:10px;margin-bottom:6px} | |
| .toggle{ | |
| position:relative;width:40px;height:22px; | |
| flex-shrink:0; | |
| } | |
| .toggle input{opacity:0;width:0;height:0} | |
| .slider{ | |
| position:absolute;inset:0;background:var(--border2);border-radius:999px; | |
| cursor:pointer;transition:.2s; | |
| } | |
| .slider:before{ | |
| content:'';position:absolute; | |
| height:16px;width:16px;left:3px;bottom:3px; | |
| background:#fff;border-radius:50%;transition:.2s; | |
| } | |
| .toggle input:checked+.slider{background:var(--y)} | |
| .toggle input:checked+.slider:before{transform:translateX(18px);background:#06060a} | |
| /* template preview */ | |
| .tpl-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:24px} | |
| .tpl{ | |
| border:2px solid var(--border); | |
| border-radius:10px;padding:16px 14px; | |
| cursor:pointer;transition:all .18s; | |
| text-align:center; | |
| } | |
| .tpl:hover{border-color:var(--border2)} | |
| .tpl.sel{border-color:var(--y);background:rgba(212,245,66,0.04)} | |
| .tpl .ico{font-size:22px;margin-bottom:8px} | |
| .tpl .name{font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:var(--text)} | |
| .tpl .desc{font-size:10px;color:var(--muted);margin-top:4px} | |
| /* ββ SMTP accounts ββ */ | |
| .smtp-list{display:flex;flex-direction:column;gap:10px;margin-bottom:14px} | |
| .smtp-row{ | |
| display:grid;grid-template-columns:1fr 1fr auto; | |
| gap:10px;align-items:end; | |
| } | |
| .remove-btn{ | |
| background:transparent;border:1px solid var(--border);border-radius:7px; | |
| color:var(--muted);padding:10px 12px;cursor:pointer;transition:.15s; | |
| font-family:'JetBrains Mono',monospace;font-size:13px; | |
| } | |
| .remove-btn:hover{border-color:var(--r);color:var(--r)} | |
| /* ββ BUTTON ββ */ | |
| .btn{ | |
| display:inline-flex;align-items:center;gap:8px; | |
| background:var(--y);color:#06060a; | |
| font-family:'Clash Display',sans-serif; | |
| font-size:15px;font-weight:700; | |
| letter-spacing:-0.01em; | |
| padding:13px 28px;border-radius:9px;border:none;cursor:pointer; | |
| transition:transform .15s,box-shadow .15s; | |
| } | |
| .btn:hover{transform:translateY(-1px);box-shadow:0 6px 28px rgba(212,245,66,.22)} | |
| .btn:active{transform:none} | |
| .btn:disabled{opacity:.35;cursor:not-allowed;transform:none;box-shadow:none} | |
| .btn-ghost{ | |
| background:transparent;border:1px solid var(--border2);color:var(--muted2); | |
| font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:400; | |
| padding:9px 16px;border-radius:8px;cursor:pointer; | |
| transition:.15s;letter-spacing:0.03em; | |
| } | |
| .btn-ghost:hover{border-color:var(--y);color:var(--y)} | |
| .btn-row{margin-top:24px;display:flex;gap:10px;align-items:center;flex-wrap:wrap} | |
| /* ββ PROGRESS ββ */ | |
| #pw{display:none;margin-top:28px} | |
| .pb-bg{background:var(--border);border-radius:999px;height:5px;overflow:hidden;margin-bottom:12px} | |
| .pb-fill{height:100%;background:var(--y);border-radius:999px;width:0;transition:width .5s} | |
| .pm{display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:12px} | |
| /* analytics row */ | |
| .analytics-row{ | |
| display:grid;grid-template-columns:repeat(4,1fr);gap:10px; | |
| margin-bottom:16px; | |
| } | |
| .ana-box{ | |
| background:var(--s2);border:1px solid var(--border);border-radius:9px; | |
| padding:12px 14px;text-align:center; | |
| } | |
| .ana-box .an{ | |
| font-family:'Clash Display',sans-serif;font-size:22px;font-weight:700; | |
| color:var(--y);line-height:1; | |
| } | |
| .ana-box .al{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:0.08em;margin-top:3px} | |
| #log{ | |
| background:var(--bg);border:1px solid var(--border);border-radius:8px; | |
| padding:14px;font-size:11px;height:200px;overflow-y:auto;line-height:1.8; | |
| } | |
| #log .ok{color:var(--g)} | |
| #log .er{color:var(--r)} | |
| #log .in{color:var(--b)} | |
| #log .wm{color:var(--o)} | |
| /* ββ ENDPOINT DOCS ββ */ | |
| .ep{border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-bottom:10px} | |
| .eph{ | |
| display:flex;align-items:center;gap:12px; | |
| padding:13px 17px;cursor:pointer;background:var(--s1); | |
| transition:background .15s; | |
| } | |
| .eph:hover{background:var(--s2)} | |
| .method{ | |
| font-family:'Clash Display',sans-serif;font-size:10px;font-weight:700; | |
| padding:3px 9px;border-radius:4px;letter-spacing:0.06em; | |
| } | |
| .GET{background:rgba(66,200,245,.1);color:var(--b)} | |
| .POST{background:rgba(212,245,66,.1);color:var(--y)} | |
| .ep-path{font-size:13px;color:var(--text);flex:1} | |
| .ep-desc{font-size:11px;color:var(--muted)} | |
| .epb{display:none;padding:16px 17px;border-top:1px solid var(--border);background:var(--bg)} | |
| .epb.open{display:block} | |
| pre{ | |
| background:#040408;border:1px solid var(--border);border-radius:7px; | |
| padding:14px;font-size:11px;overflow-x:auto;color:var(--b);line-height:1.7; | |
| } | |
| /* ββ CSV FORMAT ββ */ | |
| .csv-table{width:100%;border-collapse:collapse;font-size:12px} | |
| .csv-table th{text-align:left;padding:8px 12px;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:var(--y);border-bottom:1px solid var(--border)} | |
| .csv-table td{padding:8px 12px;border-bottom:1px solid var(--border);color:var(--muted2)} | |
| .csv-table td.req{color:var(--text)} | |
| .csv-table tr:last-child td{border-bottom:none} | |
| /* ββ FOOTER ββ */ | |
| footer{ | |
| border-top:1px solid var(--border);padding-top:24px;margin-top:48px; | |
| display:flex;flex-wrap:wrap;gap:16px;justify-content:space-between; | |
| font-size:11px;color:var(--muted); | |
| } | |
| footer a{color:var(--y);text-decoration:none} | |
| #back-link:hover{border-color:var(--y)!important;color:var(--y)!important} | |
| /* animations */ | |
| .fade{animation:fadeup .5s ease both} | |
| .fade:nth-child(2){animation-delay:.08s} | |
| .fade:nth-child(3){animation-delay:.14s} | |
| .fade:nth-child(4){animation-delay:.2s} | |
| @keyframes fadeup{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}} | |
| /* ββ JOB TAG ββ */ | |
| #jobtag{font-size:11px;color:var(--muted);letter-spacing:.04em;display:none} | |
| @media(max-width:650px){ | |
| .fg{grid-template-columns:1fr} | |
| .fg .full{grid-column:1} | |
| .hero{grid-template-columns:1fr} | |
| .tpl-grid{grid-template-columns:1fr} | |
| .analytics-row{grid-template-columns:1fr 1fr} | |
| .smtp-row{grid-template-columns:1fr 1fr;}.smtp-row button{grid-column:1/-1} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <!-- NAV --> | |
| <nav class="fade"> | |
| <div class="wordmark">Email<em>Blast</em> <span style="color:var(--muted);font-size:13px;font-weight:400">Pro</span></div> | |
| <div class="nav-right"> | |
| <div class="badge"><span class="dot"></span>Running Β· v2.0</div> | |
| <a href="https://frnklnwrld-emailblast-pro.hf.space/docs" style="font-size:11px;color:var(--muted2);text-decoration:none;border:1px solid var(--border);padding:5px 12px;border-radius:6px">Swagger β</a> | |
| </div> | |
| </nav> | |
| <!-- HERO --> | |
| <div class="hero fade"> | |
| <div class="hero-left"> | |
| <h1>Bulk Email.<br><span class="hi">Fully Branded.</span></h1> | |
| <p>Upload your list, configure branding per row, pick a template, and fire β with live open tracking, warm-up scheduling, multi-account round-robin, and webhook callbacks.</p> | |
| </div> | |
| <div class="hero-stats"> | |
| <div class="stat-box"><div class="n">3</div><div class="l">Templates</div></div> | |
| <div class="stat-box"><div class="n">β</div><div class="l">SMTP accounts</div></div> | |
| <div class="stat-box"><div class="n">π‘</div><div class="l">Open tracking</div></div> | |
| </div> | |
| </div> | |
| <!-- FEATURES --> | |
| <div class="features fade"> | |
| <div class="feat"><b>π·</b>Per-row FromName</div> | |
| <div class="feat"><b>β©</b>Reply-To override</div> | |
| <div class="feat"><b>π¨</b>HTML templates</div> | |
| <div class="feat"><b>π</b>Open tracking pixel</div> | |
| <div class="feat"><b>π</b>Round-robin SMTP</div> | |
| <div class="feat"><b>π‘</b>Warm-up mode</div> | |
| <div class="feat"><b>π‘</b>Webhook callback</div> | |
| <div class="feat"><b>π</b>CTA buttons</div> | |
| <div class="feat"><b>π«</b>Unsubscribe links</div> | |
| <div class="feat"><b>π</b>Excel + CSV</div> | |
| </div> | |
| <!-- BLAST FORM --> | |
| <section class="fade"> | |
| <div class="sh">Launch Campaign</div> | |
| <div class="card"> | |
| <!-- TABS --> | |
| <div class="tabs"> | |
| <button class="tab active" onclick="showTab('setup',this)">Setup</button> | |
| <button class="tab" onclick="showTab('template',this)">Template</button> | |
| <button class="tab" onclick="showTab('smtp',this)">SMTP</button> | |
| <button class="tab" onclick="showTab('advanced',this)">Advanced</button> | |
| </div> | |
| <!-- TAB: SETUP --> | |
| <div id="tab-setup"> | |
| <div class="fg"> | |
| <div class="full"> | |
| <label>Recipient file (.xlsx or .csv)</label> | |
| <input type="file" id="file" accept=".xlsx,.csv"/> | |
| <div class="hint">Required column: Email. Optional: FromName, ReplyTo, BodyExtra, CTAUrl</div> | |
| </div> | |
| <div> | |
| <label>Email subject</label> | |
| <input type="text" id="subject" value="Hello from Your Business"/> | |
| </div> | |
| <div> | |
| <label>Default From name</label> | |
| <input type="text" id="from_name" value="Your Business"/> | |
| </div> | |
| <div class="full"> | |
| <label>Email body (default)</label> | |
| <textarea id="body_extra">We'd love to share our latest updates with you. Thank you for being part of our community.</textarea> | |
| <div class="hint">Can be overridden per-row with a BodyExtra column in your file.</div> | |
| </div> | |
| <div class="full"> | |
| <label>Sender address (footer)</label> | |
| <input type="text" id="sender_address" value="Your Business, City, Country"/> | |
| </div> | |
| <div> | |
| <label>Max emails to send</label> | |
| <input type="number" id="max_emails" value="10" min="1" max="400"/> | |
| </div> | |
| <div> | |
| <label>Delay between emails (sec)</label> | |
| <input type="number" id="sleep" value="5" min="1" max="300"/> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TAB: TEMPLATE --> | |
| <div id="tab-template" style="display:none"> | |
| <div class="tpl-grid"> | |
| <div class="tpl sel" onclick="selectTpl('plain',this)"> | |
| <div class="ico">π</div> | |
| <div class="name">Plain Text</div> | |
| <div class="desc">Simple, high deliverability</div> | |
| </div> | |
| <div class="tpl" onclick="selectTpl('promo',this)"> | |
| <div class="ico">π</div> | |
| <div class="name">Promo</div> | |
| <div class="desc">Dark hero + CTA button</div> | |
| </div> | |
| <div class="tpl" onclick="selectTpl('newsletter',this)"> | |
| <div class="ico">π°</div> | |
| <div class="name">Newsletter</div> | |
| <div class="desc">Editorial serif layout</div> | |
| </div> | |
| </div> | |
| <div class="fg" id="cta-fields"> | |
| <div> | |
| <label>CTA button URL (Promo only)</label> | |
| <input type="url" id="cta_url" placeholder="https://yourwebsite.com"/> | |
| </div> | |
| <div> | |
| <label>CTA button text</label> | |
| <input type="text" id="cta_text" value="Learn More"/> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TAB: SMTP --> | |
| <div id="tab-smtp" style="display:none"> | |
| <p style="font-size:12px;color:var(--muted2);margin-bottom:18px;line-height:1.7"> | |
| Add multiple Gmail accounts for round-robin sending β useful for distributing volume across accounts and avoiding per-account limits. | |
| </p> | |
| <div id="smtp-list" class="smtp-list"> | |
| <div class="smtp-row" id="smtp-primary"> | |
| <div> | |
| <label>Gmail address (primary)</label> | |
| <input type="email" id="gmail_user" placeholder="you@gmail.com"/> | |
| </div> | |
| <div> | |
| <label>App password</label> | |
| <input type="password" id="gmail_pass" placeholder="xxxx xxxx xxxx xxxx"/> | |
| </div> | |
| <div style="padding-top:20px"> | |
| <div style="font-size:10px;color:var(--g);padding:10px 12px;border:1px solid var(--border);border-radius:7px">Primary</div> | |
| </div> | |
| </div> | |
| </div> | |
| <button class="btn-ghost" onclick="addSMTP()">+ Add account</button> | |
| <div class="hint" style="margin-top:10px">Use Gmail App Passwords β never your real password. Generate at myaccount.google.com/security</div> | |
| </div> | |
| <!-- TAB: ADVANCED --> | |
| <div id="tab-advanced" style="display:none"> | |
| <div class="fg"> | |
| <div> | |
| <div class="toggle-row"> | |
| <label class="toggle"> | |
| <input type="checkbox" id="warmup_enabled" onchange="toggleWarmup()"/> | |
| <span class="slider"></span> | |
| </label> | |
| <span style="font-size:12px;color:var(--text)">Enable warm-up mode</span> | |
| </div> | |
| <div class="hint">Gradually ramps up send volume per day to build sender reputation.</div> | |
| </div> | |
| <div id="warmup-day-wrap" style="opacity:.35;pointer-events:none"> | |
| <label>Warm-up day number</label> | |
| <input type="number" id="warmup_day" value="1" min="1" max="11"/> | |
| <div class="hint">Day 1β5, Day 2β10, Day 3β20, Day 4β30β¦ up to 400/day at day 11</div> | |
| </div> | |
| <div class="full"> | |
| <label>Webhook callback URL (optional)</label> | |
| <input type="url" id="webhook_url" placeholder="https://your-n8n-or-zapier-hook.com"/> | |
| <div class="hint">POST fired on job completion with job_id, sent count, errors. Connects to n8n, Zapier, Make, etc.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- SEND ROW --> | |
| <div class="btn-row"> | |
| <button class="btn" id="send-btn" onclick="startBlast()">βΆ Launch Blast</button> | |
| <span id="jobtag"></span> | |
| </div> | |
| <!-- PROGRESS --> | |
| <div id="pw"> | |
| <div class="analytics-row" style="margin-top:20px"> | |
| <div class="ana-box"><div class="an" id="a-sent">0</div><div class="al">Sent</div></div> | |
| <div class="ana-box"><div class="an" id="a-total">0</div><div class="al">Total</div></div> | |
| <div class="ana-box"><div class="an" id="a-opens">0</div><div class="al">Opens</div></div> | |
| <div class="ana-box"><div class="an" id="a-rate">β</div><div class="al">Open rate</div></div> | |
| </div> | |
| <div class="pb-bg"><div class="pb-fill" id="pbf"></div></div> | |
| <div class="pm"><span id="pm-text">β</span><span id="pm-status">β</span></div> | |
| <div id="log"></div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- API DOCS --> | |
| <section class="fade"> | |
| <div class="sh">API Endpoints</div> | |
| <div class="ep"> | |
| <div class="eph" onclick="tog(this)"> | |
| <span class="method GET">GET</span> | |
| <span class="ep-path">/health</span> | |
| <span class="ep-desc">Status + feature list</span> | |
| </div> | |
| <div class="epb"> | |
| <pre>{ | |
| "ok": true, | |
| "version": "2.0.0", | |
| "features": ["html-templates","open-tracking","round-robin-smtp","warmup-mode","webhooks","per-row-branding"] | |
| }</pre> | |
| </div> | |
| </div> | |
| <div class="ep"> | |
| <div class="eph" onclick="tog(this)"> | |
| <span class="method POST">POST</span> | |
| <span class="ep-path">/send</span> | |
| <span class="ep-desc">Upload + launch blast job</span> | |
| </div> | |
| <div class="epb"> | |
| <pre>// Multipart form fields | |
| file .xlsx or .csv (required) | |
| gmail_user string (required) | |
| gmail_pass string (required) | |
| extra_accounts JSON string: [{"user":"<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="661e26010b070f0a4805090b">[email protected]</a>","pass":"xxxx"}] | |
| subject string | |
| default_from_name string | |
| body_extra string | |
| sender_address string | |
| max_emails int | |
| sleep_interval float | |
| template_name plain | promo | newsletter | |
| cta_url string | |
| cta_text string | |
| warmup_day int (0 = disabled) | |
| webhook_url string | |
| // Response | |
| { "job_id": "a1b2c3d4", "total_rows": 120, "smtp_accounts": 2, "template": "promo" }</pre> | |
| </div> | |
| </div> | |
| <div class="ep"> | |
| <div class="eph" onclick="tog(this)"> | |
| <span class="method GET">GET</span> | |
| <span class="ep-path">/status/{job_id}</span> | |
| <span class="ep-desc">Live progress + analytics</span> | |
| </div> | |
| <div class="epb"> | |
| <pre>{ | |
| "job_id": "a1b2c3d4", | |
| "status": "running", | |
| "sent": 24, "total": 100, "errors": 1, | |
| "opens": 7, "open_rate": "29.2%", | |
| "log": ["10:04:01 β <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="8df8fee8ffcde8f5ece0fde1e8a3eee2e0">[email protected]</a> via franklnwrlddβ¦ as 'Ahmed's Shop'", ...] | |
| }</pre> | |
| </div> | |
| </div> | |
| <div class="ep"> | |
| <div class="eph" onclick="tog(this)"> | |
| <span class="method GET">GET</span> | |
| <span class="ep-path">/analytics/{job_id}</span> | |
| <span class="ep-desc">Full open event log</span> | |
| </div> | |
| <div class="epb"> | |
| <pre>{ | |
| "job_id": "a1b2c3d4", | |
| "sent": 100, "opens": 31, "open_rate": "31.0%", | |
| "open_events": [{ "email": "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="146167716654716c75796478713a777b79">[email protected]</a>", "ts": "2025-01-01T10:05:33" }, ...] | |
| }</pre> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- COLUMN GUIDE --> | |
| <section class="fade"> | |
| <div class="sh">File Format Reference</div> | |
| <div class="card"> | |
| <table class="csv-table"> | |
| <tr> | |
| <th>Column</th><th>Required</th><th>Description</th><th>Example</th> | |
| </tr> | |
| <tr> | |
| <td class="req">Email</td><td style="color:var(--r)">Yes</td> | |
| <td>Recipient address</td><td><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="87e4f2f4f3e8eae2f5c7e0eae6eeeba9e4e8ea">[email protected]</a></td> | |
| </tr> | |
| <tr> | |
| <td>FromName</td><td style="color:var(--muted)">Optional</td> | |
| <td>Display name in inbox</td><td>Ahmed's Shop</td> | |
| </tr> | |
| <tr> | |
| <td>ReplyTo</td><td style="color:var(--muted)">Optional</td> | |
| <td>Where replies are directed</td><td><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="62030a0f070622110a0d124c1209">[email protected]</a></td> | |
| </tr> | |
| <tr> | |
| <td>BodyExtra</td><td style="color:var(--muted)">Optional</td> | |
| <td>Per-recipient body text override</td><td>Here's your custom offerβ¦</td> | |
| </tr> | |
| <tr> | |
| <td>CTAUrl</td><td style="color:var(--muted)">Optional</td> | |
| <td>Per-row CTA link (Promo template)</td><td>https://shop.pk/offer</td> | |
| </tr> | |
| </table> | |
| </div> | |
| </section> | |
| <footer class="fade"> | |
| <span>EmailBlast Pro Β· FastAPI Β· Gmail SMTP Β· v2.0.0</span> | |
| <span><a href="https://frnklnwrld-emailblast-pro.hf.space/docs">Swagger</a> Β· <a href="https://frnklnwrld-emailblast-pro.hf.space/health">Health</a> Β· <a href="https://aero-woad.vercel.app">β Vercel site</a></span> | |
| </footer> | |
| </div> | |
| <script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script> | |
| <script> | |
| // ββ API base βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const API_BASE = 'https://frnklnwrld-emailblast-pro.hf.space'; | |
| // ββ Pre-fill credentials from HF Space secrets on load ββββββββββββββββββββ | |
| (async () => { | |
| try { | |
| const r = await fetch(API_BASE + '/config'); | |
| const d = await r.json(); | |
| if (d.gmail_user) { | |
| document.getElementById('gmail_user').value = d.gmail_user; | |
| document.getElementById('gmail_user').style.color = 'var(--muted2)'; | |
| } | |
| if (d.has_pass) { | |
| document.getElementById('gmail_pass').placeholder = 'ββββ ββββ ββββ ββββ (secret loaded)'; | |
| document.getElementById('gmail_pass').style.borderColor = 'var(--g)'; | |
| } | |
| } catch(e) { /* silently ignore */ } | |
| })(); | |
| // ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let selectedTemplate = 'plain'; | |
| let pollTimer = null; | |
| let currentJob = null; | |
| let extraAccounts = []; | |
| // ββ Tabs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showTab(id, el) { | |
| ['setup','template','smtp','advanced'].forEach(t => { | |
| const tabEl = document.getElementById('tab-' + t); | |
| if (tabEl) tabEl.style.display = (t === id) ? 'block' : 'none'; | |
| }); | |
| document.querySelectorAll('.tab').forEach(b => b.classList.remove('active')); | |
| if (el) el.classList.add('active'); | |
| } | |
| // ββ Template select ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function selectTpl(name, el) { | |
| selectedTemplate = name; | |
| document.querySelectorAll('.tpl').forEach(t => t.classList.remove('sel')); | |
| if (el) el.classList.add('sel'); | |
| } | |
| // ββ Warm-up toggle βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function toggleWarmup() { | |
| const en = document.getElementById('warmup_enabled').checked; | |
| const w = document.getElementById('warmup-day-wrap'); | |
| if (w) { | |
| w.style.opacity = en ? '1' : '0.35'; | |
| w.style.pointerEvents = en ? 'auto' : 'none'; | |
| } | |
| } | |
| // ββ SMTP round-robin βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function addSMTP() { | |
| const idx = extraAccounts.length; | |
| extraAccounts.push({user:'',pass:''}); | |
| const row = document.createElement('div'); | |
| row.className = 'smtp-row'; | |
| row.id = 'smtp-extra-' + idx; | |
| row.innerHTML = ` | |
| <div> | |
| <label>Gmail address</label> | |
| <input type="email" id="ex-user-${idx}" placeholder="extra@gmail.com" | |
| oninput="extraAccounts[${idx}].user = this.value"/> | |
| </div> | |
| <div> | |
| <label>App password</label> | |
| <input type="password" id="ex-pass-${idx}" placeholder="xxxx xxxx xxxx xxxx" | |
| oninput="extraAccounts[${idx}].pass = this.value"/> | |
| </div> | |
| <div style="padding-top:20px"> | |
| <button class="remove-btn" onclick="removeSMTP(${idx}, this.closest('.smtp-row'))">β</button> | |
| </div>`; | |
| document.getElementById('smtp-list').appendChild(row); | |
| } | |
| function removeSMTP(idx, row) { | |
| extraAccounts.splice(idx, 1); | |
| row.remove(); | |
| } | |
| // ββ Endpoint accordion βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function tog(header) { | |
| const body = header.nextElementSibling; | |
| if (body) body.classList.toggle('open'); | |
| } | |
| // ββ Helper: log line with color class βββββββββββββββββββββββββββββββββββββ | |
| function logLine(cls, msg) { | |
| const box = document.getElementById('log'); | |
| if (!box) return; | |
| const div = document.createElement('div'); | |
| div.className = cls; | |
| div.textContent = msg; | |
| box.appendChild(div); | |
| box.scrollTop = box.scrollHeight; | |
| } | |
| // ββ Main send ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function startBlast() { | |
| const fileEl = document.getElementById('file'); | |
| const gmail_user = document.getElementById('gmail_user').value.trim(); | |
| const gmail_pass = document.getElementById('gmail_pass').value.trim(); | |
| if (!fileEl.files?.[0]) { | |
| showTab('setup', document.querySelector('.tab')); | |
| alert('Please select a file.'); | |
| return; | |
| } | |
| if (!gmail_user || !gmail_pass) { | |
| showTab('smtp', document.querySelectorAll('.tab')[2]); | |
| alert('Gmail credentials required (SMTP tab).'); | |
| return; | |
| } | |
| const fd = new FormData(); | |
| fd.append('file', fileEl.files[0]); | |
| fd.append('gmail_user', gmail_user); | |
| fd.append('gmail_pass', gmail_pass); | |
| fd.append('extra_accounts', JSON.stringify(extraAccounts.filter(a => a.user && a.pass))); | |
| fd.append('subject', document.getElementById('subject').value); | |
| fd.append('default_from_name', document.getElementById('from_name').value); | |
| fd.append('body_extra', document.getElementById('body_extra').value); | |
| fd.append('sender_address', document.getElementById('sender_address').value); | |
| fd.append('max_emails', document.getElementById('max_emails').value); | |
| fd.append('sleep_interval', document.getElementById('sleep').value); | |
| fd.append('template_name', selectedTemplate); | |
| fd.append('cta_url', document.getElementById('cta_url').value); | |
| fd.append('cta_text', document.getElementById('cta_text').value); | |
| const warmupOn = document.getElementById('warmup_enabled').checked; | |
| fd.append('warmup_day', warmupOn ? document.getElementById('warmup_day').value : '0'); | |
| fd.append('webhook_url', document.getElementById('webhook_url').value); | |
| const btn = document.getElementById('send-btn'); | |
| btn.disabled = true; | |
| btn.textContent = 'Queuingβ¦'; | |
| try { | |
| const res = await fetch(API_BASE + '/send', { method: 'POST', body: fd }); | |
| const data = await res.json(); | |
| if (!res.ok) throw new Error(data.detail || 'Error queuing job'); | |
| currentJob = data.job_id; | |
| const tag = document.getElementById('jobtag'); | |
| tag.textContent = `Job ${currentJob} Β· ${data.smtp_accounts} SMTP Β· ${data.template} template`; | |
| tag.style.display = 'inline'; | |
| document.getElementById('pw').style.display = 'block'; | |
| document.getElementById('a-total').textContent = data.total_rows; | |
| logLine('in', `Job ${currentJob} queued β ${data.total_rows} rows Β· ${data.smtp_accounts} SMTP account(s)`); | |
| btn.textContent = 'Runningβ¦'; | |
| pollTimer = setInterval(poll, 2000); | |
| } catch (e) { | |
| alert('Error: ' + e.message); | |
| btn.disabled = false; | |
| btn.textContent = 'βΆ Launch Blast'; | |
| } | |
| } | |
| // ββ Poll status ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function poll() { | |
| if (!currentJob) return; | |
| try { | |
| const r = await fetch(API_BASE + '/status/' + currentJob); | |
| const d = await r.json(); | |
| const pct = d.total > 0 ? (d.sent / d.total) * 100 : 0; | |
| document.getElementById('pbf').style.width = pct + '%'; | |
| document.getElementById('pm-text').textContent = `${d.sent} / ${d.total}`; | |
| document.getElementById('pm-status').textContent = d.status; | |
| document.getElementById('a-sent').textContent = d.sent; | |
| document.getElementById('a-opens').textContent = d.opens || 0; | |
| document.getElementById('a-rate').textContent = d.open_rate || 'β'; | |
| const box = document.getElementById('log'); | |
| if (box) { | |
| box.innerHTML = ''; // clear or append β here we replace for simplicity | |
| for (const ln of d.log || []) { | |
| let cls = 'in'; | |
| if (ln.includes('β')) cls = 'ok'; | |
| else if (ln.includes('β') || ln.includes('failed')) cls = 'er'; | |
| else if (ln.includes('warm-up') || ln.includes('capped')) cls = 'wm'; | |
| const div = document.createElement('div'); | |
| div.className = cls; | |
| div.textContent = ln; | |
| box.appendChild(div); | |
| } | |
| box.scrollTop = box.scrollHeight; | |
| } | |
| } catch (e) { | |
| console.error('Poll error:', e); | |
| } | |
| } | |
| </script> |