initial project commit

This commit is contained in:
georg.sinn-schirwitz
2025-08-29 15:07:58 +02:00
parent 38708e6d1d
commit 23a67d7fe1
31 changed files with 3433 additions and 0 deletions

102
web/static/index.js Normal file
View 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
View 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
View 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
View 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);
});