PageSourceSearch

https://remotive.com/remotive_b2c_portal/static/src/js/save_job.js?v=15

js remotive.com collected 2026-09-25 04:08:21 UTC 15,122 bytes, 498 lines download raw bytes

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.