1/* global fetch */ 2/* eslint-disable no-console */ 3(function () { 4 var authChecked = false; 5 var isLoggedIn = false; 6 var hasPremium = false; 7 var authPromise = null; 8 var savedCacheLoaded = false; 9 var savedCache = new Set(); 10 var appliedCacheLoaded = false; 11 var appliedCache = new Map(); 12 13 // Bootstrap from server-rendered data when available (job list / SEO pages) 14 var jsVars = document.getElementById("js_variables"); 15 if (jsVars) { 16 authChecked = true; 17 isLoggedIn = jsVars.getAttribute("is_public_user") !== "True"; 18 hasPremium = jsVars.getAttribute("has_premium") === "True"; 19 } 20 21 function jsonRpc(url, params) { 22 return fetch(url, { 23 method: "POST", 24 headers: { "Content-Type": "application/json" }, 25 body: JSON.stringify({ jsonrpc: "2.0", method: "call", params: params || {} }), 26 credentials: "same-origin", 27 }).then(function (response) { 28 return response.json(); 29 }); 30 } 31 32 function getJobPayloadFromButton(btn) { 33 return { 34 job_source: btn.dataset.jobSource, 35 external_job_id: btn.dataset.jobId, 36 job_title: btn.dataset.jobTitle, 37 company_name: btn.dataset.companyName, 38 job_url: btn.dataset.jobUrl, 39 location: btn.dataset.location, 40 category_slug: btn.dataset.categorySlug, 41 logo_url: btn.dataset.logoUrl, 42 publication_date: btn.dataset.publicationDate, 43 }; 44 } 45 46 function setButtonSavedState(btn, saved) { 47 btn.classList.toggle("saved", !!saved); 48 btn.setAttribute("aria-pressed", saved ? "true" : "false"); 49 } 50 51 function showToast(message, type) { 52 var existing = document.querySelector(".save-job-toast"); 53 if (existing) { 54 existing.remove(); 55 } 56 var toast = document.createElement("div"); 57 toast.className = "save-job-toast save-job-toast--" + (type || "success"); 58 toast.textContent = message; 59 document.body.appendChild(toast); 60 setTimeout(function () { 61 if (toast && toast.parentNode) { 62 toast.parentNode.removeChild(toast); 63 } 64 }, 2500); 65 } 66 67 function getCacheKey(btn) { 68 return (btn.dataset.jobSource || "") + ":" + (btn.dataset.jobId || ""); 69 } 70 71 var applyStatusLabels = { 72 applied: "Applied", 73 sent_follow_up: "Sent Follow-Up", 74 interview_scheduled: "Interview Scheduled", 75 interview_completed: "Interview Completed", 76 offer_accepted: "Offer Accepted", 77 offer_declined: "Offer Declined", 78 application_denied: "Application Denied", 79 }; 80 81 function getApplyKey(btn) { 82 return (btn.dataset.jobSource || "") + ":" + (btn.dataset.jobId || ""); 83 } 84 85 function ensureAuth() { 86 if (authChecked) { 87 return Promise.resolve(isLoggedIn); 88 } 89 if (authPromise) { 90 return authPromise; 91 } 92 authPromise = jsonRpc("/api/my/auth-status").then(function (data) { 93 authChecked = true; 94 isLoggedIn = !!(data && data.result && data.result.logged_in); 95 hasPremium = !!(data && data.result && data.result.has_premium); 96 authPromise = null; 97 toggleSaveSearchButton(); 98 return isLoggedIn; 99 }); 100 return authPromise; 101 } 102 103 function loadSavedCache() { 104 if (!hasPremium || savedCacheLoaded) { 105 return Promise.resolve(); 106 } 107 return jsonRpc("/api/my/saved-jobs").then(function (data) { 108 savedCacheLoaded = true; 109 if (data && data.result && Array.isArray(data.result.jobs)) { 110 savedCache = new Set( 111 data.result.jobs.map(function (job) { 112 return (job.job_source || "") + ":" + String(job.external_job_id || ""); 113 }) 114 ); 115 } 116 }); 117 } 118 119 function loadAppliedCache() { 120 if (!hasPremium || appliedCacheLoaded) { 121 return Promise.resolve(); 122 } 123 return jsonRpc("/api/my/applied-jobs").then(function (data) { 124 appliedCacheLoaded = true; 125 appliedCache = new Map(); 126 if (data && data.result && Array.isArray(data.result.jobs)) { 127 data.result.jobs.forEach(function (job) { 128 var key = (job.job_source || "") + ":" + String(job.external_job_id || ""); 129 appliedCache.set(key, job.status || "applied"); 130 }); 131 } 132 }); 133 } 134 135 function updateButtonState(btn) { 136 var key = getCacheKey(btn); 137 if (savedCacheLoaded && key) { 138 setButtonSavedState(btn, savedCache.has(key)); 139 return Promise.resolve(); 140 } 141 return jsonRpc("/api/my/job/is-saved", { 142 job_source: btn.dataset.jobSource, 143 external_job_id: btn.dataset.jobId, 144 }).then(function (data) { 145 if (data && data.result) { 146 setButtonSavedState(btn, data.result.saved); 147 } 148 }); 149 } 150 151 function setApplyDropdownSelection(wrap, status) { 152 if (!wrap) return; 153 var items = wrap.querySelectorAll(".job-apply-dropdown-item");
154 items.forEach(function (item) { 155 var isSelected = (item.dataset.status || "") === (status || ""); 156 item.classList.toggle("selected", isSelected); 157 }); 158 } 159 160 function setApplyButtonState(btn, status) { 161 var hasStatus = !!status; 162 btn.classList.toggle("applied", hasStatus); 163 btn.setAttribute("aria-pressed", hasStatus ? "true" : "false"); 164 btn.dataset.status = status || ""; 165 166 var label = btn.querySelector(".job-apply-label"); 167 if (label) { 168 label.textContent = hasStatus ? applyStatusLabels[status] : "Track application"; 169 } 170 171 var wrap = btn.closest(".job-apply-wrap"); 172 setApplyDropdownSelection(wrap, status || ""); 173 } 174 175 function updateApplyControl(btn) { 176 var key = getApplyKey(btn); 177 if (appliedCacheLoaded && key) { 178 var status = appliedCache.get(key) || ""; 179 setApplyButtonState(btn, status); 180 return Promise.resolve(); 181 } 182 return jsonRpc("/api/my/job/is-applied", { 183 job_source: btn.dataset.jobSource, 184 external_job_id: btn.dataset.jobId, 185 }).then(function (data) { 186 if (data && data.result) { 187 setApplyButtonState(btn, data.result.status || ""); 188 } 189 }); 190 } 191 192 function refreshApplyControls() { 193 var buttons = document.querySelectorAll(".job-apply-btn"); 194 if (!buttons.length) return; 195 196 ensureAuth().then(function () { 197 buttons.forEach(function (btn) { 198 var wrap = btn.closest(".job-apply-wrap"); 199 if (!hasPremium) { 200 btn.style.display = "none"; 201 if (wrap) { 202 wrap.style.display = "none"; 203 } 204 return; 205 } 206 btn.style.display = "inline-flex"; 207 // if (wrap) { 208 // wrap.style.display = "inline-flex"; 209 // } 210 }); 211 212 loadAppliedCache().then(function () { 213 buttons.forEach(function (btn) { 214 if (hasPremium) { 215 updateApplyControl(btn); 216 } 217 }); 218 }); 219 }); 220 } 221 222 function refreshSaveButtons() { 223 var buttons = document.querySelectorAll(".job-save-btn"); 224 if (!buttons.length) return; 225 226 ensureAuth().then(function () { 227 buttons.forEach(function (btn) { 228 if (!hasPremium) { 229 btn.style.display = "none"; 230 return; 231 } 232 btn.style.display = "inline-flex"; 233 }); 234 235 loadSavedCache().then(function () { 236 buttons.forEach(function (btn) { 237 if (hasPremium) { 238 updateButtonState(btn); 239 } 240 }); 241 }); 242 }); 243 } 244 245 function refreshAllControls() { 246 refreshSaveButtons(); 247 refreshApplyControls(); 248 } 249 250 function handleSaveClick(event) { 251 var btn = event.currentTarget; 252 if (!btn || btn.classList.contains("job-save-btn") === false) return; 253 254 var isSaved = btn.classList.contains("saved"); 255 var endpoint = isSaved ? "/api/my/job/unsave" : "/api/my/job/save"; 256 var key = getCacheKey(btn); 257 var params = isSaved 258 ? { 259 job_source: btn.dataset.jobSource, 260 external_job_id: btn.dataset.jobId, 261 } 262 : { job_data: getJobPayloadFromButton(btn) }; 263 264 jsonRpc(endpoint, params).then(function (data) { 265 if (data && data.result && data.result.success) { 266 setButtonSavedState(btn, data.result.saved); 267 if (key) { 268 if (data.result.saved) { 269 savedCache.add(key); 270 } else { 271 savedCache.delete(key); 272 } 273 } 274 showToast( 275 data.result.saved ? "Saved to your list." : "Removed from your saved jobs.", 276 "success" 277 ); 278 return; 279 } 280 if (data && data.result && data.result.error === "limit_reached") { 281 showToast("Saved job limit reached. Remove a job to save another.", "error"); 282 return; 283 } 284 showToast("Something went wrong. Please try again.", "error"); 285 }); 286 } 287 288 function handleApplyChange(btn, status) { 289 if (!btn) return; 290 var key = getApplyKey(btn); 291 var previous = btn.dataset.status || ""; 292 var params = { job_data: getJobPayloadFromButton(btn) }; 293 294 if (!status) { 295 jsonRpc("/api/my/job/unapply", { 296 job_source: btn.dataset.jobSource, 297 external_job_id: btn.dataset.jobId, 298 }).then(function (data) { 299 if (data && data.result && data.result.success) { 300 appliedCache.delete(key); 301 setApplyButtonState(btn, ""); 302 showToast("Application status cleared.", "success"); 303 return; 304 } 305 setApplyButtonState(btn, previous); 306 showToast("Something went wrong. Please try again.", "error"); 307 }); 308 return; 309 } 310 311 params.status = status; 312 jsonRpc("/api/my/job/apply", params).then(function (data) { 313 if (data && data.result && data.result.success) { 314 appliedCache.set(key, status); 315 setApplyButtonState(btn, status); 316 if (key) { 317 savedCache.add(key); 318 } 319 refreshSaveButtons(); 320 showToast("Application status updated.", "success"); 321 return; 322 } 323 if (data && data.result && data.result.error === "saved_limit_reached") { 324 setApplyButtonState(btn, previous); 325 showToast("Saved job limit reached. Remove a saved job to apply.", "error"); 326 return; 327 } 328 if (data && data.result && data.result.error === "applied_limit_reached") { 329 setApplyButtonState(btn, previous); 330 showToast("Applied job limit reached. Remove an applied job to add another.", "error"); 331 return; 332 } 333 setApplyButtonState(btn, previous); 334 showToast("Something went wrong. Please try again.", "error"); 335 }); 336 } 337 338 function bindSaveButtons() { 339 document.addEventListener("click", function (event) { 340 var target = event.target; 341 if (!target) return;
342 var btn = target.closest(".job-save-btn"); 343 if (!btn) return; 344 event.preventDefault(); 345 event.stopPropagation(); 346 if (event.stopImmediatePropagation) { 347 event.stopImmediatePropagation(); 348 } 349 handleSaveClick({ currentTarget: btn }); 350 }, true); 351 } 352 353 function closeApplyDropdowns() { 354 document.querySelectorAll(".job-apply-dropdown").forEach(function (dropdown) { 355 dropdown.classList.add("tw-hidden"); 356 var wrap = dropdown.closest(".job-apply-wrap"); 357 if (wrap) { 358 var btn = wrap.querySelector(".job-apply-btn"); 359 if (btn) { 360 btn.setAttribute("aria-expanded", "false"); 361 } 362 } 363 }); 364 } 365 366 function bindApplyControls() { 367 document.addEventListener("click", function (event) { 368 var target = event.target; 369 if (!target) return; 370 371 var applyBtn = target.closest(".job-apply-btn"); 372 if (applyBtn) { 373 event.preventDefault(); 374 event.stopPropagation(); 375 if (event.stopImmediatePropagation) { 376 event.stopImmediatePropagation(); 377 } 378 var wrap = applyBtn.closest(".job-apply-wrap"); 379 var dropdown = wrap ? wrap.querySelector(".job-apply-dropdown") : null; 380 if (!dropdown) return; 381 var isOpen = !dropdown.classList.contains("tw-hidden"); 382 closeApplyDropdowns(); 383 if (!isOpen) { 384 dropdown.classList.remove("tw-hidden"); 385 applyBtn.setAttribute("aria-expanded", "true"); 386 } 387 return; 388 } 389 390 var item = target.closest(".job-apply-dropdown-item"); 391 if (item) { 392 event.preventDefault(); 393 event.stopPropagation(); 394 if (event.stopImmediatePropagation) { 395 event.stopImmediatePropagation(); 396 } 397 var wrapEl = item.closest(".job-apply-wrap"); 398 var btn = wrapEl ? wrapEl.querySelector(".job-apply-btn") : null; 399 if (!btn) return; 400 var status = item.dataset.status || ""; 401 closeApplyDropdowns(); 402 handleApplyChange(btn, status); 403 return; 404 } 405 406 closeApplyDropdowns(); 407 }, true); 408 } 409 410 function toggleSaveSearchButton() { 411 var btn = document.getElementById("save-search-btn"); 412 if (!btn) return; 413 if (authChecked && hasPremium) { 414 btn.style.display = "block"; 415 } else { 416 btn.style.display = "none"; 417 } 418 } 419 420 window.openSaveSearchModal = function () { 421 ensureAuth().then(function () { 422 if (!hasPremium) { 423 if (!isLoggedIn) { 424 showToast("Please log in to save searches.", "error"); 425 } else { 426 showToast("Premium access required to save searches.", "error"); 427 } 428 return; 429 } 430 var modal = document.getElementById("save-search-modal"); 431 if (modal) { 432 modal.classList.remove("tw-hidden"); 433 var input = document.getElementById("save-search-name"); 434 if (input) { 435 input.value = ""; 436 input.focus(); 437 } 438 } 439 }); 440 }; 441 442 window.closeSaveSearchModal = function () { 443 var modal = document.getElementById("save-search-modal"); 444 if (modal) { 445 modal.classList.add("tw-hidden"); 446 } 447 }; 448 449 window.handleSaveSearch = function () { 450 var input = document.getElementById("save-search-name"); 451 var name = input ? input.value.trim() : ""; 452 if (!name) { 453 showToast("Please name your search.", "error"); 454 return; 455 } 456 var url = window.location.href; 457 jsonRpc("/api/my/search/save", { name: name, search_url: url }).then(function (data) { 458 if (data && data.result && data.result.success) { 459 showToast("Search saved.", "success"); 460 window.closeSaveSearchModal(); 461 return; 462 } 463 if (data && data.result && data.result.error === "limit_reached") { 464 showToast("Saved search limit reached. Remove a search to save another.", "error"); 465 return; 466 } 467 showToast("Something went wrong. Please try again.", "error"); 468 }); 469 }; 470 471 window.deleteSavedSearch = function (searchId) { 472 if (!searchId) return false; 473 jsonRpc("/api/my/search/unsave", { search_id: searchId }).then(function (data) { 474 if (data && data.result && data.result.success) { 475 window.location.reload(); 476 return; 477 } 478 showToast("Something went wrong. Please try again.", "error"); 479 }); 480 return false;
481 }; 482 483 document.addEventListener("DOMContentLoaded", function () { 484 refreshAllControls(); 485 bindSaveButtons(); 486 bindApplyControls(); 487 toggleSaveSearchButton(); 488 }); 489 490 document.addEventListener("savedJobs:refresh", function () { 491 refreshAllControls(); 492 }); 493 494 window.remotiveSavedJobs = { 495 refresh: refreshAllControls, 496 }; 497})(); 498
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.