Skip to content

Instantly share code, notes, and snippets.

@srghma
Created May 17, 2026 14:27
Show Gist options
  • Select an option

  • Save srghma/3a18ab5450012cc649d17c8769d6a345 to your computer and use it in GitHub Desktop.

Select an option

Save srghma/3a18ab5450012cc649d17c8769d6a345 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Djinni Quick Apply Pro
// @namespace http://tampermonkey.net/
// @version 2.2.0
// @description Automate job applications on Djinni.co directly via Tampermonkey
// @author You
// @match https://djinni.co/*
// @grant none
// ==/UserScript==
(function () {
"use strict";
/* ========================================================================
1. STATE MANAGEMENT (localStorage)
======================================================================== */
const STATE_KEY = "djinni_bot_state";
const POS_KEY = "djinni_bot_pos";
const defaultState = {
queue: [],
salary: 3000,
isRunning: false,
status: "Idle",
};
const Storage = {
get: () => {
try {
const data = localStorage.getItem(STATE_KEY);
return data ? JSON.parse(data) : defaultState;
} catch {
return defaultState;
}
},
set: (state) => {
localStorage.setItem(STATE_KEY, JSON.stringify(state));
renderUI(); // Automatically sync UI on state change
},
update: (updates) => {
const current = Storage.get();
const next = { ...current, ...updates };
Storage.set(next);
return next;
},
};
/* ========================================================================
2. DOM UTILITIES
======================================================================== */
const isVisible = (el) => {
if (!el || !(el instanceof HTMLElement)) return false;
if (el.tagName.toLowerCase() === "input" && el.type === "hidden") return false;
const style = window.getComputedStyle(el);
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
return false;
}
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return false;
return true;
};
const waitForVisibleElement = async (selector, timeout = 5000) => {
const start = Date.now();
while (Date.now() - start < timeout) {
const el = document.querySelector(selector);
if (isVisible(el)) return el;
await new Promise((r) => setTimeout(r, 200));
}
return null;
};
/* ========================================================================
3. BACKGROUND FETCH CONTROLLER (MACRO 2)
======================================================================== */
let isFetchingQueue = false;
const processFetchQueue = async () => {
if (isFetchingQueue) return;
isFetchingQueue = true;
while (true) {
const state = Storage.get();
const itemToFetch = state.queue.find((q) => q.fetchStatus === "pending");
if (!itemToFetch) break; // Finished queue
// Update UI to show fetching
Storage.update({
queue: state.queue.map((q) =>
q.id === itemToFetch.id ? { ...q, fetchStatus: "fetching..." } : q
),
});
try {
const res = await fetch(itemToFetch.url);
const html = await res.text();
// Check for specific Djinni statuses
const alreadyApplied = html.includes("You've applied to this job already");
const hasConversation = html.includes("You have a conversation with this recruiter already");
const canApply = html.includes("js-inbox-toggle-reply-form") || html.includes("Apply for the job");
const countryIssue =
html.includes("from your location") ||
html.includes("from your country") ||
html.toLowerCase().includes("does not hire") ||
html.toLowerCase().includes("doesn't hire");
let status = "new";
let shouldKeepChecked = canApply && !alreadyApplied;
if (alreadyApplied) {
status = "cannot_apply_you_already_applied";
} else if (hasConversation) {
if (canApply) {
status = "have_conversation_already_but_job_is_new_and_can_apply";
} else if (countryIssue) {
status = "have_conversation_already_but_job_is_new_and_cannot_apply_bc_they_dont_hire_from_my_country";
} else {
status = "have_conversation_already_cannot_apply_other_reason";
}
} else {
if (canApply) {
status = "dont_have_conversation_job_is_new_and_can_apply";
} else if (countryIssue) {
status = "dont_have_conversation_job_is_new_and_cannot_apply_bc_they_dont_hire_from_my_country";
} else {
status = "cannot_apply_other_reason";
}
}
const latestState = Storage.get();
Storage.update({
queue: latestState.queue.map((q) =>
q.id === itemToFetch.id
? { ...q, fetchStatus: status, active: shouldKeepChecked }
: q
),
});
} catch (e) {
console.error("[Djinni Bot] Fetch Error:", e);
const latestState = Storage.get();
Storage.update({
queue: latestState.queue.map((q) =>
q.id === itemToFetch.id ? { ...q, fetchStatus: "error_fetching_page" } : q
),
});
}
// 5 Seconds delay as requested to respect rate limits
await new Promise((r) => setTimeout(r, 5000));
}
isFetchingQueue = false;
};
/* ========================================================================
4. AUTOMATION LOGIC (MACRO)
======================================================================== */
const finishJob = (reason, isSuccess = true) => {
console.log(`[Djinni Bot] ${reason}`);
const state = Storage.get();
state.queue.shift(); // Remove the current job from the queue
state.status = isSuccess ? "Job applied! Moving to next..." : `Skipped: ${reason}`;
Storage.set(state);
setTimeout(() => {
processQueue(); // proceed to next item
}, 1500);
};
const pauseJob = (reason) => {
console.warn(`[Djinni Bot] Paused: ${reason}`);
Storage.update({ isRunning: false, status: reason });
};
const runApplyMacro = async (salary) => {
console.log("[Djinni Bot] Starting macro...");
Storage.update({ status: "Running automation..." });
// Let the DOM settle
await new Promise((r) => setTimeout(r, 1000));
const bodyText = document.body.innerText.replace(/\s+/g, " ");
const isExistingOkVisible = isVisible(document.getElementById("applied_ok"));
const alreadyApplied = bodyText.includes("You've applied to this job already") || isExistingOkVisible;
// 1. Hard Block: Already Applied
if (alreadyApplied) {
finishJob("Already applied to this specific job", false);
return;
}
// 2. Look for Apply Button (Overrides "Existing Conversation" warning)
const applyBtn = document.querySelector(".js-inbox-toggle-reply-form");
const isApplyBtnVisible = isVisible(applyBtn);
if (!isApplyBtnVisible) {
finishJob("Apply button not found or not permitted", false);
return;
}
// 3. Click Apply
console.log("[Djinni Bot] Found Apply button, clicking...");
applyBtn.scrollIntoView({ behavior: "smooth", block: "center" });
await new Promise((r) => setTimeout(r, 800));
applyBtn.click();
const formTextarea = await waitForVisibleElement("#message", 5000);
if (!formTextarea) {
pauseJob("Form did not load in time");
return;
}
// 4. Handle Template
const textarea = document.getElementById("message");
if (textarea && textarea.tagName === "TEXTAREA" && !textarea.value.trim()) {
const template = document.querySelector(".js-template-put");
if (isVisible(template)) {
template.click();
let attempts = 0;
while (!textarea.value.trim() && attempts < 20) {
await new Promise((r) => setTimeout(r, 200));
attempts++;
}
}
}
// 5. Handle Salary
const salaryBtn = document.querySelector(".js-salary-toggle-btn");
if (isVisible(salaryBtn)) salaryBtn.click();
const salaryInput = document.querySelector(".js-salary-input");
if (isVisible(salaryInput) && salaryInput.tagName === "INPUT") {
const maxAttr = salaryInput.getAttribute("max");
let finalSalary = salary;
if (maxAttr) {
const maxVal = parseInt(maxAttr, 10);
if (salary > maxVal) finalSalary = maxVal;
}
salaryInput.value = finalSalary.toString();
salaryInput.dispatchEvent(new Event("input", { bubbles: true }));
salaryInput.dispatchEvent(new Event("change", { bubbles: true }));
}
await new Promise((r) => setTimeout(r, 1500)); // Allow HTMX/DOM bindings to settle
// 6. Check Required Fields
const allRequiredElements = document.querySelectorAll("input[required], textarea[required], select[required]");
const missingField = Array.from(allRequiredElements).find((el) => {
if (!isVisible(el)) return false;
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
if (el.type === "checkbox" || el.type === "radio") return !el.checked;
return !el.value.trim();
}
if (el.tagName === "SELECT") return !el.value.trim();
return false;
});
if (missingField) {
console.warn("[Djinni Bot] Missing field:", missingField);
pauseJob("Please fill missing required fields");
return;
}
// 7. Submit
const submitBtn = document.getElementById("job_apply");
if (isVisible(submitBtn)) {
submitBtn.scrollIntoView({ behavior: "smooth", block: "center" });
await new Promise((r) => setTimeout(r, 500));
submitBtn.click();
} else {
pauseJob("Submit button not visible");
return;
}
// 8. Observe for Success
let timeSpentWaiting = 0;
const checkSuccessInterval = setInterval(() => {
timeSpentWaiting += 500;
const isOkVisible = isVisible(document.getElementById("applied_ok"));
const isToastVisible = isVisible(document.getElementById("candidate-notification"));
if (isOkVisible || isToastVisible) {
clearInterval(checkSuccessInterval);
finishJob("Successfully Applied", true);
}
if (timeSpentWaiting >= 15000) {
clearInterval(checkSuccessInterval);
pauseJob("Timeout waiting for success message");
}
}, 500);
};
/* ========================================================================
5. QUEUE CONTROLLER
======================================================================== */
const processQueue = () => {
const state = Storage.get();
if (!state.isRunning) return;
if (state.queue.length === 0) {
Storage.update({ isRunning: false, status: "Queue finished." });
return;
}
const currentJob = state.queue[0];
if (!currentJob.active) {
finishJob("Job unchecked/invalid, skipping...", false);
return;
}
const match = window.location.href.match(/\/jobs\/(\d+)/);
const currentIdOnPage = match ? match[1] : null;
if (currentIdOnPage === currentJob.id) {
setTimeout(() => runApplyMacro(state.salary), 1000);
} else {
Storage.update({ status: `Loading ${currentJob.title.substring(0, 15)}...` });
window.location.href = currentJob.url;
}
};
/* ========================================================================
6. UI INJECTION & RENDER LOOP
======================================================================== */
const injectStyles = () => {
const style = document.createElement("style");
style.textContent = `
#dj-bot-widget {
position: fixed; width: 340px; background: white; border: 2px solid #2563eb;
border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); z-index: 999999;
font-family: ui-sans-serif, system-ui, sans-serif; color: #1f2937;
}
.dj-drag-handle {
background: #eff6ff; color: #2563eb; font-size: 10px; font-weight: bold;
text-align: center; padding: 6px; cursor: move; user-select: none;
border-top-left-radius: 6px; border-top-right-radius: 6px; border-bottom: 1px solid #bfdbfe;
}
.dj-content { padding: 12px; }
.dj-input-group { margin-bottom: 12px; }
.dj-input-group label { display: block; font-size: 12px; font-weight: bold; margin-bottom: 4px; }
.dj-input-group input { width: 100%; padding: 6px; border: 1px solid #d1d5db; border-radius: 4px; box-sizing: border-box; }
.dj-queue-container { max-height: 200px; overflow-y: auto; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 4px; padding: 6px; margin-bottom: 12px; }
.dj-queue-header { font-size: 10px; font-weight: bold; text-transform: uppercase; color: #6b7280; margin-bottom: 6px; }
.dj-queue-item { display: flex; align-items: center; gap: 8px; padding: 6px; border: 1px solid #e5e7eb; border-radius: 4px; margin-bottom: 6px; background: white; flex-wrap: wrap; }
.dj-queue-item.dj-active-item { background: #eff6ff; border-color: #93c5fd; }
.dj-queue-item a { font-size: 10px; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #1d4ed8; text-decoration: none; min-width: 150px; }
.dj-queue-item a:hover { text-decoration: underline; }
.dj-queue-item button { background: none; border: none; color: #ef4444; cursor: pointer; padding: 2px; font-size: 14px; }
.dj-queue-item button:hover { background: #fee2e2; border-radius: 4px; }
.dj-actions { display: flex; gap: 8px; }
.dj-btn { flex: 1; padding: 8px; border: none; border-radius: 4px; font-weight: bold; font-size: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 4px;}
.dj-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.dj-btn-green { background: #16a34a; color: white; }
.dj-btn-green:hover { background: #15803d; }
.dj-btn-yellow { background: #eab308; color: white; }
.dj-btn-yellow:hover { background: #ca8a04; }
.dj-btn-gray { background: #e5e7eb; color: #4b5563; flex: 0.3; }
.dj-btn-gray:hover { background: #d1d5db; }
.dj-btn-blue { background: #2563eb; color: white; width: 100%; margin-top: 8px; }
.dj-btn-blue:hover { background: #1d4ed8; }
.dj-status { margin-top: 10px; font-size: 10px; text-align: center; font-weight: 500; color: #6b7280; }
.dj-status span { color: #2563eb; }
.helper-cb { margin-right: 12px; transform: scale(1.5); accent-color: #2563eb; cursor: pointer; }
`;
document.head.appendChild(style);
};
const createWidget = () => {
const isJobPage = /^\/jobs\/\d+/.test(window.location.pathname);
const widget = document.createElement("div");
widget.id = "dj-bot-widget";
widget.innerHTML = `
<div class="dj-drag-handle" id="dj-drag-handle">≡ DRAG TO REPOSITION ≡</div>
<div class="dj-content">
<div class="dj-input-group">
<label>Desired Salary ($)</label>
<input type="number" id="dj-salary" />
</div>
<div class="dj-queue-container">
<div class="dj-queue-header">Queue (<span id="dj-q-count">0</span>)</div>
<div id="dj-queue-list"></div>
</div>
<div class="dj-actions">
<button id="dj-btn-run" class="dj-btn dj-btn-green">▶ RUN</button>
<button id="dj-btn-pause" class="dj-btn dj-btn-yellow" style="display:none">⏸ PAUSE</button>
<button id="dj-btn-clear" class="dj-btn dj-btn-gray" title="Clear Queue">⏹</button>
</div>
${isJobPage ? `<button id="dj-btn-apply-current" class="dj-btn dj-btn-blue">⚡ Apply for Current Job</button>` : ""}
<div class="dj-status">Status: <span id="dj-status-text">Idle</span></div>
</div>
`;
document.body.appendChild(widget);
// Make Draggable
const handle = document.getElementById("dj-drag-handle");
let isDragging = false, startX, startY, initialX, initialY;
try {
const savedPos = JSON.parse(localStorage.getItem(POS_KEY));
if (savedPos) {
widget.style.left = savedPos.left;
widget.style.top = savedPos.top;
widget.style.bottom = "auto";
} else {
widget.style.left = "20px";
widget.style.bottom = "20px";
}
} catch {
widget.style.left = "20px";
widget.style.bottom = "20px";
}
handle.addEventListener("mousedown", (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
initialX = widget.offsetLeft;
initialY = widget.offsetTop;
document.body.style.userSelect = "none";
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
widget.style.left = `${initialX + (e.clientX - startX)}px`;
widget.style.top = `${initialY + (e.clientY - startY)}px`;
widget.style.bottom = "auto";
});
document.addEventListener("mouseup", () => {
if (isDragging) {
isDragging = false;
document.body.style.userSelect = "";
localStorage.setItem(POS_KEY, JSON.stringify({ left: widget.style.left, top: widget.style.top }));
}
});
document.getElementById("dj-salary").addEventListener("change", (e) => Storage.update({ salary: parseInt(e.target.value) || 0 }));
document.getElementById("dj-btn-run").addEventListener("click", () => {
Storage.update({ isRunning: true });
processQueue();
});
document.getElementById("dj-btn-pause").addEventListener("click", () => Storage.update({ isRunning: false, status: "Paused manually" }));
document.getElementById("dj-btn-clear").addEventListener("click", () => Storage.update({ isRunning: false, queue: [], status: "Queue cleared" }));
if (isJobPage) {
document.getElementById("dj-btn-apply-current").addEventListener("click", () => {
const url = window.location.href;
const title = document.querySelector("h1")?.innerText || "Current Job";
const match = url.match(/\/jobs\/(\d+)/);
const jobId = match ? match[1] : null;
if (jobId) {
const state = Storage.get();
const filteredQ = state.queue.filter((q) => q.id !== jobId);
filteredQ.unshift({ id: jobId, url, title, active: true, fetchStatus: "pending" });
Storage.update({ queue: filteredQ, isRunning: true });
setTimeout(processFetchQueue, 500); // Trigger background fetch logic
processQueue();
}
});
}
renderUI();
};
const renderUI = () => {
const state = Storage.get();
const salaryInput = document.getElementById("dj-salary");
if (salaryInput && document.activeElement !== salaryInput) {
salaryInput.value = state.salary;
salaryInput.disabled = state.isRunning;
}
const qCount = document.getElementById("dj-q-count");
if (qCount) qCount.innerText = state.queue.length;
const listContainer = document.getElementById("dj-queue-list");
if (listContainer) {
if (state.queue.length === 0) {
listContainer.innerHTML = `<div style="font-size:10px; color:#9ca3af; padding:4px;">Select jobs on the list...</div>`;
} else {
listContainer.innerHTML = state.queue.map((item, idx) => `
<div class="dj-queue-item ${idx === 0 ? "dj-active-item" : ""}">
<div style="display: flex; align-items: center; width: 100%; gap: 8px;">
<input type="checkbox" class="dj-q-cb" data-id="${item.id}" ${item.active ? "checked" : ""}>
<a href="${item.url}" target="_blank" title="${item.title}">${item.title}</a>
<button class="dj-q-del" data-id="${item.id}">✖</button>
</div>
${item.fetchStatus ? `
<div style="flex-basis: 100%; font-size: 9px; color: ${item.fetchStatus.includes('cannot_apply') ? '#ef4444' : (item.fetchStatus === 'pending' || item.fetchStatus === 'fetching...' ? '#eab308' : '#16a34a')}; margin-top: 2px; padding-left: 20px; word-break: break-all;">
${item.fetchStatus}
</div>` : ''}
</div>
`).join("");
document.querySelectorAll(".dj-q-cb").forEach((cb) => {
cb.addEventListener("change", (e) => {
const id = e.target.getAttribute("data-id");
const st = Storage.get();
const newQ = st.queue.map((q) => (q.id === id ? { ...q, active: e.target.checked } : q));
Storage.update({ queue: newQ });
});
});
document.querySelectorAll(".dj-q-del").forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = e.target.getAttribute("data-id");
const st = Storage.get();
Storage.update({ queue: st.queue.filter((q) => q.id !== id) });
});
});
}
}
const btnRun = document.getElementById("dj-btn-run");
const btnPause = document.getElementById("dj-btn-pause");
if (btnRun && btnPause) {
if (state.isRunning) {
btnRun.style.display = "none";
btnPause.style.display = "flex";
} else {
btnRun.style.display = "flex";
btnRun.disabled = state.queue.length === 0;
btnPause.style.display = "none";
}
}
const statusText = document.getElementById("dj-status-text");
if (statusText) statusText.innerText = state.status;
document.querySelectorAll(".helper-cb").forEach((cb) => {
const jobId = cb.getAttribute("data-jobid");
cb.checked = state.queue.some((q) => q.id === jobId);
});
};
/* ========================================================================
7. LIST PAGE CHECKBOX INJECTION
======================================================================== */
const injectCheckboxes = () => {
const items = document.querySelectorAll(".job-item:not(.helper-ready)");
const state = Storage.get();
items.forEach((item) => {
item.classList.add("helper-ready");
const linkEl = item.querySelector(".job_item__header-link") || item.querySelector("a.profile");
if (!linkEl) return;
const title = item.querySelector("h2")?.innerText?.trim() || item.querySelector(".job-list-item__link")?.innerText?.trim() || "Job";
const url = linkEl.href;
const match = url.match(/\/jobs\/(\d+)/);
const jobId = match ? match[1] : item.id.replace("job-item-", "");
const cb = document.createElement("input");
cb.type = "checkbox";
cb.className = "helper-cb";
cb.setAttribute("data-jobid", jobId);
cb.checked = state.queue.some((q) => q.id === jobId);
cb.addEventListener("change", (e) => {
const isChecked = e.target.checked;
const freshState = Storage.get();
let newQ = [...freshState.queue];
if (isChecked) {
if (!newQ.some(q => q.id === jobId)) {
newQ.push({ id: jobId, url, title, active: true, fetchStatus: "pending" });
setTimeout(processFetchQueue, 500); // Trigger queue handler
}
} else {
newQ = newQ.filter((q) => q.id !== jobId);
}
Storage.update({ queue: newQ });
});
const targetCol = item.querySelector(".col-auto") || item.querySelector("h2")?.parentElement || item;
if (targetCol) {
const wrapper = document.createElement("div");
wrapper.style.display = "inline-flex";
wrapper.style.alignItems = "center";
wrapper.appendChild(cb);
targetCol.prepend(wrapper);
}
});
};
/* ========================================================================
8. INITIALIZATION
======================================================================== */
const init = () => {
console.log("[Djinni Bot] Initializing...");
injectStyles();
createWidget();
const observer = new MutationObserver(() => injectCheckboxes());
observer.observe(document.body, { childList: true, subtree: true });
injectCheckboxes();
const state = Storage.get();
// Jumpstart fetch queue if anything was left pending
if (state.queue.some(q => q.fetchStatus === "pending" || q.fetchStatus === "fetching...")) {
Storage.update({ queue: state.queue.map(q => q.fetchStatus === "fetching..." ? { ...q, fetchStatus: "pending" } : q) });
setTimeout(processFetchQueue, 1000);
}
if (state.isRunning && state.queue.length > 0) {
setTimeout(() => processQueue(), 1000);
}
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
// ==UserScript==
// @name Google Maps Scraper Widget
// @namespace http://tampermonkey.net/
// @version 2.1
// @description Extracts Google Maps business data precisely and allows auto-scrolling
// @author You
// @match https://www.google.com/maps/*
// @grant GM_setClipboard
// ==/UserScript==
(function() {
'use strict';
// ==========================================
// 1. Create the floating UI Widget
// ==========================================
const widget = document.createElement('div');
widget.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 999999;
background: white;
border: 1px solid #ccc;
padding: 16px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
font-family: Arial, sans-serif;
font-size: 14px;
display: flex;
flex-direction: column;
gap: 12px;
width: 260px;
`;
const title = document.createElement('strong');
title.innerText = '🗺️ Maps Scraper';
title.style.textAlign = 'center';
const scrollBtn = document.createElement('button');
scrollBtn.innerText = 'Scroll to End';
scrollBtn.style.cssText = 'padding: 10px; cursor: pointer; background: #1a73e8; color: white; border: none; border-radius: 4px; font-weight: bold; transition: 0.2s;';
const copyBtn = document.createElement('button');
copyBtn.innerText = 'Copy Visible Info to JSON';
copyBtn.style.cssText = 'padding: 10px; cursor: pointer; background: #34a853; color: white; border: none; border-radius: 4px; font-weight: bold;';
const statusText = document.createElement('div');
statusText.style.cssText = 'font-size: 12px; color: #555; text-align: center; font-weight: bold;';
statusText.innerText = 'Ready';
widget.appendChild(title);
widget.appendChild(scrollBtn);
widget.appendChild(copyBtn);
widget.appendChild(statusText);
document.body.appendChild(widget);
// ==========================================
// 2. Auto-Scrolling Logic
// ==========================================
let scrollInterval = null;
let isScrolling = false;
let lastScrollHeight = 0;
let unchangedTicks = 0;
function stopScrolling(reason) {
clearInterval(scrollInterval);
isScrolling = false;
scrollBtn.innerText = 'Scroll to End';
scrollBtn.style.background = '#1a73e8';
statusText.innerText = reason;
statusText.style.color = '#1a73e8';
}
scrollBtn.addEventListener('click', () => {
const feed = document.querySelector('div[role="feed"]');
if (!feed) {
statusText.innerText = 'List not found. Search first!';
statusText.style.color = 'red';
return;
}
if (isScrolling) {
stopScrolling('Scrolling stopped manually.');
} else {
isScrolling = true;
scrollBtn.innerText = 'Stop Scrolling';
scrollBtn.style.background = '#ea4335';
statusText.innerText = 'Scrolling...';
statusText.style.color = '#ea4335';
lastScrollHeight = 0;
unchangedTicks = 0;
scrollInterval = setInterval(() => {
feed.scrollTop = feed.scrollHeight;
// Detection 1: Check if Google explicitly says the list ended (.HlvSq or .PbZDve classes)
const endMessage1 = document.querySelector('.HlvSq');
const endMessage2 = document.querySelector('.PbZDve');
if ((endMessage1 && endMessage1.innerText.length > 0) || (endMessage2 && endMessage2.innerText.length > 0)) {
stopScrolling('Reached end of list!');
return;
}
// Detection 2: Check if height hasn't changed for ~2 seconds (4 ticks)
if (feed.scrollHeight === lastScrollHeight) {
unchangedTicks++;
if (unchangedTicks >= 4) {
stopScrolling('Reached end of list!');
}
} else {
lastScrollHeight = feed.scrollHeight;
unchangedTicks = 0; // reset
}
}, 500);
}
});
// ==========================================
// 3. Precise Data Extraction Logic
// ==========================================
copyBtn.addEventListener('click', () => {
const articles = document.querySelectorAll('div[role="article"]');
if (articles.length === 0) {
statusText.innerText = 'No items found on page.';
statusText.style.color = 'red';
return;
}
const data = Array.from(document.querySelectorAll('div[role="article"]')).map(infoContainer => {
// 1. Name
const nameEl = infoContainer.querySelector('.qBF1Pd');
const name = nameEl ? nameEl.textContent.trim() : null;
// 2. URLs and Coordinates
const mainLinkEl = infoContainer.querySelector('a.hfpxzc');
const googleMapsUrl = mainLinkEl ? mainLinkEl.href : null;
let lat = null, lng = null;
if (googleMapsUrl) {
const latMatch = googleMapsUrl.match(/!3d([0-9.-]+)/);
const lngMatch = googleMapsUrl.match(/!4d([0-9.-]+)/);
if (latMatch) lat = parseFloat(latMatch[1]);
if (lngMatch) lng = parseFloat(lngMatch[1]);
}
// 3. Rating & Reviews
let rating = null;
let numberOfReviews = null;
const mw4etd = infoContainer.querySelector('.MW4etd');
if (mw4etd) {
rating = parseFloat(mw4etd.textContent.trim().replace(',', '.'));
} else {
const ratingSpan = infoContainer.querySelector('span[role="img"].ZkP5Je');
if (ratingSpan) {
const match = ratingSpan.getAttribute('aria-label').match(/([\d\.,]+)/);
if (match) rating = parseFloat(match[1].replace(',', '.'));
}
}
const reviewsEl = infoContainer.querySelector('.UY7F9');
if (reviewsEl) {
numberOfReviews = parseInt(reviewsEl.textContent.trim().replace(/[^\d]/g, ''), 10);
}
// 4. Phone
const phoneEl = infoContainer.querySelector('.UsdlK');
const phoneNumber = phoneEl ? phoneEl.textContent.trim() : null;
// 5. Website (Language agnostic: looks for exact class/icon or data-value)
let website = null;
const websiteEls = infoContainer.querySelectorAll('a');
for (let a of websiteEls) {
const href = a.href;
const hasIcon = a.querySelector('.Cw1rxd') !== null;
const hasDataValue = a.getAttribute('data-value') === 'Сайт' || a.getAttribute('data-value') === 'Website' || a.getAttribute('data-value') === 'Web site';
if (href && href.startsWith('http') && !href.includes('google.com/maps') && (hasIcon || hasDataValue)) {
website = href.trim();
break;
}
}
// 6. Address, Category, and Opening Status (DOM-based precise extraction)
let category = null;
let address = null;
let openingStatus = null;
// .W4Efsd > .W4Efsd targets the exact wrapper rows holding the strings
const detailRows = infoContainer.querySelectorAll('.W4Efsd > .W4Efsd');
if (detailRows.length > 0) {
// Row 1: Usually contains Category and Address
const row1Spans = Array.from(detailRows[0].querySelectorAll(':scope > span'));
const validTexts = [];
row1Spans.forEach(span => {
// Filter out the span entirely if it holds the accessibility/wheelchair icon
if (span.querySelector('.google-symbols')) return;
let text = span.textContent.trim().replace(/^·\s*/, '').replace(/\s*·$/, '').trim();
if (text) validTexts.push(text);
});
if (validTexts.length > 0) category = validTexts[0];
if (validTexts.length > 1) address = validTexts.slice(1).join(', ');
}
if (detailRows.length > 1) {
// Row 2: Usually contains Status and Phone Number
const row2Spans = Array.from(detailRows[1].querySelectorAll(':scope > span'));
const statusTexts = [];
row2Spans.forEach(span => {
// Filter out the span entirely if it holds the phone number (we already have it)
if (span.querySelector('.UsdlK') || span.classList.contains('UsdlK')) return;
let text = span.textContent.trim().replace(/^·\s*/, '').replace(/\s*·$/, '').trim();
if (text) statusTexts.push(text);
});
if (statusTexts.length > 0) {
openingStatus = statusTexts.join(' · ');
}
}
return {
name,
rating,
numberOfReviews,
category,
address,
openingStatus,
phoneNumber,
website,
googleMapsUrl,
latitude: lat,
longitude: lng
};
});
// Convert the final array to JSON
const jsonOutput = JSON.stringify(data, null, 2);
// Copy to clipboard using Tampermonkey API
GM_setClipboard(jsonOutput);
// Update UI
statusText.innerText = `✅ Copied ${data.length} items to clipboard!`;
statusText.style.color = '#34a853';
console.log("Extracted Data:", data);
});
})();
// ==UserScript==
// @name Indeed SmartApply Ultimate Dashboard
// @namespace http://tampermonkey.net/
// @version 4.0.0
// @description Automated filling and navigation for Indeed Smart Apply.
// @match https://smartapply.indeed.com/*
// @grant none
// ==/UserScript==
(function () {
"use strict";
const WIDGET_ID = "ind-ultimate-v4";
const SETTINGS_KEY = "ind_settings_v4"; // Only stores { autoContinue: bool }
const POS_KEY = "ind_dashboard_pos_v4";
// 1. DATA: Kept in code as requested
const FORM_DEFAULTS = {
"Current Location": "Phnom Penh",
"Preferred Location": "Remote",
"Country": "Cambodia",
"Current Designation": "Senior Software Engineer",
"Current Company": "no",
"Expected CTC": "500",
"Notice Period": "Immediate",
"Total Work Experience": "15",
"How Many Years of working in a role utilizing your Networking Experience, do you have?": "Master of Science in Computer Science",
"Networking Experience": "7+ Years",
"What does CDN stand for and what is it?": "Content Delivery Network.",
"Do you require a work permission?": "No",
"Address": "Phnom Penh",
"City": "Phnom Penh",
"State": "Phnom Penh",
"Postal Code": "12000",
"How many years of experience do you have in Customer Service?": "15",
"How many years of experience do you have in Customer Support?": "15",
"Please describe any certifications and experience you have with Salesforce": "I have extensive experience with Salesforce, Apex and Lightning Web Components.",
"Please describe your experience with GIT or other source control systems.": "I have extensive experience with GIT, including branching strategies, pull requests, and source control management.",
"Please describe your experience with DevOps and the Software Development Lifecycle.": "I have worked with CI/CD pipelines, Docker, and the full Software Development Lifecycle.",
"Please briefly describe any experience working at a startup. What do you like most about working at a startup?": "I enjoy the fast-paced, high-impact environment of startups where I can contribute to core product decisions."
};
// 2. HELPERS: Logic extracted into clean methods
const FieldHelper = {
normalize: (s) => s ? s.replace(/\s+/g, ' ').replace(/\*/g, '').trim().toLowerCase() : "",
getType: (container) => {
if (container.querySelector('input[type="radio"]')) return "radio";
if (container.querySelector('select')) return "select";
if (container.querySelector('textarea')) return "textarea";
if (container.querySelector('input[type="number"]')) return "number";
return "text";
},
// Checks if field is empty or Indeed flagged it red
isValid: (type, value, container) => {
if (!value || value.trim() === "") return false;
if (container.querySelector('[aria-invalid="true"]')) return false;
if (container.innerHTML.includes('Choose an option to continue')) return false;
if (type === "number" && isNaN(value)) return false;
if (type === "radio") {
const opts = Array.from(container.querySelectorAll('label')).map(l => FieldHelper.normalize(l.innerText));
return opts.some(o => o === FieldHelper.normalize(value) || o.includes(FieldHelper.normalize(value)));
}
return true;
},
fill: (type, container, value) => {
const cleanVal = FieldHelper.normalize(value);
if (type === "radio") {
container.querySelectorAll('label').forEach(lbl => {
const txt = FieldHelper.normalize(lbl.innerText);
if (txt === cleanVal || txt.includes(cleanVal)) {
const i = lbl.querySelector('input');
if (i) { i.checked = true; i.click(); i.dispatchEvent(new Event('change', {bubbles:true})); }
lbl.click();
lbl.querySelectorAll('span').forEach(s => s.click());
}
});
} else if (type === "select") {
const s = container.querySelector('select');
const opt = Array.from(s.options).find(o => FieldHelper.normalize(o.text).includes(cleanVal));
if (opt) { s.value = opt.value; s.dispatchEvent(new Event('change', {bubbles:true})); }
} else {
const i = container.querySelector('input, textarea');
if (!i) return;
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(i), 'value')?.set;
if (setter) setter.call(i, value);
i.dispatchEvent(new Event('input', { bubbles: true }));
i.dispatchEvent(new Event('change', { bubbles: true }));
i.dispatchEvent(new Event('blur', { bubbles: true }));
}
}
};
// 3. AUTOMATION LOGIC
const Automation = {
getAutoContinue: () => JSON.parse(localStorage.getItem(SETTINGS_KEY))?.autoContinue || false,
// Find "Continue" or "Next" buttons on any page
getContinueButton: () => {
return document.querySelector('button[data-testid*="continue"]') ||
document.querySelector('button.mosaic-provider-module-apply-questions-6ja1uy') ||
document.querySelector('button.mosaic-provider-module-apply-contact-info-evwlos') ||
document.querySelector('.mosaic-provider-module-apply-resume-selection button');
},
run: () => {
const isQuestionsPage = !!document.querySelector('.ia-Questions-item');
let filledCount = 0;
if (isQuestionsPage) {
const questions = document.querySelectorAll('.ia-Questions-item');
questions.forEach(q => {
const label = FieldHelper.normalize(q.querySelector('label')?.innerText || "");
const key = Object.keys(FORM_DEFAULTS).find(k => label.includes(FieldHelper.normalize(k)));
if (key) {
FieldHelper.fill(FieldHelper.getType(q), q, FORM_DEFAULTS[key]);
filledCount++;
}
});
}
// AUTO-CONTINUE logic
if (Automation.getAutoContinue()) {
const btn = Automation.getContinueButton();
if (!btn) return;
// On questions page, only click if no errors visible
if (isQuestionsPage) {
const hasErrors = !!document.querySelector('[aria-invalid="true"]') || document.body.innerHTML.includes('Choose an option to continue');
if (!hasErrors) setTimeout(() => btn.click(), 1000);
} else {
// On simple pages (Resume, Profile, Location), just click
setTimeout(() => btn.click(), 1000);
}
}
}
};
// 4. UI LOGIC (No jitter re-render)
let lastHash = "";
const render = () => {
if (document.activeElement?.classList.contains('ib-val')) return;
const widget = document.getElementById(WIDGET_ID) || createWidget();
const body = widget.querySelector('.ib-body');
const pageQuestions = document.querySelectorAll('.ia-Questions-item');
const activeMap = new Map();
let currentHash = "";
pageQuestions.forEach(q => {
const txt = FieldHelper.normalize(q.querySelector('label')?.innerText || "");
activeMap.set(txt, { type: FieldHelper.getType(q), container: q });
currentHash += txt;
});
// Detect Profile/Resume simple pages
const isSimplePage = !pageQuestions.length && !!Automation.getContinueButton();
if (isSimplePage) currentHash = "simple-page";
if (currentHash === lastHash) {
updateValidation(activeMap);
return;
}
lastHash = currentHash;
const keys = Object.keys(FORM_DEFAULTS);
const activeKeys = keys.filter(k => Array.from(activeMap.keys()).some(am => am.includes(FieldHelper.normalize(k))));
const inactiveKeys = keys.filter(k => !activeKeys.includes(k));
body.innerHTML = [...activeKeys, ...inactiveKeys].map(k => {
const normK = FieldHelper.normalize(k);
const activeData = Array.from(activeMap.entries()).find(([m]) => m.includes(normK))?.[1];
const type = activeData ? activeData.type : "";
const invalid = activeData && !FieldHelper.isValid(type, FORM_DEFAULTS[k], activeData.container);
return `
<div class="ib-row ${activeData ? 'active' : ''} ${invalid ? 'invalid' : ''}" data-key="${k}">
<div class="ib-label-row">
<span class="ib-label">${k}</span>
${activeData ? `<span class="ib-type">${type.toUpperCase()}</span>` : ''}
</div>
<input type="text" class="ib-val" value="${FORM_DEFAULTS[k]}" readonly />
</div>
`;
}).join('') + `<button id="ib-manual-fill" class="ib-btn">⚡ FORCE FILL PAGE</button>`;
body.querySelector('#ib-manual-fill').onclick = Automation.run;
};
const updateValidation = (activeMap) => {
document.querySelectorAll('.ib-row').forEach(row => {
const k = row.dataset.key;
const normK = FieldHelper.normalize(k);
const activeData = Array.from(activeMap.entries()).find(([m]) => m.includes(normK))?.[1];
if (activeData) {
const valid = FieldHelper.isValid(activeData.type, FORM_DEFAULTS[k], activeData.container);
row.classList.toggle('invalid', !valid);
}
});
};
const createWidget = () => {
const widget = document.createElement("div");
widget.id = WIDGET_ID;
widget.innerHTML = `<div class="ib-head">INDEED AUTO-APPLY v4</div><div class="ib-body"></div><div class="ib-foot"></div>`;
document.body.appendChild(widget);
const foot = widget.querySelector('.ib-foot');
foot.innerHTML = `<input type="checkbox" id="ib-auto-cb" ${Automation.getAutoContinue() ? 'checked' : ''}><label for="ib-auto-cb">Auto-Continue (All Pages)</label>`;
foot.querySelector('#ib-auto-cb').onchange = (e) => {
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ autoContinue: e.target.checked }));
};
const head = widget.querySelector('.ib-head');
let isMoving = false, ox, oy;
head.onmousedown = (e) => { isMoving = true; ox = e.clientX - widget.offsetLeft; oy = e.clientY - widget.offsetTop; };
document.onmousemove = (e) => { if (isMoving) { widget.style.left = (e.clientX - ox) + 'px'; widget.style.top = (e.clientY - oy) + 'px'; widget.style.right = 'auto'; } };
document.onmouseup = () => { if (isMoving) { isMoving = false; localStorage.setItem(POS_KEY, JSON.stringify({ left: widget.style.left, top: widget.style.top })); } };
const saved = JSON.parse(localStorage.getItem(POS_KEY));
if (saved) Object.assign(widget.style, saved);
return widget;
};
const injectStyles = () => {
const style = document.createElement("style");
style.textContent = `
#${WIDGET_ID} { position: fixed; width: 330px; background: #fff; border: 2px solid #2557a7; border-radius: 8px; box-shadow: 0 10px 40px #0004; z-index: 10001; font-family: sans-serif; top: 10px; right: 10px; display: flex; flex-direction: column; }
.ib-head { background: #2557a7; color: #fff; padding: 10px; cursor: move; font-size: 11px; font-weight: bold; text-align: center; border-radius: 6px 6px 0 0; }
.ib-body { padding: 8px; max-height: 450px; overflow-y: auto; background: #f9f9f9; }
.ib-row { margin-bottom: 5px; padding: 6px; border-radius: 4px; border: 1px solid #ddd; background: #fff; opacity: 0.7; }
.ib-row.active { opacity: 1; border-color: #38b2ac; background: #e6fffa; border-left: 4px solid #38b2ac; }
.ib-row.invalid { border-color: #e53e3e !important; background: #fff5f5 !important; }
.ib-label-row { display: flex; justify-content: space-between; gap: 4px; margin-bottom: 3px;}
.ib-label { font-size: 9px; font-weight: bold; color: #555; line-height: 1; flex: 1; }
.ib-type { font-size: 8px; font-weight: bold; background: #edf2f7; padding: 1px 3px; border-radius: 2px; }
.ib-val { width: 100%; border: none; background: transparent; font-size: 11px; color: #333; outline: none; pointer-events: none; }
.ib-btn { width: 100%; padding: 10px; background: #2557a7; color: #fff; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; margin-top: 5px; font-size: 11px; }
.ib-foot { font-size: 11px; padding: 8px; border-top: 1px solid #eee; display: flex; align-items: center; gap: 5px; background: #fff; border-radius: 0 0 8px 8px; }
`;
document.head.appendChild(style);
};
// INIT
injectStyles();
setInterval(() => {
render();
Automation.run();
}, 1500);
})();
// ==UserScript==
// @name Indeed.com: Highlight non-sponsored jobs
// @namespace localhost
// @description This script just highlights sponsored and non-sponsored jobs by different colors for better visualization.
// @include *.indeed.com/*
// @include *.indeed.co.uk/*
// @include *.indeed.*/*
// @run-at document-end
// @author lukie80
// @copyright Creative Commons Attribution-ShareAlike 3.0 Unported (CC-BY-SA 3.0)
// @license http://creativecommons.org/licenses/by-sa/3.0/
// @version 1.5
// @lastupdated 2016.06.15
//
// @downloadURL https://update.greasyfork.org/scripts/20617/Indeedcom%3A%20Highlight%20non-sponsored%20jobs.user.js
// @updateURL https://update.greasyfork.org/scripts/20617/Indeedcom%3A%20Highlight%20non-sponsored%20jobs.meta.js
// ==/UserScript==
//-------------------------------------------------------------------------------------------------------------------
//source: http://stackoverflow.com/a/9496574 - not needed for script, just here for educational purposes
function getAllElementsWithAttribute(attribute)
{
var matchingElements = [];
var allElements = document.getElementsByTagName('*');
for (var i = 0, n = allElements.length; i < n; i++)
{
if (allElements[i].getAttribute(attribute) !== null)
{
// Element exists with attribute. Add to array.
matchingElements.push(allElements[i]);
}
}
return matchingElements;
}
//source: http://stackoverflow.com/a/4275177 - needed
function getElementsStartsWithId( id ) {
var children = document.body.getElementsByTagName('*');
var elements = [], child;
for (var i = 0, length = children.length; i < length; i++) {
child = children[i];
if (child.id.substr(0, id.length) == id)
elements.push(child);
}
return elements;
}
var goodDivs = getElementsStartsWithId("p_");
for (var i = 0; i < goodDivs.length; i++){
goodDivs[i].style.background = '#F8F8F8';
}
if (getElementsStartsWithId("pj_")[0]){
var badDivs = getElementsStartsWithId("pj_");
for (var i = 0; i < badDivs.length; i++){
badDivs[i].style.background = '#fdf9fd';
badDivs[i].style.border = 'thin solid #f7e6f7';
badDivs[i].style.margin = "-1px -1px -1px -1px";
//badDivs[i].remove();
//this can remove the sponsored jobs but this is not suggested
//because they are not spam. However they are
//quantitative spam.
}
}
// create link company job search
if (document.getElementsByClassName("company")){
var companyEles = document.getElementsByClassName("company");
for (var i = 0; i < companyEles.length; i++){
var companyLinkEle = document.createElement('a');
companyLinkEle.href = "http:\/\/"+window.location.href.match(/[\w.]*indeed[\w.]+/)+"\/jobs?q=company:\""+encodeURI(companyEles[i].textContent.replace(/^\s*/, ''))+"\"&amp;l=";
companyLinkEle.innerHTML = companyEles[i].textContent;
companyLinkEle.setAttribute("target", "_blank");
companyEles[i].textContent="";
companyEles[i].appendChild(companyLinkEle);
}
}
//-------------------------------------------------------------------------------------------------------------------
// ==UserScript==
// @name WorkUa Quick Apply Pro
// @namespace http://tampermonkey.net/
// @version 1.3.0
// @description Automate job applications on Work.ua directly via Tampermonkey
// @author You
// @match https://www.work.ua/*
// @grant none
// ==/UserScript==
(() => {
"use strict";
/* ========================================================================
1. CONFIG & CONSTANTS
======================================================================== */
const KEYS = { STATE: "workua_bot_state", POS: "workua_bot_pos" };
const DEFAULT_STATE = {
queue: [],
coverLetter: "Really want to be part of Your team. Ok from Asia? https://srghma.github.io/resume/",
resumeKeyword: "Full stack developer",
isRunning: false,
status: "Idle",
};
const CLASSES = {
legacyWrapper: 'custom-job-checkbox-group btn btn-tertiary btn-icon btn-icon-left mr-lg',
tailwindWrapper: 'tw-font-medium tw-py-[7px] tw-min-h-[40px] tw-rounded-lg tw-whitespace-nowrap md:tw-text-var-md md:tw-font-semibold tw-px-sm tw-text-gray-900 tw-inline-flex tw-items-center tw-justify-center tw-mr-lg',
};
/* ========================================================================
2. UTILS
======================================================================== */
const Utils = {
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
isVisible: (el) => {
if (!el || !(el instanceof HTMLElement)) return false;
const style = window.getComputedStyle(el);
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
return el.getBoundingClientRect().width > 0;
},
waitForElement: async (selector, timeout = 5000) => {
const start = Date.now();
while (Date.now() - start < timeout) {
const el = document.querySelector(selector);
if (Utils.isVisible(el)) return el;
await Utils.sleep(200);
}
return null;
},
findByText: (selector, text) =>
Array.from(document.querySelectorAll(selector)).find(el => el.textContent.includes(text)),
// React relies on internal state; we must bypass it to set input values natively
setReactInput: (element, value) => {
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
setter.call(element, value);
element.dispatchEvent(new Event("input", { bubbles: true }));
element.dispatchEvent(new Event("change", { bubbles: true }));
},
// Blocks events from bubbling up to parent listeners (Fixes the redirect bug)
stopEventBubbling: (el) => {
['click', 'mousedown', 'mouseup', 'pointerdown'].forEach(evt =>
el.addEventListener(evt, e => e.stopPropagation())
);
}
};
/* ========================================================================
3. STATE STORE
======================================================================== */
const Store = {
get: () => {
try { return JSON.parse(localStorage.getItem(KEYS.STATE)) || DEFAULT_STATE; }
catch { return DEFAULT_STATE; }
},
set: (state) => {
localStorage.setItem(KEYS.STATE, JSON.stringify(state));
UI.render(); // Always sync UI when state changes
},
update: (updates) => Store.set({ ...Store.get(), ...updates })
};
/* ========================================================================
4. BOT LOGIC (MACRO)
======================================================================== */
const Bot = {
finishJob: (reason, isSuccess = true) => {
console.log(`[WorkUa Bot] ${reason}`);
const state = Store.get();
Store.update({
queue: state.queue.slice(1),
status: isSuccess ? "Job applied! Next..." : `Skipped: ${reason}`
});
setTimeout(Bot.processQueue, 1500);
},
pause: (reason) => {
console.warn(`[WorkUa Bot] Paused: ${reason}`);
Store.update({ isRunning: false, status: reason });
},
processQueue: () => {
const state = Store.get();
if (!state.isRunning) return;
if (state.queue.length === 0) return Store.update({ isRunning: false, status: "Queue finished." });
const currentJob = state.queue[0];
if (!currentJob.active) return Bot.finishJob("Job unchecked, skipping...", false);
const currentIdOnPage = window.location.href.match(/\/jobs\/(\d+)/)?.[1];
if (currentIdOnPage === currentJob.id) {
setTimeout(Bot.runMacro, 1000); // Execute Macro
} else {
Store.update({ status: `Loading ${currentJob.title.substring(0, 15)}...` });
const urlObj = new URL(currentJob.url, window.location.origin);
urlObj.searchParams.set('modal', 'send-resume');
window.location.href = urlObj.toString(); // Navigate
}
},
runMacro: async () => {
console.log("[WorkUa Bot] Starting macro...");
Store.update({ status: "Running automation..." });
const state = Store.get();
await Utils.sleep(1500);
// 1. Check if already applied
if (Utils.isVisible(document.querySelector('.already-sent'))) {
return Bot.finishJob("Already applied to this job", false);
}
// 2. Locate or trigger the modal form
let formEl = await Utils.waitForElement('form[autocomplete="off"].tw-card', 5000);
const noApplyBtn = Utils.findByText('button', 'Ні, не відгукатися');
if (noApplyBtn && Utils.isVisible(noApplyBtn)) {
noApplyBtn.click();
await Utils.sleep(500);
return Bot.finishJob("Skipped: Apply again modal detected", false);
}
if (!formEl) {
const applyBtn = document.querySelector('button[data-open-react-send-resume-modal="true"]');
if (applyBtn && Utils.isVisible(applyBtn)) {
applyBtn.click();
formEl = await Utils.waitForElement('form[autocomplete="off"].tw-card', 5000);
}
if (!formEl) return Bot.pause("Modal form did not load in time");
}
// 3. Select Resume
const labels = Array.from(document.querySelectorAll('form.tw-card label'));
const targetLabel = labels.find(l => l.innerText.toLowerCase().includes(state.resumeKeyword.toLowerCase()));
if (targetLabel) {
const radio = document.getElementById(targetLabel.getAttribute('for'));
if (radio && !radio.disabled) radio.click();
} else {
const fallbackRadio = document.querySelector('input[name="resume"]:not([disabled])');
if (fallbackRadio) fallbackRadio.click();
else return Bot.pause("No valid resume found to select.");
}
await Utils.sleep(500);
// 4. Fill Cover Letter
const addDescCheckbox = document.querySelector('input[name="addDescription"]');
if (addDescCheckbox && !addDescCheckbox.checked) {
addDescCheckbox.click();
await Utils.sleep(500);
}
const textarea = await Utils.waitForElement('textarea[name="description"]', 3000);
if (textarea) Utils.setReactInput(textarea, state.coverLetter);
else return Bot.pause("Description textarea not found");
await Utils.sleep(1000);
// 5. Submit
const submitBtn = Utils.findByText('button[type="submit"]', 'Надіслати');
if (submitBtn && Utils.isVisible(submitBtn)) {
submitBtn.click();
} else {
return Bot.pause("Submit button not found");
}
await Utils.sleep(3000);
Bot.finishJob("Successfully Applied", true);
}
};
/* ========================================================================
5. UI MODIFIERS (Widget & List Injector)
======================================================================== */
const UI = {
init: () => {
UI.injectGlobalStyles();
UI.createWidget();
const observer = new MutationObserver(() => UI.injectCheckboxesToList());
observer.observe(document.body, { childList: true, subtree: true });
UI.injectCheckboxesToList();
},
injectGlobalStyles: () => {
if (document.getElementById('wk-bot-styles')) return;
document.head.insertAdjacentHTML('beforeend', `
<style id="wk-bot-styles">
#wk-bot-widget { position: fixed; width: 340px; background: white; border: 2px solid #0056b3; border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); z-index: 999999; font-family: ui-sans-serif, system-ui, sans-serif; color: #1f2937; }
.wk-drag-handle { background: #e6f0fa; color: #0056b3; font-size: 10px; font-weight: bold; text-align: center; padding: 6px; cursor: move; user-select: none; border-radius: 6px 6px 0 0; border-bottom: 1px solid #b3d4ff; }
.wk-content { padding: 12px; }
.wk-input-group { margin-bottom: 8px; }
.wk-input-group label { display: block; font-size: 11px; font-weight: bold; margin-bottom: 2px; }
.wk-input-group input, .wk-input-group textarea { width: 100%; padding: 6px; border: 1px solid #d1d5db; border-radius: 4px; box-sizing: border-box; font-size:11px;}
.wk-input-group textarea { resize: vertical; height: 50px; }
.wk-queue-container { max-height: 150px; overflow-y: auto; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 4px; padding: 6px; margin-bottom: 12px; }
.wk-queue-header { font-size: 10px; font-weight: bold; text-transform: uppercase; color: #6b7280; margin-bottom: 6px; }
.wk-queue-item { display: flex; align-items: center; gap: 8px; padding: 6px; border: 1px solid #e5e7eb; border-radius: 4px; margin-bottom: 4px; background: white; }
.wk-queue-item.wk-active { background: #eff6ff; border-color: #93c5fd; }
.wk-queue-item a { font-size: 10px; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #0056b3; text-decoration: none; }
.wk-queue-item button { background: none; border: none; color: #ef4444; cursor: pointer; padding: 2px; font-size: 12px; }
.wk-actions { display: flex; gap: 8px; }
.wk-btn { flex: 1; padding: 8px; border: none; border-radius: 4px; font-weight: bold; font-size: 12px; cursor: pointer; }
.wk-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.wk-btn-green { background: #16a34a; color: white; }
.wk-btn-yellow { background: #eab308; color: white; }
.wk-btn-gray { background: #e5e7eb; color: #4b5563; flex: 0.3; }
.wk-btn-blue { background: #0056b3; color: white; width: 100%; margin-top: 8px; }
.wk-status { margin-top: 10px; font-size: 10px; text-align: center; font-weight: 500; color: #6b7280; }
</style>
`);
},
makeDraggable: (handle, widget) => {
let isDragging = false, startX, startY, initX, initY;
handle.addEventListener("mousedown", (e) => {
isDragging = true; startX = e.clientX; startY = e.clientY;
initX = widget.offsetLeft; initY = widget.offsetTop;
document.body.style.userSelect = "none";
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
widget.style.left = `${initX + (e.clientX - startX)}px`;
widget.style.top = `${initY + (e.clientY - startY)}px`;
});
document.addEventListener("mouseup", () => {
if (!isDragging) return;
isDragging = false;
document.body.style.userSelect = "";
localStorage.setItem(KEYS.POS, JSON.stringify({ left: widget.style.left, top: widget.style.top }));
});
},
createWidget: () => {
if (document.getElementById('wk-bot-widget')) return;
const isJobPage = /^\/jobs\/\d+/.test(window.location.pathname);
const pos = JSON.parse(localStorage.getItem(KEYS.POS) || '{"left":"20px","top":"100px"}');
document.body.insertAdjacentHTML('beforeend', `
<div id="wk-bot-widget" style="left:${pos.left}; top:${pos.top};">
<div class="wk-drag-handle" id="wk-drag-handle">≡ DRAG TO REPOSITION ≡</div>
<div class="wk-content">
<div class="wk-input-group"><label>Resume Keyword</label><input type="text" id="wk-resume" /></div>
<div class="wk-input-group"><label>Cover Letter Text</label><textarea id="wk-cover"></textarea></div>
<div class="wk-queue-container">
<div class="wk-queue-header">Queue (<span id="wk-q-count">0</span>)</div>
<div id="wk-queue-list"></div>
</div>
<div class="wk-actions">
<button id="wk-btn-run" class="wk-btn wk-btn-green">▶ RUN</button>
<button id="wk-btn-pause" class="wk-btn wk-btn-yellow" style="display:none">⏸ PAUSE</button>
<button id="wk-btn-clear" class="wk-btn wk-btn-gray">⏹</button>
</div>
${isJobPage ? `<button id="wk-btn-apply-current" class="wk-btn wk-btn-blue">⚡ Apply for Current Job</button>` : ""}
<div class="wk-status">Status: <span id="wk-status-text">Idle</span></div>
</div>
</div>
`);
UI.makeDraggable(document.getElementById("wk-drag-handle"), document.getElementById("wk-bot-widget"));
UI.attachWidgetListeners();
UI.render();
},
attachWidgetListeners: () => {
const el = id => document.getElementById(id);
el("wk-resume").addEventListener("input", e => Store.update({ resumeKeyword: e.target.value }));
el("wk-cover").addEventListener("input", e => Store.update({ coverLetter: e.target.value }));
el("wk-btn-clear").addEventListener("click", () => Store.update({ isRunning: false, queue: [], status: "Queue cleared" }));
el("wk-btn-pause").addEventListener("click", () => Store.update({ isRunning: false, status: "Paused manually" }));
el("wk-btn-run").addEventListener("click", () => {
Store.update({ isRunning: true });
Bot.processQueue();
});
// Handle individual queue items
document.getElementById("wk-queue-list").addEventListener("change", (e) => {
if(e.target.classList.contains("wk-q-cb")) {
const queue = Store.get().queue.map(q => q.id === e.target.dataset.id ? { ...q, active: e.target.checked } : q);
Store.update({ queue });
}
});
document.getElementById("wk-queue-list").addEventListener("click", (e) => {
if(e.target.classList.contains("wk-q-del")) {
const queue = Store.get().queue.filter(q => q.id !== e.target.dataset.id);
Store.update({ queue });
}
});
const applyCurrentBtn = el("wk-btn-apply-current");
if (applyCurrentBtn) {
applyCurrentBtn.addEventListener("click", () => {
const jobId = window.location.href.match(/\/jobs\/(\d+)/)?.[1];
if (!jobId) return;
const url = window.location.href.split('?')[0];
const title = document.querySelector("h1")?.innerText || "Current Job";
const queue = [{ id: jobId, url, title, active: true }, ...Store.get().queue.filter(q => q.id !== jobId)];
Store.update({ queue, isRunning: true });
Bot.processQueue();
});
}
},
render: () => {
const state = Store.get();
const el = id => document.getElementById(id);
// Update Inputs safely
if (document.activeElement !== el("wk-resume")) {
el("wk-resume").value = state.resumeKeyword;
el("wk-resume").disabled = state.isRunning;
}
if (document.activeElement !== el("wk-cover")) {
el("wk-cover").value = state.coverLetter;
el("wk-cover").disabled = state.isRunning;
}
el("wk-q-count").innerText = state.queue.length;
el("wk-status-text").innerText = state.status;
// Update Queue HTML
el("wk-queue-list").innerHTML = state.queue.length === 0
? `<div style="font-size:10px; color:#9ca3af;">Select jobs from the search list...</div>`
: state.queue.map((item, i) => `
<div class="wk-queue-item ${i === 0 ? "wk-active" : ""}">
<input type="checkbox" class="wk-q-cb" data-id="${item.id}" ${item.active ? "checked" : ""}>
<a href="${item.url}" target="_blank" title="${item.title}">${item.title}</a>
<button class="wk-q-del" data-id="${item.id}">✖</button>
</div>
`).join("");
// Update Action Buttons
el("wk-btn-run").style.display = state.isRunning ? "none" : "flex";
el("wk-btn-run").disabled = state.queue.length === 0;
el("wk-btn-pause").style.display = state.isRunning ? "flex" : "none";
// Sync injected page checkboxes
document.querySelectorAll(".helper-cb").forEach(cb => {
cb.checked = state.queue.some(q => q.id === cb.dataset.jobid);
});
},
parseCardForInjection: (card) => {
if (card.classList.contains('helper-ready') || card.innerHTML.includes('Вже відгукнулися')) return null;
const isNew = card.classList.contains('tw-card');
const titleEl = card.querySelector('h2 a');
if (!titleEl) return null;
const url = titleEl.href.split('?')[0];
const title = titleEl.innerText.trim();
const jobId = isNew ? card.id.replace('job-', '') : (card.getAttribute('data-id') || url.match(/\/jobs\/(\d+)/)?.[1]);
const refElement = isNew
? Array.from(card.querySelectorAll('button')).find(b => b.innerHTML.includes('icon-visibility-off') || b.innerText.includes('Не показувати'))
: card.querySelector('button.js-hide-job');
if (!refElement || !refElement.parentElement) return null;
return { card, isNew, url, title, jobId, actionContainer: refElement.parentElement, refElement };
},
injectCheckboxesToList: () => {
const rawCards = Array.from(document.querySelectorAll('.card.job-link, .tw-card[id^="job-"]'));
const validCards = rawCards.map(UI.parseCardForInjection).filter(Boolean);
validCards.forEach(({ card, isNew, url, title, jobId, actionContainer, refElement }) => {
card.classList.add('helper-ready');
const wrapper = document.createElement('label');
wrapper.className = isNew ? CLASSES.tailwindWrapper : CLASSES.legacyWrapper;
Object.assign(wrapper.style, isNew
? { cursor: 'pointer', marginBottom: '0' }
: { display: 'inline-flex', alignItems: 'center', cursor: 'pointer', padding: '7px 10px', borderRadius: '10px', fontWeight: 'normal', marginBottom: '0', fontSize: '18px' }
);
// PREVENT REDIRECT BUG: Blocks all parent click listeners from hijacking the event
Utils.stopEventBubbling(wrapper);
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'helper-cb';
cb.dataset.jobid = jobId;
cb.checked = Store.get().queue.some(q => q.id === jobId);
cb.style.cssText = `appearance: checkbox !important; width: 20px !important; height: 20px !important; margin: 0 12px 0 0 !important; cursor: pointer !important; position: relative !important; z-index: 1000 !important; accent-color: #007bff !important;`;
const text = document.createElement('span');
text.innerText = 'Fast submit';
text.className = isNew ? 'tw-hidden md:tw-inline-block' : 'hidden-xs';
wrapper.append(cb, text);
// Sync with local storage safely
cb.addEventListener('change', (e) => {
const state = Store.get();
const exists = state.queue.some(q => q.id === jobId);
if (e.target.checked && !exists) {
Store.update({ queue: [...state.queue, { id: jobId, url, title, active: true }] });
} else if (!e.target.checked && exists) {
Store.update({ queue: state.queue.filter(q => q.id !== jobId) });
}
});
actionContainer.insertBefore(wrapper, refElement);
});
}
};
/* ========================================================================
6. BOOTSTRAP
======================================================================== */
const init = () => {
console.log("[WorkUa Bot] Initializing...");
UI.init();
// Auto-resume queue if it was running
const state = Store.get();
if (state.isRunning && state.queue.length > 0) {
setTimeout(Bot.processQueue, 1500);
}
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment