PageSourceSearch

https://www.trackply.com/src/js/components/modal.js

js trackply.com collected 2026-10-02 00:58:01 UTC 19,911 bytes, 320 lines download raw bytes

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 &amp; 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 &amp; cover letters</li>
89            <li>✓ LinkedIn Optimizer — 30 analyses/month</li>
90            <li>✓ Scam &amp; 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 &amp; tracking</li>
107              <li>✓ Unlimited AI resumes &amp; 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 &amp; tailors 10 jobs/run</li>
126              <li>✓ Unlimited Connections &amp; 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 &amp; 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 &amp; tracking</li>
141            <li>✓ 5 AI resume tailors &amp; 3 cover letters/mo</li>
142            <li>✓ Contract &amp; 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 &amp; tracking</li>
246    <li>✅ Unlimited AI resume scoring &amp; 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 &amp; 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 &amp; tracking</li>
260    <li>✅ Unlimited AI resume scoring &amp; 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 &amp; tracking</li>
267    <li>✅ AI tools in the Chrome extension</li>
268    <li>✅ 5 AI resume tailors &amp; 3 cover letters/month</li>
269    <li>✅ Contract &amp; 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.