initial project commit
This commit is contained in:
102
web/static/index.js
Normal file
102
web/static/index.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// Update the table with job data
|
||||
function updateTableData(jobs) {
|
||||
const jobsContainer = document.getElementById("jobs");
|
||||
jobsContainer.innerHTML = ""; // Clear existing jobs
|
||||
jobs.forEach((job) => {
|
||||
const jobElement = document.createElement("div");
|
||||
jobElement.classList.add("job");
|
||||
jobElement.innerHTML = `
|
||||
<h3><a href="${job.url}" target="_blank">${job.title}</a></h3>
|
||||
<p class="job-posted-time">${job.posted_time}</p>
|
||||
<span class="job-region region-${job.region
|
||||
.replace(" ", "")
|
||||
.toLowerCase()}">${job.region}</span>
|
||||
<span class="job-keyword keyword-${job.keyword
|
||||
.replace(" ", "")
|
||||
.toLowerCase()}">${job.keyword}</span>
|
||||
`;
|
||||
jobsContainer.appendChild(jobElement);
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch job data from the server
|
||||
function fetchJobs() {
|
||||
fetch("/jobs")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
updateTableData(data);
|
||||
})
|
||||
.catch((error) => console.error("Error fetching jobs:", error));
|
||||
}
|
||||
|
||||
// scrape form submission
|
||||
function updateScrapeInfo(message, color) {
|
||||
let scrapingInfo = document.getElementById("scrape-info");
|
||||
scrapingInfo.style.display = "inline-block"; // Show the scraping info
|
||||
scrapingInfo.innerText = message;
|
||||
scrapingInfo.style.color = color;
|
||||
}
|
||||
|
||||
function scrape(event) {
|
||||
event.preventDefault(); // Prevent the default form submission
|
||||
updateScrapeInfo("Scraping in progress...", "blue");
|
||||
fetch("/scrape")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.status) {
|
||||
updateScrapeInfo(data.status, "green");
|
||||
} else {
|
||||
updateScrapeInfo("Scraping failed. Please try again.", "red");
|
||||
}
|
||||
})
|
||||
.catch((error) => console.error("Error:", error));
|
||||
}
|
||||
|
||||
function updateJobsFiltered() {
|
||||
const selectedRegion = document.getElementById("region").value;
|
||||
const selectedKeyword = document.getElementById("keyword").value;
|
||||
const filterForm = document.getElementById("filter-form");
|
||||
const queryString = new URLSearchParams({
|
||||
region: selectedRegion,
|
||||
keyword: selectedKeyword,
|
||||
}).toString();
|
||||
filterForm.action = `/?${queryString}`;
|
||||
filterForm.submit(); // Submit the form to apply filters
|
||||
}
|
||||
|
||||
function regionClick(event) {
|
||||
const region = event.target.innerText;
|
||||
const regionInput = document.getElementById("region");
|
||||
regionInput.value = region;
|
||||
updateJobsFiltered();
|
||||
}
|
||||
|
||||
function keywordClick(event) {
|
||||
const keyword = event.target.innerText;
|
||||
const keywordInput = document.getElementById("keyword");
|
||||
keywordInput.value = keyword;
|
||||
updateJobsFiltered();
|
||||
}
|
||||
|
||||
document.querySelectorAll(".job-keyword").forEach((element) => {
|
||||
element.addEventListener("click", keywordClick);
|
||||
});
|
||||
document.querySelectorAll(".job-region").forEach((element) => {
|
||||
element.addEventListener("click", regionClick);
|
||||
});
|
||||
|
||||
document.getElementById("scrape-form").addEventListener("submit", scrape);
|
||||
document
|
||||
.getElementById("region")
|
||||
.addEventListener("change", updateJobsFiltered);
|
||||
document
|
||||
.getElementById("keyword")
|
||||
.addEventListener("change", updateJobsFiltered);
|
||||
document
|
||||
.getElementById("filter-form")
|
||||
.addEventListener("submit", updateJobsFiltered);
|
||||
document.getElementById("reset-filters").addEventListener("click", () => {
|
||||
document.getElementById("region").value = "";
|
||||
document.getElementById("keyword").value = "";
|
||||
updateJobsFiltered();
|
||||
});
|
||||
61
web/static/settings.js
Normal file
61
web/static/settings.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/* javascript form handling */
|
||||
document
|
||||
.getElementById("user-settings-form")
|
||||
.addEventListener("submit", function (event) {
|
||||
event.preventDefault(); // Prevent default form submission
|
||||
|
||||
const form = event.target;
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Collect selected regions and keywords
|
||||
const selectedRegions = [];
|
||||
const selectedKeywords = [];
|
||||
formData.forEach((value, key) => {
|
||||
if (key === "region") {
|
||||
selectedRegions.push(value);
|
||||
} else if (key === "keyword") {
|
||||
selectedKeywords.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
// Add new region if provided
|
||||
const newRegion = formData.get("new-region").trim();
|
||||
if (newRegion) {
|
||||
selectedRegions.push(newRegion);
|
||||
}
|
||||
|
||||
// Add new keyword if provided
|
||||
const newKeyword = formData.get("new-keyword").trim();
|
||||
if (newKeyword) {
|
||||
selectedKeywords.push(newKeyword);
|
||||
}
|
||||
|
||||
// Prepare data to send
|
||||
const dataToSend = {
|
||||
regions: selectedRegions,
|
||||
keywords: selectedKeywords,
|
||||
csrf_token: formData.get("csrf_token"),
|
||||
};
|
||||
|
||||
// Send data via Fetch API
|
||||
fetch(form.action, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]')
|
||||
.content,
|
||||
},
|
||||
body: JSON.stringify(dataToSend),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
window.location.reload(); // Reload to reflect changes
|
||||
} else {
|
||||
alert("Error saving preferences.");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
alert("Error saving preferences.");
|
||||
});
|
||||
});
|
||||
144
web/static/styles.css
Normal file
144
web/static/styles.css
Normal file
@@ -0,0 +1,144 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
footer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
nav {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#filters {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
#filters #filter-form {
|
||||
display: inline-block;
|
||||
max-width: 500px;
|
||||
}
|
||||
#filters #scrape-form {
|
||||
display: inline-block;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
#filters #scrape-form span#scrape-info {
|
||||
display: none;
|
||||
color: blue;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
#jobs {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.job {
|
||||
border: 1px solid #ccc;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.job a {
|
||||
display: inline-block;
|
||||
}
|
||||
.job h3 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.job-posted-time {
|
||||
font-weight: normal;
|
||||
font-size: 0.8em;
|
||||
color: #666;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.job-region,
|
||||
.job-keyword {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.8rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
display: inline;
|
||||
margin-right: 0.5rem;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
#job-details {
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.job-description {
|
||||
margin-top: 5px;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.25;
|
||||
font-size: 14px;
|
||||
}
|
||||
.job-description br {
|
||||
margin: -5px 0;
|
||||
}
|
||||
|
||||
.job-title {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Taxonomy Management */
|
||||
#regions-table,
|
||||
#keywords-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
#regions-table table,
|
||||
#keywords-table table {
|
||||
max-width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
#regions-table th,
|
||||
#regions-table td,
|
||||
#keywords-table th,
|
||||
#keywords-table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
#regions-table th,
|
||||
#keywords-table th {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
/* Admin User Management */
|
||||
#users {
|
||||
margin-top: 20px;
|
||||
}
|
||||
#users table {
|
||||
max-width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
#users th,
|
||||
#users td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
#users th {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
41
web/static/taxonomy.js
Normal file
41
web/static/taxonomy.js
Normal file
@@ -0,0 +1,41 @@
|
||||
function updateColor(id, type, newColor) {
|
||||
fetch("/admin/taxonomy", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action:
|
||||
type === "region" ? "change_region_color" : "change_keyword_color",
|
||||
[type + "_id"]: id,
|
||||
[type + "_color"]: newColor,
|
||||
}),
|
||||
}).then((response) => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert("Failed to update " + type + " color");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document
|
||||
.getElementById("region-color-form")
|
||||
.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
const regionId = this.querySelector('input[name="region_id"]').value;
|
||||
const newColor = this.querySelector('input[name="new_region_color"]').value;
|
||||
updateColor(regionId, "region", newColor);
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("keyword-color-form")
|
||||
.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
const keywordId = this.querySelector('input[name="keyword_id"]').value;
|
||||
const newColor = this.querySelector(
|
||||
'input[name="new_keyword_color"]'
|
||||
).value;
|
||||
updateColor(keywordId, "keyword", newColor);
|
||||
});
|
||||
Reference in New Issue
Block a user