keyword-sniper / index.html
delorme's picture
Update index.html
03afb09 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KW Manager</title>
<style>
* { box-sizing: border-box; }
body {
font: 14px sans-serif;
padding: 20px;
background: #f5f7fa;
margin: 0;
}
.hdr {
background: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.hdr h2 {
margin: 0 0 15px 0;
color: #1a73e8;
}
.stats {
display: flex;
gap: 20px;
margin: 15px 0;
flex-wrap: wrap;
}
.stat {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
min-width: 120px;
}
.stat strong {
display: block;
font-size: 28px;
color: #1a73e8;
font-weight: 600;
}
.stat span {
font-size: 12px;
color: #666;
text-transform: uppercase;
}
.run-selector {
margin: 15px 0;
padding: 15px;
background: #e8f5e9;
border-radius: 6px;
border-left: 4px solid #0f9d58;
}
.run-selector label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.run-selector select {
width: 100%;
max-width: 400px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
background: #fff;
}
.auto-search {
margin: 20px 0;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
border-left: 4px solid #f9ab00;
}
.auto-search h3 {
margin: 0 0 10px 0;
color: #f9ab00;
font-size: 18px;
}
.auto-search p {
margin: 0 0 15px 0;
font-size: 12px;
color: #666;
}
.search-input-group {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.search-input-group textarea {
flex: 1;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
min-height: 80px;
font-family: inherit;
resize: vertical;
}
.search-input-group button {
padding: 12px 24px;
white-space: nowrap;
align-self: flex-start;
}
.queue-list {
margin-top: 15px;
max-height: 300px;
overflow-y: auto;
}
.queue-item {
background: #f8f9fa;
padding: 12px 15px;
margin: 5px 0;
border-radius: 4px;
font-size: 13px;
}
.queue-item.processing {
background: #fff3cd;
border-left: 3px solid #f9ab00;
}
.queue-item.completed {
background: #d4edda;
border-left: 3px solid #28a745;
}
.queue-item.error {
background: #f8d7da;
border-left: 3px solid #dc3545;
}
.queue-main {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.queue-detail {
font-size: 11px;
color: #666;
margin-top: 4px;
}
.run-info {
background: #e3f2fd;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
margin-top: 10px;
color: #1565c0;
}
.btn {
padding: 10px 20px;
margin: 5px;
border: none;
background: #1a73e8;
color: #fff;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.btn:hover { background: #1557b0; }
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.bg { background: #0f9d58; }
.bg:hover { background: #0d8547; }
.by { background: #f9ab00; }
.by:hover { background: #e69500; }
.br { background: #d93025; }
.br:hover { background: #c5221f; }
.bs {
padding: 6px 12px;
font-size: 12px;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.show { display: flex; }
.mc {
background: #fff;
padding: 30px;
border-radius: 12px;
max-width: 900px;
width: 90%;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.mc h3 {
margin-top: 0;
color: #1a73e8;
font-size: 24px;
}
.mc input {
width: 100%;
padding: 12px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.ri {
background: #fff;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
border-left: 4px solid #1a73e8;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.rh {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.rh strong {
font-size: 16px;
color: #333;
}
.rh small {
color: #666;
font-size: 12px;
}
.ri-stats {
display: flex;
gap: 15px;
margin-top: 8px;
font-size: 13px;
color: #666;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin-top: 20px;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
th, td {
border: 1px solid #e0e0e0;
padding: 10px;
text-align: left;
}
th {
background: #f8f9fa;
font-weight: 600;
color: #333;
}
tr:hover { background: #f5f5f5; }
.tag {
padding: 4px 10px;
background: #e3f2fd;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
color: #1565c0;
display: inline-block;
}
.seed {
background: #fff3cd;
padding: 8px 15px;
border-radius: 4px;
display: inline-block;
margin-bottom: 10px;
font-weight: 600;
color: #856404;
}
.preview-collapsed {
max-height: 400px;
overflow: hidden;
position: relative;
}
.preview-collapsed::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 100px;
background: linear-gradient(transparent, #fff);
pointer-events: none;
}
.expand-btn {
margin-top: 10px;
text-align: center;
}
.welcome {
text-align: center;
padding: 60px 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.welcome h1 {
color: #1a73e8;
margin-bottom: 20px;
}
.welcome p {
color: #666;
font-size: 16px;
margin: 10px 0;
}
.welcome code {
background: #f8f9fa;
padding: 2px 8px;
border-radius: 3px;
font-family: monospace;
color: #d93025;
}
</style>
</head>
<body>
<div id="welcomeScreen" class="welcome">
<h1>🎯 KW Manager</h1>
<p><strong>Ready to collect keywords!</strong></p>
<p>Run the bookmarklet on any Google SERP page with the keyword explorer extension active.</p>
<p>Your data will be stored here permanently at:</p>
<p><code id="currentUrl"></code></p>
<p style="margin-top: 30px; color: #999; font-size: 12px;">Waiting for data...</p>
</div>
<div id="mainContent" style="display:none;">
<div class="hdr">
<h2>KW Manager</h2>
<div class="run-selector">
<label>Current Run:</label>
<select id="runSelect">
<option value="">Select a run...</option>
</select>
<button id="newRunBtn" class="btn bg bs" style="margin-left:10px">+ New Run</button>
<button id="switchRunBtn" class="btn bs">Switch Run</button>
</div>
<div class="stats">
<div class="stat">
<strong id="cc">0</strong>
<span>Current Batch</span>
</div>
<div class="stat">
<strong id="rc">0</strong>
<span>Run Keywords</span>
</div>
<div class="stat">
<strong id="tr">0</strong>
<span>Total Runs</span>
</div>
<div class="stat">
<strong id="tk">0</strong>
<span>Total Keywords</span>
</div>
</div>
<div>
<button id="addBtn" class="btn bg">Add to Current Run</button>
<button id="manageBtn" class="btn by">Manage Runs</button>
<button id="dlBtn" class="btn">Download Current</button>
<button id="cpBtn" class="btn">Copy Current</button>
</div>
</div>
<div class="auto-search">
<h3>🔄 Auto-Search Queue</h3>
<p>Enter keywords separated by commas or new lines (e.g., "keyword1, keyword2" or one per line)</p>
<div class="search-input-group">
<textarea id="seedInput" placeholder="Enter seed keywords...&#10;keyword1, keyword2, keyword3&#10;or one per line"></textarea>
<button id="addQueueBtn" class="btn by">Add to Queue</button>
</div>
<div style="display:flex;gap:10px;margin-bottom:10px">
<button id="startQueueBtn" class="btn bg" disabled>Start Queue</button>
<button id="stopQueueBtn" class="btn br" style="display:none">Stop</button>
<button id="clearQueueBtn" class="btn" style="background:#666">Clear Queue</button>
</div>
<div class="run-info" id="targetRunInfo">⚠️ Select or create a run first</div>
<div class="queue-list" id="queueList"></div>
</div>
<div id="manageModal" class="modal">
<div class="mc">
<h3>Manage Runs</h3>
<div id="runsList"></div>
<button id="closeManage" class="btn" style="margin-top:20px">Close</button>
</div>
</div>
<div id="newRunModal" class="modal">
<div class="mc" style="max-width:500px">
<h3>Create New Run</h3>
<p>Enter a name for this run:</p>
<input type="text" id="newRunName" placeholder="e.g., Main Keywords, Client Project, etc.">
<div style="margin-top:20px">
<button id="cancelNewRun" class="btn" style="background:#666">Cancel</button>
<button id="saveNewRun" class="btn bg">Create Run</button>
</div>
</div>
</div>
<div id="previewSection" style="overflow:auto;background:#fff;padding:20px;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,0.1)">
<div class="seed" id="seedDisplay">Seed Keyword: -</div>
<h3>Preview (<span id="previewCount">0</span> keywords)</h3>
<div id="previewContainer" class="preview-collapsed">
<table>
<thead>
<tr>
<th>Source</th>
<th>Keyword</th>
<th>Volume</th>
<th>CPC</th>
<th>Competition</th>
</tr>
</thead>
<tbody id="previewBody">
<tr>
<td colspan="5" style="text-align:center;color:#666;padding:40px">No data yet - run the bookmarklet to collect keywords</td>
</tr>
</tbody>
</table>
</div>
<div class="expand-btn">
<button id="expandBtn" class="btn bs">Show All Keywords</button>
</div>
</div>
</div>
<script>
// Display current URL
document.getElementById('currentUrl').textContent = window.location.href;
// This will be populated by the bookmarklet
window.kwManagerInit = function(data, header, seed) {
document.getElementById('welcomeScreen').style.display = 'none';
document.getElementById('mainContent').style.display = 'block';
if (!window.kwManager) {
window.kwManager = {
initialData: data,
header: header,
seed: seed,
allPreviewData: [...data],
queue: [],
isProcessing: false,
shouldStop: false
};
initializeApp();
} else {
// Update with new data
window.kwManager.initialData = data;
window.kwManager.allPreviewData = [...window.kwManager.allPreviewData, ...data];
document.getElementById('cc').textContent = data.length;
document.getElementById('seedDisplay').textContent = 'Seed Keyword: ' + seed;
document.getElementById('previewCount').textContent = window.kwManager.allPreviewData.length;
refreshPreviewTable();
}
};
function initializeApp() {
const km = window.kwManager;
const boxAbbrev = {0:"PAA", 1:"Topical", 2:"SERP", 3:"Related", 4:"Long-Tail"};
document.getElementById('cc').textContent = km.initialData.length;
document.getElementById('seedDisplay').textContent = 'Seed Keyword: ' + km.seed;
document.getElementById('previewCount').textContent = km.allPreviewData.length;
// Render initial preview
refreshPreviewTable();
function getRuns() {
const r = [];
for (let k in localStorage) {
if (k.startsWith("kw_run_")) {
try { r.push(JSON.parse(localStorage[k])); } catch(e) {}
}
}
return r.sort((a,b) => b.timestamp - a.timestamp);
}
function getCurrentRun() {
return localStorage.getItem("kw_current_run") || "";
}
function setCurrentRun(id) {
localStorage.setItem("kw_current_run", id);
updateStats();
updateRunSelector();
updateTargetRunInfo();
}
function updateStats() {
const runs = getRuns();
const total = runs.reduce((s,r) => s + r.keywords.length, 0);
document.getElementById("tr").textContent = runs.length;
document.getElementById("tk").textContent = total;
const curr = getCurrentRun();
if (curr) {
const run = runs.find(r => r.id === curr);
if (run) {
const unique = new Set(run.keywords.map(k => k.data)).size;
document.getElementById("rc").textContent = unique;
} else {
document.getElementById("rc").textContent = 0;
}
} else {
document.getElementById("rc").textContent = 0;
}
}
function updateRunSelector() {
const sel = document.getElementById("runSelect");
const curr = getCurrentRun();
const runs = getRuns();
sel.innerHTML = '<option value="">Select a run...</option>';
runs.forEach(r => {
const opt = document.createElement("option");
opt.value = r.id;
opt.textContent = r.name + " (" + r.keywords.length + " kw)";
if (r.id === curr) opt.selected = true;
sel.appendChild(opt);
});
if (curr) {
const run = runs.find(r => r.id === curr);
if (run) {
document.querySelector(".run-selector").style.background = "#e8f5e9";
document.querySelector(".run-selector").style.borderColor = "#0f9d58";
}
}
}
function updateTargetRunInfo() {
const curr = getCurrentRun();
const info = document.getElementById("targetRunInfo");
if (!curr) {
info.innerHTML = "⚠️ Select or create a run first";
info.style.background = "#fff3cd";
info.style.color = "#856404";
} else {
const runs = getRuns();
const run = runs.find(r => r.id === curr);
if (run) {
info.innerHTML = "✓ Keywords will be added to: <strong>" + run.name + "</strong> (" + run.keywords.length + " kw)";
info.style.background = "#d4edda";
info.style.color = "#155724";
}
}
}
function updateQueueDisplay() {
const list = document.getElementById("queueList");
if (km.queue.length === 0) {
list.innerHTML = '<div style="text-align:center;padding:20px;color:#666">No keywords in queue</div>';
return;
}
let html = "";
km.queue.forEach((item, i) => {
let cls = "queue-item";
let statusText = "⏳ Waiting";
let detail = "";
if (item.status === "processing") {
cls += " processing";
statusText = "🔄 Processing...";
detail = '<div class="queue-detail">Waiting for extension buttons to load...</div>';
} else if (item.status === "completed") {
cls += " completed";
statusText = "✓ Completed";
const sources = [];
if (item.boxCounts) {
for (let b in item.boxCounts) {
if (item.boxCounts[b] > 0) sources.push(boxAbbrev[b] + ": " + item.boxCounts[b]);
}
}
detail = '<div class="queue-detail">' + (item.count || 0) + ' kw > ' + (sources.length > 0 ? sources.join(", ") : "No data") + '</div>';
} else if (item.status === "error") {
cls += " error";
statusText = "✗ Error";
detail = '<div class="queue-detail">Failed to collect data</div>';
}
html += '<div class="' + cls + '"><div class="queue-main"><div><strong>' + item.keyword + '</strong> - ' + statusText + '</div><button onclick="removeFromQueue(' + i + ')" class="btn br bs" ' + ((item.status === "processing") ? "disabled" : "") + '>Remove</button></div>' + detail + '</div>';
});
list.innerHTML = html;
}
window.removeFromQueue = function(idx) {
if (km.isProcessing && km.queue[idx].status === "processing") {
alert("Cannot remove currently processing item");
return;
}
km.queue.splice(idx, 1);
updateQueueDisplay();
if (km.queue.length === 0) {
document.getElementById("startQueueBtn").disabled = true;
}
};
document.getElementById("addQueueBtn").onclick = function() {
const input = document.getElementById("seedInput");
const text = input.value.trim();
if (!text) {
alert("Enter keywords");
return;
}
const keywords = text.split(/[,\n]+/).map(k => k.trim()).filter(k => k.length > 0);
if (keywords.length === 0) {
alert("No valid keywords found");
return;
}
let added = 0;
keywords.forEach(kw => {
if (!km.queue.some(q => q.keyword.toLowerCase() === kw.toLowerCase())) {
km.queue.push({keyword: kw, status: "waiting", count: 0, boxCounts: {}});
added++;
}
});
input.value = "";
updateQueueDisplay();
document.getElementById("startQueueBtn").disabled = false;
alert("Added " + added + " keyword(s) to queue");
};
document.getElementById("clearQueueBtn").onclick = function() {
if (km.isProcessing) {
alert("Cannot clear queue while processing");
return;
}
if (km.queue.length === 0) {
alert("Queue is already empty");
return;
}
if (confirm("Clear all " + km.queue.length + " keywords from queue?")) {
km.queue = [];
updateQueueDisplay();
document.getElementById("startQueueBtn").disabled = true;
}
};
document.getElementById("startQueueBtn").onclick = async function() {
const curr = getCurrentRun();
if (!curr) {
alert("Please select or create a run first!");
return;
}
if (km.queue.length === 0) {
alert("Add keywords to queue first");
return;
}
km.isProcessing = true;
km.shouldStop = false;
document.getElementById("startQueueBtn").style.display = "none";
document.getElementById("stopQueueBtn").style.display = "inline-block";
document.getElementById("addQueueBtn").disabled = true;
document.getElementById("clearQueueBtn").disabled = true;
for (let i = 0; i < km.queue.length; i++) {
if (km.shouldStop) {
km.queue[i].status = "waiting";
break;
}
km.queue[i].status = "processing";
updateQueueDisplay();
const delay = Math.floor(Math.random() * 4000) + 6000;
await new Promise(r => setTimeout(r, delay));
try {
const result = await searchAndCollect(km.queue[i].keyword);
if (result.success) {
km.queue[i].status = "completed";
km.queue[i].count = result.count;
km.queue[i].boxCounts = result.boxCounts;
addToPreview(result.data);
} else {
km.queue[i].status = "error";
}
} catch(e) {
km.queue[i].status = "error";
}
updateQueueDisplay();
updateStats();
}
km.isProcessing = false;
document.getElementById("startQueueBtn").style.display = "inline-block";
document.getElementById("stopQueueBtn").style.display = "none";
document.getElementById("addQueueBtn").disabled = false;
document.getElementById("clearQueueBtn").disabled = false;
alert("Queue processing complete!");
};
document.getElementById("stopQueueBtn").onclick = function() {
km.shouldStop = true;
this.disabled = true;
this.textContent = "Stopping...";
};
async function searchAndCollect(keyword) {
return new Promise((resolve) => {
const searchUrl = "https://www.google.com/search?q=" + encodeURIComponent(keyword);
const tab = window.open(searchUrl, "_blank");
if (!tab) {
resolve({success: false, count: 0, data: [], boxCounts: {}});
return;
}
let waited = 0;
const waitForButtons = setInterval(async () => {
waited += 500;
try {
if (tab.closed) {
clearInterval(waitForButtons);
resolve({success: false, count: 0, data: [], boxCounts: {}});
} else if (waited >= 12000) {
const buttons = tab.document.querySelectorAll("button.xt-copy-csv.xt-ke-btn");
if (buttons.length === 5) {
clearInterval(waitForButtons);
let collectedData = [];
let boxCounts = {0:0, 1:0, 2:0, 3:0, 4:0};
const boxLabels = ["People Also Ask", "Topical Keywords", "SERP Keywords", "Related Keywords", "Long-Tail Keywords"];
for (let i = 0; i < 5; i++) {
buttons[i].click();
await new Promise(r => setTimeout(r, 800));
try {
const clipText = await tab.navigator.clipboard.readText();
const rows = clipText.trim().split("\n");
rows.slice(1).forEach(row => {
if (row.trim() && row.includes(";")) {
collectedData.push({box: boxLabels[i], data: row});
boxCounts[i]++;
}
});
} catch(e) {}
}
const curr = getCurrentRun();
const runs = getRuns();
const run = runs.find(r => r.id === curr);
if (run) {
const existing = new Set(run.keywords.map(k => k.data));
collectedData.forEach(item => {
if (!existing.has(item.data)) {
run.keywords.push({box: item.box, data: item.data, added: Date.now()});
existing.add(item.data);
}
});
localStorage.setItem(run.id, JSON.stringify(run));
}
tab.close();
resolve({success: true, count: collectedData.length, data: collectedData, boxCounts: boxCounts});
} else {
clearInterval(waitForButtons);
tab.close();
resolve({success: false, count: 0, data: [], boxCounts: {}});
}
} else {
try {
const buttons = tab.document.querySelectorAll("button.xt-copy-csv.xt-ke-btn");
if (buttons.length === 5) {
clearInterval(waitForButtons);
let collectedData = [];
let boxCounts = {0:0, 1:0, 2:0, 3:0, 4:0};
const boxLabels = ["People Also Ask", "Topical Keywords", "SERP Keywords", "Related Keywords", "Long-Tail Keywords"];
for (let i = 0; i < 5; i++) {
buttons[i].click();
await new Promise(r => setTimeout(r, 800));
try {
const clipText = await tab.navigator.clipboard.readText();
const rows = clipText.trim().split("\n");
rows.slice(1).forEach(row => {
if (row.trim() && row.includes(";")) {
collectedData.push({box: boxLabels[i], data: row});
boxCounts[i]++;
}
});
} catch(e) {}
}
const curr = getCurrentRun();
const runs = getRuns();
const run = runs.find(r => r.id === curr);
if (run) {
const existing = new Set(run.keywords.map(k => k.data));
collectedData.forEach(item => {
if (!existing.has(item.data)) {
run.keywords.push({box: item.box, data: item.data, added: Date.now()});
existing.add(item.data);
}
});
localStorage.setItem(run.id, JSON.stringify(run));
}
tab.close();
resolve({success: true, count: collectedData.length, data: collectedData, boxCounts: boxCounts});
}
} catch(e) {}
}
} catch(e) {}
}, 500);
setTimeout(() => {
clearInterval(waitForButtons);
if (!tab.closed) tab.close();
resolve({success: false, count: 0, data: [], boxCounts: {}});
}, 60000);
});
}
function addToPreview(newData) {
km.allPreviewData = [...km.allPreviewData, ...newData];
document.getElementById("previewCount").textContent = km.allPreviewData.length;
const container = document.getElementById("previewContainer");
if (!container.classList.contains("preview-collapsed")) {
refreshPreviewTable();
}
}
function refreshPreviewTable() {
const tbody = document.getElementById("previewBody");
const container = document.getElementById("previewContainer");
const showAll = !container.classList.contains("preview-collapsed");
const dataToShow = showAll ? km.allPreviewData : km.allPreviewData.slice(0, 30);
let html = "";
dataToShow.forEach(x => {
const p = x.data.split(";").map(v => v.replace(/"/g, "").trim());
html += '<tr><td><span class="tag">' + x.box + '</span></td><td>' + p[0] + '</td><td>' + p[1] + '</td><td>' + p[2] + '</td><td>' + p[3] + '</td></tr>';
});
if (!showAll && km.allPreviewData.length > 30) {
html += '<tr><td colspan="5" style="text-align:center;color:#666;font-style:italic">... and ' + (km.allPreviewData.length - 30) + ' more keywords (click "Show All Keywords" to expand)</td></tr>';
}
tbody.innerHTML = html;
}
document.getElementById("expandBtn").onclick = function() {
const container = document.getElementById("previewContainer");
if (container.classList.contains("preview-collapsed")) {
container.classList.remove("preview-collapsed");
this.textContent = "Show Less";
refreshPreviewTable();
} else {
container.classList.add("preview-collapsed");
this.textContent = "Show All Keywords";
refreshPreviewTable();
}
};
document.getElementById("runSelect").onchange = function() {
if (this.value) {
setCurrentRun(this.value);
}
};
document.getElementById("switchRunBtn").onclick = function() {
const sel = document.getElementById("runSelect");
if (sel.value) {
setCurrentRun(sel.value);
alert("Switched to: " + sel.options[sel.selectedIndex].text);
} else {
alert("Please select a run first");
}
};
document.getElementById("newRunBtn").onclick = function() {
document.getElementById("newRunName").value = "";
document.getElementById("newRunModal").classList.add("show");
document.getElementById("newRunName").focus();
};
document.getElementById("cancelNewRun").onclick = function() {
document.getElementById("newRunModal").classList.remove("show");
};
document.getElementById("saveNewRun").onclick = function() {
const name = document.getElementById("newRunName").value.trim();
if (!name) {
alert("Please enter a run name");
return;
}
const id = "kw_run_" + Date.now();
const run = {
id: id,
name: name,
timestamp: Date.now(),
created: new Date().toLocaleString(),
keywords: []
};
try {
localStorage.setItem(id, JSON.stringify(run));
setCurrentRun(id);
document.getElementById("newRunModal").classList.remove("show");
alert("Created new run: " + name);
updateStats();
updateRunSelector();
} catch(e) {
alert("Error creating run: " + e.message);
}
};
document.getElementById("addBtn").onclick = function() {
const curr = getCurrentRun();
if (!curr) {
alert("Please create or select a run first!");
document.getElementById("newRunBtn").click();
return;
}
const runs = getRuns();
const run = runs.find(r => r.id === curr);
if (!run) {
alert("Run not found!");
return;
}
const existing = new Set(run.keywords.map(k => k.data));
let added = 0;
km.initialData.forEach(item => {
if (!existing.has(item.data)) {
run.keywords.push({box: item.box, data: item.data, added: Date.now()});
existing.add(item.data);
added++;
}
});
try {
localStorage.setItem(run.id, JSON.stringify(run));
alert("Added " + added + " new keywords to \"" + run.name + "\"\nTotal in run: " + run.keywords.length);
updateStats();
updateRunSelector();
} catch(e) {
alert("Error: " + e.message);
}
};
document.getElementById("manageBtn").onclick = function() {
const runs = getRuns();
let html = "";
if (!runs.length) {
html = '<div style="text-align:center;padding:40px;color:#666"><p style="font-size:18px">No runs yet</p><p>Create your first run to start organizing keywords!</p></div>';
} else {
runs.forEach((r, i) => {
const unique = new Set(r.keywords.map(k => k.data)).size;
html += '<div class="ri"><div class="rh"><div><strong>' + r.name + '</strong><br><small>Created: ' + r.created + '</small></div><div><button onclick="exportRun(\'' + r.id + '\')" class="btn bg bs">Export</button> <button onclick="deleteRun(\'' + r.id + '\')" class="btn br bs">Delete</button></div></div><div class="ri-stats"><span><strong>' + r.keywords.length + '</strong> total keywords</span><span><strong>' + unique + '</strong> unique</span></div></div>';
});
}
document.getElementById("runsList").innerHTML = html;
document.getElementById("manageModal").classList.add("show");
};
document.getElementById("closeManage").onclick = function() {
document.getElementById("manageModal").classList.remove("show");
};
window.exportRun = function(id) {
const runs = getRuns();
const run = runs.find(r => r.id === id);
if (!run) {
alert("Run not found");
return;
}
const rows = [["Source", "Keyword", "Volume", "CPC", "Competition"]];
run.keywords.forEach(k => {
const p = k.data.split(";").map(v => v.replace(/"/g, "").trim());
rows.push([k.box, p[0] || "", p[1] || "", p[2] || "", p[3] || ""]);
});
const csv = rows.map(row => row.map(cell => {
const s = String(cell);
return (s.includes(",") || s.includes("\n") || s.includes('"')) ? '"' + s.replace(/"/g, '""') + '"' : s;
}).join(",")).join("\n");
const blob = new Blob(["\uFEFF" + csv], {type: "text/csv"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = run.name.replace(/[^a-z0-9]/gi, "_") + "_" + Date.now() + ".csv";
a.click();
URL.revokeObjectURL(url);
alert("Exported: " + run.name);
};
window.deleteRun = function(id) {
const runs = getRuns();
const run = runs.find(r => r.id === id);
if (!run) {
alert("Run not found");
return;
}
if (!confirm('Delete "' + run.name + '" with ' + run.keywords.length + ' keywords?\n\nThis cannot be undone!')) {
return;
}
localStorage.removeItem(id);
if (getCurrentRun() === id) {
localStorage.removeItem("kw_current_run");
}
alert("Deleted: " + run.name);
updateStats();
updateRunSelector();
document.getElementById("manageBtn").click();
};
document.getElementById("dlBtn").onclick = function() {
const curr = getCurrentRun();
if (!curr) {
alert("No run selected");
return;
}
const runs = getRuns();
const run = runs.find(r => r.id === curr);
if (!run) {
alert("Run not found");
return;
}
exportRun(run.id);
};
document.getElementById("cpBtn").onclick = function() {
const curr = getCurrentRun();
if (!curr) {
alert("No run selected");
return;
}
const runs = getRuns();
const run = runs.find(r => r.id === curr);
if (!run) {
alert("Run not found");
return;
}
const rows = [["Source", "Keyword", "Volume", "CPC", "Competition"]];
run.keywords.forEach(k => {
const p = k.data.split(";").map(v => v.replace(/"/g, "").trim());
rows.push([k.box, p[0] || "", p[1] || "", p[2] || "", p[3] || ""]);
});
const txt = rows.map(row => row.join("\t")).join("\n");
navigator.clipboard.writeText(txt).then(() => {
alert("Copied " + run.keywords.length + " keywords!\nPaste into Google Sheets with Ctrl+V");
}).catch(() => {
const ta = document.createElement("textarea");
ta.value = txt;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
alert("Copied!");
});
};
updateStats();
updateRunSelector();
updateTargetRunInfo();
}
</script>
</body>
</html>