<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Saad Habib]]></title><description><![CDATA[Saad Habib]]></description><link>https://iamsaadhabib.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 05 Nov 2026 16:43:55 GMT</lastBuildDate><atom:link href="https://iamsaadhabib.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Built a Lead Tracker Dashboard with Vanilla JavaScript]]></title><description><![CDATA[Build a lead tracker dashboard in one HTML file with vanilla JavaScript: localStorage, search, filters, win rate stats and CSV export. Saad Habib]]></description><link>https://iamsaadhabib.hashnode.dev/how-i-built-a-lead-tracker-dashboard-with-vanilla-javascript</link><guid isPermaLink="true">https://iamsaadhabib.hashnode.dev/how-i-built-a-lead-tracker-dashboard-with-vanilla-javascript</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[webdev]]></category><category><![CDATA[HTML5]]></category><category><![CDATA[HTML]]></category><category><![CDATA[CSS]]></category><category><![CDATA[saadhabib]]></category><category><![CDATA[fivup]]></category><dc:creator><![CDATA[Saad Habib]]></dc:creator><pubDate>Sun, 01 Nov 2026 17:05:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ae76c794440dd31e4241971/0c3fab01-3e37-4a37-8dd3-4220f943b489.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I wanted a simple way to track leads without signing up for a heavy CRM, so I built one in a single HTML file. No framework, no backend, no build step. It runs in any browser and keeps data on your own device.</p>
<p><strong>Live demo:</strong> <a href="https://iamsaadhabib.github.io/lead-tracker/">iamsaadhabib.github.io/lead-tracker</a> <strong>Source code:</strong><br /><br /><a href="https://github.com/iamsaadhabib/lead-tracker">github.com/iamsaadhabib/lead-tracker</a></p>
<h2>What it does</h2>
<ul>
<li>Add leads with name, company, email, source and status</li>
<li>Pipeline stages: New, Contacted, Qualified, Won, Lost</li>
<li>Live stats: total leads, count per stage and win rate</li>
<li>Search by name or company, filter by status</li>
<li>Export all leads to CSV</li>
<li>Saves data in the browser, nothing leaves your device</li>
</ul>
<h2>1. Keep the data in one array</h2>
<p>Every lead is an object in a single array. Saving and loading is just JSON in <code>localStorage</code>, wrapped in <code>try/catch</code> because storage can be blocked in private windows.</p>
<pre><code class="language-js">const KEY = "fivup-lead-tracker";
let leads = [];
try { leads = JSON.parse(localStorage.getItem(KEY)) || []; } catch (e) { leads = []; }

function save() {
  try { localStorage.setItem(KEY, JSON.stringify(leads)); } catch (e) {}
}
</code></pre>
<h2>2. Render from state, always</h2>
<p>Instead of patching the page by hand, one <code>render()</code> function rebuilds the stats and the table from the array. Every action (add, delete, change status) updates the array, saves it, and calls <code>render()</code>.</p>
<pre><code class="language-js">function render() { renderStats(); renderRows(); }

document.getElementById("leadForm").addEventListener("submit", e =&gt; {
  e.preventDefault();
  const d = Object.fromEntries(new FormData(e.target));
  leads.unshift({ id: Date.now(), ...d });
  save(); e.target.reset(); render();
});
</code></pre>
<p>This keeps the logic simple. There is only one place that decides what the screen looks like.</p>
<h2>3. Escape what the user types</h2>
<p>Because the table is built with template strings, anything a user types goes straight into HTML. A name like <code>&lt;img src=x onerror=...&gt;</code> would run code. A small escape function fixes that:</p>
<pre><code class="language-js">function esc(s) {
  return String(s).replace(/[&amp;&lt;&gt;"']/g, c =&gt; ({
    "&amp;": "&amp;amp;", "&lt;": "&amp;lt;", "&gt;": "&amp;gt;", '"': "&amp;quot;", "'": "&amp;#39;"
  }[c]));
}
</code></pre>
<p>Every value is passed through <code>esc()</code> before it is placed in the table.</p>
<h2>4. Stats and win rate</h2>
<p>The stats row counts leads per stage and calculates the win rate:</p>
<pre><code class="language-js">function renderStats() {
  const counts = Object.fromEntries(STATUSES.map(s =&gt; [s, 0]));
  leads.forEach(l =&gt; counts[l.status]++);
  const total = leads.length;
  const rate = total ? Math.round((counts.Won / total) * 100) : 0;
  // ...build the tiles from counts and rate
}
</code></pre>
<p>The <code>total ? ... : 0</code> check avoids dividing by zero when the list is empty.</p>
<h2>5. Search and filter</h2>
<p>Search and the status filter work together on the same list:</p>
<pre><code class="language-js">const list = leads.filter(l =&gt;
  (!f || l.status === f) &amp;&amp;
  (!q || l.name.toLowerCase().includes(q) || l.company.toLowerCase().includes(q))
);
</code></pre>
<h2>6. Export to CSV</h2>
<p>CSV export builds the file in memory and downloads it with a temporary link. Double quotes inside values are escaped by doubling them, which is how CSV expects them:</p>
<pre><code class="language-js">const cell = v =&gt; `"${String(v || "").replace(/"/g, '""')}"`;
const csv = [header.join(",")]
  .concat(leads.map(l =&gt; [l.name, l.company, l.email, l.source, l.status].map(cell).join(",")))
  .join("\n");
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
a.download = "leads.csv";
a.click();
URL.revokeObjectURL(a.href);
</code></pre>
<h2>What I would add next</h2>
<ul>
<li>A Node.js and MongoDB backend so a team can share the same leads</li>
<li>Follow-up reminders by email</li>
<li>Import leads from a CSV file</li>
</ul>
<h2>Takeaways</h2>
<ul>
<li>A single HTML file is enough for many small tools.</li>
<li>Keep one source of truth (the array) and re-render from it.</li>
<li>Always escape user input when you build HTML from strings.</li>
</ul>
<p>I'm Saad Habib, founder of <a href="https://www.linkedin.com/company/106092344/">Fivup Leads</a>, where we help businesses get qualified leads and build websites that convert. You can find me on <a href="https://www.linkedin.com/in/saad-habib-me/">LinkedIn</a> and <a href="https://github.com/iamsaadhabib">GitHub</a>.</p>
<p>What feature would you add to the Lead Tracker? Tell me in the comments.</p>
]]></content:encoded></item></channel></rss>