1import { svgIcon } from '../utils.js'; 2import { decorateLocalPrices } from '../currency.js'; 3import { track } from '../analytics.js'; 4 5const overlay = () => document.getElementById('modal-overlay'); 6 7export function openModal({ title, body, footer, onClose } = {}) { 8 const el = overlay(); 9 el.innerHTML = ` 10 <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title"> 11 <div class="modal-header"> 12 <h2 class="modal-title" id="modal-title">${title || ''}</h2> 13 <button class="modal-close" id="modal-close-btn" aria-label="Close">${svgIcon('x', 'currentColor', 16)}</button> 14 </div> 15 <div class="modal-body">${body || ''}</div> 16 ${footer ? `<div class="modal-footer">${footer}</div>` : ''} 17 </div> 18 `; 19 el.classList.remove('hidden'); 20 21 const close = () => { 22 el.classList.add('hidden'); 23 el.innerHTML = ''; 24 if (onClose) onClose(); 25 }; 26 27 document.getElementById('modal-close-btn').addEventListener('click', close); 28 el.addEventListener('click', e => { if (e.target === el) close(); }); 29 document.addEventListener('keydown', function esc(e) { 30 if (e.key === 'Escape') { close(); document.removeEventListener('keydown', esc); } 31 }); 32 33 return { close, el }; 34} 35 36export function closeModal() { 37 const el = overlay(); 38 el.classList.add('hidden'); 39 el.innerHTML = ''; 40} 41 42export async function showUpgradeModal(reason = '') { 43 const { getAccessToken } = await import('../auth.js'); 44 45 track('upgrade_modal.opened', { reason: reason || 'generic', surface: 'dashboard' }); 46 47 const reasonText = { 48 jobs: "You've hit the 5-job free limit.", 49 cover_letter_limit: "You've used your free AI cover letter.", 50 cover_letter_pro_limit: "You've hit your monthly AI cover letter limit.", 51 resume_limit: "You've used your free AI resume score.", 52 resume_pro_limit: "You've hit your monthly AI resume limit.", 53 linkedin_limit: "You've used your free LinkedIn analysis.", 54 linkedin_pro_limit: "You've hit your monthly LinkedIn Optimizer limit.", 55 saved_resumes_limit: "Your first resume is free on every plan. A version tailored to each target role outperforms a generic one â upgrade to build as many as you need.", 56 job_hunter_pro_max: "The AI Job Hunter finds jobs for you automatically â it's a Pro Max feature.", 57 meta_agent_ultra: "The Job Hunt Meta Agent sources, scores, and tailors 10 jobs in one click.", 58 connections_limit: "You've used your Connections searches for this month.", 59 scam_limit: "You've used your Scam & Ghost Detector scans for this month.", 60 scam_lifetime: "You've used your 1 free Scam & Ghost Detector scan.", 61 connections_lifetime: "You've used your 1 free Connections search.", 62 contracts_pro: "The Contracts / Gigs tracker is a paid feature â track freelance & contract work on any paid plan.", 63 chrome_extension: "One-click job tracking in the Chrome Extension is free â upgrade to unlock unlimited tracking and AI tools right on the job page.", 64 upgrade_from_global: "Unlock unlimited AI tools, PDF export, and priority support.", 65 // tier_upgrade_signal nudge â the user hasn't hit a wall; keep the tone 66 // earned, not limit-flavored. 67 power_user: "You're already using Trackply like a power user â here's what the full toolkit looks like.", 68 }[reason] || "You've reached a free plan limit."; 69 70 const { close } = openModal({ 71 title: 'Upgrade Trackply', 72 body: ` 73 <div style="padding:4px 0 4px"> 74 <p style="color:var(--text-secondary);margin:0 0 4px;font-size:13px;text-align:center">${reasonText}</p> 75 <p style="color:var(--text-muted);margin:0 0 16px;font-size:11px;text-align:center">Join 250+ job seekers saving 10+ hours/week on their search.</p> 76 77 <!-- Pro Max â hero tier --> 78 <div style="position:relative;background:linear-gradient(135deg,rgba(180,0,255,0.1),rgba(255,0,140,0.1));border:2px solid rgba(180,0,255,0.45);border-radius:12px;padding:14px;margin-bottom:10px">
79 <span style="position:absolute;top:-10px;left:50%;transform:translateX(-50%);font-size:10px;font-weight:800;letter-spacing:.07em;color:#fff;background:linear-gradient(135deg,#b400ff,#ff008c);padding:3px 10px;border-radius:10px;white-space:nowrap">MOST POPULAR</span> 80 <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px"> 81 <div style="font-weight:800;font-size:16px;background:linear-gradient(135deg,#b400ff,#ff008c);-webkit-background-clip:text;-webkit-text-fill-color:transparent">Pro Max</div> 82 <div style="font-size:15px;font-weight:700;color:var(--text-primary)">$29<span style="font-size:11px;font-weight:400;color:var(--text-muted)">/mo</span><span class="tp-local-price" data-usd="29" style="font-size:10px;font-weight:500;color:var(--text-muted)"></span></div> 83 </div> 84 <p style="font-size:12px;color:var(--text-muted);margin:0 0 8px">The AI finds, scores & tailors jobs for you â while you sleep.</p> 85 <ul style="list-style:none;padding:0;margin:0 0 10px;font-size:12px;color:var(--text-secondary);line-height:1.7"> 86 <li>â <strong>AI Job Hunter</strong> â finds matching jobs 3Ã/day automatically</li> 87 <li>â <strong>Nightly auto-search</strong> + AI match scoring on every result</li> 88 <li>â Unlimited AI resume tailoring & cover letters</li> 89 <li>â LinkedIn Optimizer â 30 analyses/month</li> 90 <li>â Scam & Ghost Detector â 150 scans/month</li> 91 <li>â Connections outreach â 10 jobs/month</li> 92 </ul> 93 <button class="btn btn-sm" id="upgrade-max-btn" style="width:100%;font-size:13px;font-weight:700;padding:10px;background:linear-gradient(135deg,#b400ff,#ff008c);color:#fff;border:none;border-radius:8px">Start Pro Max â $29/mo â</button> 94 </div> 95 96 <!-- Pro + Ultra side by side --> 97 <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px"> 98 <!-- Pro --> 99 <div style="background:var(--surface-2);border:1px solid var(--border);border-radius:10px;padding:10px 12px"> 100 <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px"> 101 <div style="font-weight:700;font-size:13px;color:var(--accent)">Pro</div> 102 <div style="font-size:13px;font-weight:700;color:var(--text-primary)">$10<span style="font-size:10px;font-weight:400;color:var(--text-muted)">/mo</span><span class="tp-local-price" data-usd="10" style="font-size:9.5px;font-weight:500;color:var(--text-muted)"></span></div> 103 </div> 104 <p style="font-size:11px;color:var(--text-muted);margin:0 0 6px">Unlimited tools, no AI hunter.</p> 105 <ul style="list-style:none;padding:0;margin:0 0 8px;font-size:11px;color:var(--text-secondary);line-height:1.55"> 106 <li>â Unlimited job apps & tracking</li> 107 <li>â Unlimited AI resumes & covers</li> 108 <li>â LinkedIn Optimizer â 10/mo</li> 109 <li>â Scam Detector â 15/mo</li> 110 <li>â AI tools in the Chrome extension</li> 111 </ul> 112 <button class="btn btn-primary btn-sm" id="upgrade-pro-btn" style="width:100%;font-size:11px">Start Pro â $10/mo</button> 113 </div> 114 115 <!-- Ultra --> 116 <div style="background:linear-gradient(135deg,rgba(124,58,237,0.09),rgba(180,0,255,0.09));border:1px solid rgba(124,58,237,0.35);border-radius:10px;padding:10px 12px"> 117 <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px"> 118 <div style="font-weight:700;font-size:13px;background:linear-gradient(135deg,#7c3aed,#b400ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent">Ultra</div> 119 <div style="font-size:13px;font-weight:700;color:var(--text-primary)">$69<span style="font-size:10px;font-weight:400;color:var(--text-muted)">/mo</span><span class="tp-local-price" data-usd="69" style="font-size:9.5px;font-weight:500;color:var(--text-muted)"></span></div> 120 </div> 121 <p style="font-size:11px;color:var(--text-muted);margin:0 0 6px">Full autopilot job search.</p> 122 <ul style="list-style:none;padding:0;margin:0 0 8px;font-size:11px;color:var(--text-secondary);line-height:1.55"> 123 <li>â Everything in Pro Max</li> 124 <li>â <strong>Meta Agent</strong> â 1 full run/day</li> 125 <li>â Sources, scores & tailors 10 jobs/run</li> 126 <li>â Unlimited Connections & Scans</li> 127 </ul> 128 <button class="btn btn-sm" id="upgrade-ultra-modal-btn" style="width:100%;font-size:11px;background:linear-gradient(135deg,#7c3aed,#b400ff);color:#fff;border:none;border-radius:6px">Start Ultra â $69/mo</button> 129 </div> 130 </div> 131 132 <!-- Global plan option --> 133 <div style="background:linear-gradient(135deg,rgba(13,148,136,.07),rgba(8,145,178,.07));border:1px solid rgba(13,148,136,.3);border-radius:10px;padding:10px 12px;margin-bottom:10px"> 134 <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:3px"> 135 <div style="font-weight:700;font-size:13px;background:linear-gradient(135deg,#0d9488,#0891b2);-webkit-background-clip:text;-webkit-text-fill-color:transparent">Global</div> 136 <div style="font-size:13px;font-weight:700;color:var(--text-primary)">$4<span style="font-size:10px;font-weight:400;color:var(--text-muted)">/mo</span><span class="tp-local-price" data-usd="4" style="font-size:9.5px;font-weight:500;color:var(--text-muted)"></span></div> 137 </div> 138 <p style="font-size:11px;color:var(--text-muted);margin:0 0 5px">Great for international job seekers (South Asia, SE Asia & more).</p> 139 <ul style="list-style:none;padding:0;margin:0 0 8px;font-size:11px;color:var(--text-secondary);line-height:1.55"> 140 <li>â Unlimited job applications & tracking</li> 141 <li>â 5 AI resume tailors & 3 cover letters/mo</li> 142 <li>â Contract & gig tracker included</li> 143 </ul> 144 <button class="btn btn-sm" id="upgrade-global-btn" style="width:100%;font-size:11px;background:linear-gradient(135deg,#0d9488,#0891b2);color:#fff;border:none;border-radius:6px">Start Global â $4/mo</button> 145 </div> 146 147 <!-- Promo code --> 148 <div style="margin-bottom:10px"> 149 <label for="promo-code-input" style="display:block;font-size:11px;color:var(--text-muted);margin-bottom:4px">Promo code <span style="opacity:.6">(applies to Pro)</span></label> 150 <input id="promo-code-input" type="text" autocomplete="off" spellcheck="false" placeholder="e.g. TrackPro7Free" 151 style="width:100%;box-sizing:border-box;background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:7px 10px;color:var(--text-primary);font-size:12px" /> 152 <div id="promo-code-hint" style="font-size:11px;margin-top:4px;display:none"></div> 153 </div> 154 155 <div id="upgrade-modal-error" style="color:#ef4444;font-size:12px;margin-top:4px;display:none"></div> 156 <div class="tp-currency-disclaimer" style="font-size:10.5px;color:var(--text-muted);text-align:center;margin-top:6px;display:none"></div> 157 </div> 158 `, 159 footer: ` 160 <button class="btn btn-ghost btn-sm" id="upgrade-modal-cancel" style="font-size:11px;color:var(--text-muted)">No thanks, stay on free</button> 161 `, 162 }); 163 164 // Fill in local-currency estimates next to the USD prices (no-op for USD markets). 165 decorateLocalPrices(document.getElementById('modal-overlay')); 166 167 document.getElementById('upgrade-modal-cancel').addEventListener('click', close); 168 169 async function startCheckoutFor(plan) { 170 track('checkout.started', { plan }); 171 const btnId = plan === 'pro_max' ? 'upgrade-max-btn'
172 : plan === 'ultra' ? 'upgrade-ultra-modal-btn' 173 : plan === 'global' ? 'upgrade-global-btn' 174 : 'upgrade-pro-btn'; 175 const btn = document.getElementById(btnId); 176 const origText = btn.textContent; 177 btn.disabled = true; 178 btn.textContent = 'Redirectingâ¦'; 179 try { 180 const token = await getAccessToken(); 181 // Promo codes apply to the Pro plan only; don't send for Pro Max / Ultra. 182 const promoEl = document.getElementById('promo-code-input'); 183 const promoCode = (plan === 'pro' && promoEl) ? promoEl.value.trim() : ''; 184 const res = await fetch('/api/create-checkout', { 185 method: 'POST', 186 headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, 187 body: JSON.stringify(promoCode ? { plan, promoCode } : { plan }), 188 }); 189 const data = await res.json(); 190 if (data.url) { 191 window.location.href = data.url; 192 } else if (data.message) { 193 // e.g. "Already on Pro Max plan" â the user is already subscribed. 194 // Refresh plan state so the UI catches up, then close. 195 const errEl = document.getElementById('upgrade-modal-error'); 196 if (errEl) { 197 errEl.style.color = 'var(--text-secondary)'; 198 errEl.textContent = `${data.message}. Refreshingâ¦`; 199 errEl.style.display = 'block'; 200 } 201 try { 202 const { loadPlan } = await import('../plan.js'); 203 await loadPlan(token); 204 } catch {} 205 setTimeout(() => window.location.reload(), 1200); 206 } else { 207 throw new Error(data.error || 'Could not start checkout'); 208 } 209 } catch (err) { 210 const errEl = document.getElementById('upgrade-modal-error'); 211 if (errEl) { errEl.textContent = err.message; errEl.style.display = 'block'; } 212 btn.disabled = false; 213 btn.textContent = origText; 214 } 215 } 216 217 document.getElementById('upgrade-pro-btn').addEventListener('click', () => startCheckoutFor('pro')); 218 document.getElementById('upgrade-max-btn').addEventListener('click', () => startCheckoutFor('pro_max')); 219 document.getElementById('upgrade-ultra-modal-btn')?.addEventListener('click', () => startCheckoutFor('ultra')); 220 document.getElementById('upgrade-global-btn')?.addEventListener('click', () => startCheckoutFor('global')); 221 222 // Live feedback for the known promo code (validation is still enforced server-side). 223 const promoInput = document.getElementById('promo-code-input'); 224 const promoHint = document.getElementById('promo-code-hint'); 225 promoInput?.addEventListener('input', () => { 226 const v = promoInput.value.trim().toUpperCase(); 227 if (!v) { promoHint.style.display = 'none'; return; } 228 promoHint.style.display = 'block'; 229 if (v === 'TRACKPRO7FREE') { 230 promoHint.style.color = '#22c55e'; 231 promoHint.textContent = 'â 1 week free Pro trial â choose Pro to apply.'; 232 } else { 233 promoHint.style.color = 'var(--text-muted)'; 234 promoHint.textContent = "We'll check this code at checkout."; 235 } 236 }); 237} 238 239export function showUpgradeSuccessModal(plan = 'pro') { 240 const isGlobal = plan === 'global'; 241 const isProMax = plan === 'pro_max'; 242 const isUltra = plan === 'ultra'; 243 244 const benefitsProMax = ` 245 <li>â Unlimited job applications & tracking</li> 246 <li>â Unlimited AI resume scoring & cover letters</li> 247 <li>â LinkedIn Optimizer â 30 analyses/month</li> 248 <li>â 3 AI Job Hunter searches/day <span style="color:var(--text-muted);font-size:11px">(+$1.50 per extra)</span></li> 249 <li>â 1 free Meta Agent test run <span style="color:var(--text-muted);font-size:11px">(then $7/run)</span></li> 250 <li>â Full Contracts / Gigs tracker</li>`; 251 252 const benefitsUltra = ` 253 <li>â Everything in Pro Max</li> 254 <li>â Job Hunt Meta Agent â 1 full run/day <span style="color:var(--text-muted);font-size:11px">(+$7 per extra)</span></li> 255 <li>â Sources, scores & tailors up to 10 jobs per run</li> 256 <li>â Early access to every new AI agent</li>`; 257 258 const benefitsPro = ` 259 <li>â Unlimited job applications & tracking</li> 260 <li>â Unlimited AI resume scoring & cover letters</li> 261 <li>â LinkedIn Optimizer â 10 analyses/month</li> 262 <li>â 1 free Meta Agent test run <span style="color:var(--text-muted);font-size:11px">(then $7/run)</span></li> 263 <li>â Full Contracts / Gigs tracker</li>`; 264 265 const benefitsGlobal = ` 266 <li>â Unlimited job applications & tracking</li> 267 <li>â AI tools in the Chrome extension</li> 268 <li>â 5 AI resume tailors & 3 cover letters/month</li> 269 <li>â Contract & gig tracker (full)</li> 270 <li>â 1 free Meta Agent test run <span style="color:var(--text-muted);font-size:11px">(then $7/run)</span></li>`; 271 272 const planLabel = isUltra 273 ? '<span style="background:linear-gradient(135deg,#7c3aed,#b400ff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-weight:700">Ultra</span>' 274 : isProMax 275 ? '<span style="background:linear-gradient(135deg,#b400ff,#ff008c);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-weight:700">Pro Max</span>' 276 : isGlobal 277 ? '<span style="background:linear-gradient(135deg,#0d9488,#0891b2);-webkit-background-clip:text;-webkit-text-fill-color:transparent;font-weight:700">Global</span>' 278 : '<span style="color:var(--accent);font-weight:700">Pro</span>'; 279
280 openModal({ 281 title: 'ð Upgrade Successful!', 282 body: ` 283 <div style="text-align:center;padding:4px 0 8px"> 284 <p style="font-size:16px;margin:0 0 16px;color:var(--text-primary)"> 285 You're now on ${planLabel} 286 </p> 287 288 <div style="background:var(--surface-2);border:1px solid var(--border);border-radius:10px;padding:14px 16px;text-align:left;margin-bottom:20px"> 289 <div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px">What you unlocked</div> 290 <ul style="list-style:none;padding:0;margin:0;font-size:13px;color:var(--text-secondary);line-height:2"> 291 ${isUltra ? benefitsUltra : isProMax ? benefitsProMax : isGlobal ? benefitsGlobal : benefitsPro} 292 </ul> 293 </div> 294 295 <div style="background:${isProMax ? 'linear-gradient(135deg,rgba(180,0,255,0.07),rgba(255,0,140,0.07))' : 'rgba(var(--accent-rgb,99,102,241),0.07)'};border:1px solid ${isProMax ? 'rgba(180,0,255,0.25)' : 'rgba(var(--accent-rgb,99,102,241),0.25)'};border-radius:10px;padding:14px 16px;text-align:left;margin-bottom:8px"> 296 <div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">Next Step</div> 297 <p style="font-size:13px;color:var(--text-secondary);margin:0 0 12px;line-height:1.5"> 298 Get the Trackply Chrome Extension to auto-track jobs from LinkedIn, Indeed, and more. 299 </p> 300 <a href="#" class="btn btn-primary btn-sm" style="display:inline-flex;align-items:center;gap:6px;text-decoration:none"> 301 Download Chrome Extension â 302 </a> 303 </div> 304 </div> 305 `, 306 }); 307} 308 309export function confirmDialog(message, onConfirm, confirmLabel = 'Delete', danger = true) { 310 const { close } = openModal({ 311 title: 'Confirm', 312 body: `<div class="confirm-dialog"><p>${message}</p></div>`, 313 footer: ` 314 <button class="btn btn-ghost btn-sm" id="confirm-cancel">Cancel</button> 315 <button class="btn ${danger ? 'btn-danger' : 'btn-primary'} btn-sm" id="confirm-ok">${confirmLabel}</button> 316 `, 317 }); 318 document.getElementById('confirm-cancel').addEventListener('click', close); 319 document.getElementById('confirm-ok').addEventListener('click', () => { close(); onConfirm(); }); 320}
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.