initial project commit
This commit is contained in:
9
web/templates/admin/login.html
Normal file
9
web/templates/admin/login.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends 'base.html' %} {% block content %}
|
||||
<h2>Login</h2>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<label>Username <input type="text" name="username" required /></label>
|
||||
<label>Password <input type="password" name="password" /></label>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
142
web/templates/admin/taxonomy.html
Normal file
142
web/templates/admin/taxonomy.html
Normal file
@@ -0,0 +1,142 @@
|
||||
{% extends 'base.html' %} {% block content %}
|
||||
<h2>Taxonomy</h2>
|
||||
<section>
|
||||
<h3>Regions</h3>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="action" value="add_region" />
|
||||
<input type="text" name="region_name" placeholder="New region" required />
|
||||
<label for="region_color">Color:</label>
|
||||
<input type="color" name="region_color" id="region_color" value="#ffffff" />
|
||||
<button type="submit">Add Region</button>
|
||||
</form>
|
||||
<div id="regions-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Rename</th>
|
||||
<th>Color</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in regions %}
|
||||
<tr>
|
||||
<td>{{ r.region_id }}</td>
|
||||
|
||||
<td>{{ r.name }}</td>
|
||||
<td>
|
||||
<form
|
||||
method="post"
|
||||
style="display: flex; gap: 0.5rem; align-items: center"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="action" value="rename_region" />
|
||||
<input type="hidden" name="region_id" value="{{ r.region_id }}" />
|
||||
<input
|
||||
type="text"
|
||||
name="new_region_name"
|
||||
placeholder="New name"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Rename</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form
|
||||
method="post"
|
||||
style="display: flex; gap: 0.5rem; align-items: center"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="action" value="change_region_color" />
|
||||
<input type="hidden" name="region_id" value="{{ r.region_id }}" />
|
||||
<input
|
||||
type="color"
|
||||
name="new_region_color"
|
||||
value="{{ r.color }}"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Change Color</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Keywords</h3>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="action" value="add_keyword" />
|
||||
<input type="text" name="keyword_name" placeholder="New keyword" required />
|
||||
<label for="keyword_color">Color:</label>
|
||||
<input
|
||||
type="color"
|
||||
name="keyword_color"
|
||||
id="keyword_color"
|
||||
value="#ffffff"
|
||||
/>
|
||||
<button type="submit">Add Keyword</button>
|
||||
</form>
|
||||
<div id="keywords-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Rename</th>
|
||||
<th>Color</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for k in keywords %}
|
||||
<tr>
|
||||
<td>{{ k.keyword_id }}</td>
|
||||
<td>{{ k.name }}</td>
|
||||
<td>
|
||||
<form
|
||||
method="post"
|
||||
style="display: flex; gap: 0.5rem; align-items: center"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="action" value="rename_keyword" />
|
||||
<input type="hidden" name="keyword_id" value="{{ k.keyword_id }}" />
|
||||
<input
|
||||
type="text"
|
||||
name="new_keyword_name"
|
||||
placeholder="New name"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Rename</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form
|
||||
method="post"
|
||||
style="display: flex; gap: 0.5rem; align-items: center"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="action" value="change_keyword_color" />
|
||||
<input type="hidden" name="keyword_id" value="{{ k.keyword_id }}" />
|
||||
<input
|
||||
type="color"
|
||||
name="new_keyword_color"
|
||||
value="{{ k.color }}"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Change Color</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %} {% block footer_scripts %}
|
||||
<script src="{{ url_for('static', filename='taxonomy.js') }}"></script>
|
||||
</script>
|
||||
{% endblock %}
|
||||
139
web/templates/admin/users.html
Normal file
139
web/templates/admin/users.html
Normal file
@@ -0,0 +1,139 @@
|
||||
{% extends 'base.html' %} {% block content %}
|
||||
<div id="users">
|
||||
<h2>Users</h2>
|
||||
<form id="user-form" method="post" action="{{ url_for('admin_users') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Username</th>
|
||||
<th>Admin</th>
|
||||
<th>Active</th>
|
||||
<th colspan="2">Password</th>
|
||||
<th>Created</th>
|
||||
<th>Last Login</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr class="user-row" data-user-id="{{ u.user_id }}">
|
||||
<td>
|
||||
{{ u.user_id }}<input
|
||||
type="hidden"
|
||||
name="user_id"
|
||||
value="{{ u.user_id }}"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
value="{{ u.username }}"
|
||||
required
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="is_admin" {{ 'checked' if u.is_admin
|
||||
else '' }} />
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" name="is_active" {{ 'checked' if u.is_active
|
||||
else '' }} />
|
||||
</td>
|
||||
<td>{{ '✅' if u.has_password else '❌' }}</td>
|
||||
<td><input type="password" name="password" /></td>
|
||||
<td>{{ u.created_at }}</td>
|
||||
<td>{{ u.last_login or 'never' }}</td>
|
||||
<td>
|
||||
<button type="submit" data-user-id="{{ u.user_id }}">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<h3>Create / Update User</h3>
|
||||
<form
|
||||
id="create-update-user-form"
|
||||
method="post"
|
||||
action="{{ url_for('admin_users') }}"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<label>Username <input type="text" name="username" required /></label>
|
||||
<label>Password <input type="password" name="password" /></label>
|
||||
<label>Admin <input type="checkbox" name="is_admin" value="1" /></label>
|
||||
<label
|
||||
>Active <input type="checkbox" name="is_active" value="1" checked
|
||||
/></label>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
{% endblock %} {% block footer_scripts %}
|
||||
<script>
|
||||
function updateUser(userId) {
|
||||
const row = document.querySelector(`.user-row[data-user-id="${userId}"]`);
|
||||
const passwordInput = row.querySelector('input[name="password"]');
|
||||
const hasPassword =
|
||||
row.querySelector("td:nth-child(5)").textContent.trim() === "✅";
|
||||
const formData = row.querySelector("form").elements;
|
||||
const username = formData.username.value;
|
||||
const password = hasPassword ? passwordInput.value : undefined;
|
||||
const isAdmin = formData.is_admin.checked;
|
||||
const isActive = formData.is_active.checked;
|
||||
|
||||
fetch("/admin/users", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
password: password,
|
||||
username: username,
|
||||
is_admin: isAdmin,
|
||||
is_active: isActive,
|
||||
csrf_token: formData.csrf_token.value,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
alert("User updated successfully");
|
||||
// Clear the password field after successful update
|
||||
passwordInput.value = "";
|
||||
} else {
|
||||
alert("Error updating user");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
alert("Error updating user");
|
||||
});
|
||||
}
|
||||
|
||||
function initUserForm() {
|
||||
const form = document.getElementById("user-form");
|
||||
const createUpdateForm = document.getElementById("create-update-user-form");
|
||||
|
||||
form.addEventListener("submit", function (event) {
|
||||
const userId = event.target.querySelector('input[name="user_id"]').value;
|
||||
event.preventDefault(); // Prevent the default form submission
|
||||
updateUser(userId);
|
||||
});
|
||||
|
||||
form.addEventListener("click", function (event) {
|
||||
const userId = event.target.closest(".user-row").dataset.userId;
|
||||
updateUser(userId);
|
||||
});
|
||||
|
||||
createUpdateForm.addEventListener("submit", function (event) {
|
||||
const passwordInput = createUpdateForm.querySelector(
|
||||
'input[name="password"]'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
initUserForm();
|
||||
</script>
|
||||
{% endblock %}
|
||||
43
web/templates/base.html
Normal file
43
web/templates/base.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{ title }}</title>
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="{{ url_for('static', filename='styles.css') }}"
|
||||
/>
|
||||
{% block styles %}{% endblock %} {% block scripts %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block header %}
|
||||
<header>
|
||||
<h1><a href="/">{{ title or 'Admin' }}</a></h1>
|
||||
<nav>
|
||||
{% if username %}<span>Hi, {{ username }}</span> | {% endif %}
|
||||
<a href="{{ url_for('index') }}">Home</a> |
|
||||
<a href="{{ url_for('user_settings') }}">Preferences</a> {% if
|
||||
current_user and current_user.is_admin %} |
|
||||
<a href="{{ url_for('admin_taxonomy') }}">Taxonomy</a> |
|
||||
<a href="{{ url_for('admin_users') }}">Users</a> {% endif %} | {% if
|
||||
session.get('username') %}
|
||||
<a href="{{ url_for('logout') }}">Logout</a> {% else %} |
|
||||
<a href="{{ url_for('login') }}">Login</a>{% endif %}
|
||||
</nav>
|
||||
{% with messages = get_flashed_messages() %} {% if messages %}
|
||||
<ul>
|
||||
{% for m in messages %}
|
||||
<li>{{ m }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %} {% endwith %}
|
||||
</header>
|
||||
{% endblock %} {% block content %}{% endblock %}
|
||||
<footer>
|
||||
<p>© 2025 Job Listings</p>
|
||||
</footer>
|
||||
{% block footer_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
55
web/templates/index.html
Normal file
55
web/templates/index.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %} {% block styles %}
|
||||
<style>
|
||||
/* for each keyword, create a different background color */
|
||||
{% for keyword in keywords %}
|
||||
.keyword-{{ keywords[keyword].name }} {
|
||||
background-color: {{ keywords[keyword].color }};
|
||||
}
|
||||
{% endfor %}
|
||||
/* for each region, create a different background color */
|
||||
{% for region in regions %}
|
||||
.region-{{ region }} {
|
||||
background-color: {{ regions[region].color }};
|
||||
}{% endfor %}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block title %}Job Listings{% endblock %}
|
||||
{% block content %}
|
||||
<div id="filters">
|
||||
<form id="filter-form" method="GET" action="/">
|
||||
<label for="region">Region:</label>
|
||||
<select name="region" id="region">
|
||||
<option value="">All</option>
|
||||
{% for region in regions %}
|
||||
<option value="{{ region }}" {% if region == selected_region %}selected{% endif %}>{{ region }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="keyword">Keyword:</label>
|
||||
<select name="keyword" id="keyword">
|
||||
<option value="">All</option>
|
||||
{% for keyword in keywords %}
|
||||
<option value="{{ keyword }}" {% if keyword == selected_keyword %}selected{% endif %}>{{ keyword }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit">Filter</button>
|
||||
<button type="button" id="reset-filters">Reset</button>
|
||||
</form>
|
||||
<form id="scrape-form" method="GET" action="/scrape">
|
||||
<button type="submit">Scrape Jobs</button>
|
||||
<span id="scrape-info"></span>
|
||||
</form>
|
||||
</div>
|
||||
<div id="jobs">
|
||||
{% for job in jobs %}
|
||||
<div class="job">
|
||||
<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'] }}">{{ job['region'] }}</span>
|
||||
<span class="job-keyword keyword-{{ job['keyword']|replace(' ', '')|lower }}">{{ job['keyword'] }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block footer_scripts %}
|
||||
<script src="{{ url_for('static', filename='index.js') }}"></script>
|
||||
{% endblock %}
|
||||
27
web/templates/job.html
Normal file
27
web/templates/job.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %} {% block title %}Job Details{% endblock %} {% block
|
||||
styles %}{% endblock %} {% block content %}
|
||||
<div id="job-details">
|
||||
<p><strong>ID:</strong> {{ job.id }}</p>
|
||||
<p>
|
||||
<strong>Title:</strong> {{ job.title }} | <strong>Company:</strong> {{
|
||||
job.company }} | <strong>Location:</strong> {{ job.location }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Salary:</strong> {{ job.salary }} | <strong>Posted on:</strong> {{
|
||||
job.posted_date }}
|
||||
</p>
|
||||
|
||||
<h2>Job Description</h2>
|
||||
<hr />
|
||||
<p class="job-description">{{ job.description|safe }}</p>
|
||||
<hr />
|
||||
<p>
|
||||
<strong>Original URL:</strong>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ job.url }}" target="_blank" class="job-title"
|
||||
>{{ job.title }}</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
84
web/templates/user/settings.html
Normal file
84
web/templates/user/settings.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% extends 'base.html' %} {% block title %}Your Preferences{% endblock %} {%
|
||||
block content %}
|
||||
<h2>Your Preferences</h2>
|
||||
<form
|
||||
id="user-settings-form"
|
||||
method="post"
|
||||
action="{{ url_for('user_settings') }}"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<fieldset>
|
||||
<legend>Regions</legend>
|
||||
<p>
|
||||
<small>Add new Region:</small>
|
||||
<input
|
||||
type="text"
|
||||
name="new-region"
|
||||
id="new-region"
|
||||
value=""
|
||||
placeholder="Type a region and save to add & select"
|
||||
size="30"
|
||||
/>
|
||||
</p>
|
||||
{% if all_regions %} {% for r in all_regions %}
|
||||
<label style="display: block; background-color: {{ r.color }}">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="region"
|
||||
id="region-{{ r.name }}"
|
||||
value="{{ r.name }}"
|
||||
{%
|
||||
if
|
||||
r
|
||||
in
|
||||
user_regions
|
||||
%}checked{%
|
||||
endif
|
||||
%}
|
||||
/>
|
||||
{{ r.name }}
|
||||
</label>
|
||||
{% endfor %} {% else %}
|
||||
<p>No regions available. Ask an admin to add some.</p>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Keywords</legend>
|
||||
<p>
|
||||
<small>Add new Keyword:</small>
|
||||
<input
|
||||
type="text"
|
||||
name="new-keyword"
|
||||
id="new-keyword"
|
||||
value=""
|
||||
placeholder="Type a keyword and save to add & select"
|
||||
size="30"
|
||||
/>
|
||||
</p>
|
||||
{% if all_keywords %} {% for k in all_keywords %}
|
||||
<label style="display: block; background-color: {{ k.color }}">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="keyword"
|
||||
id="keyword-{{ k.name }}"
|
||||
value="{{ k.name }}"
|
||||
{%
|
||||
if
|
||||
k
|
||||
in
|
||||
user_keywords
|
||||
%}checked{%
|
||||
endif
|
||||
%}
|
||||
/>
|
||||
{{ k.name }}
|
||||
</label>
|
||||
{% endfor %} {% else %}
|
||||
<p>No keywords available. Ask an admin to add some.</p>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
{% endblock %} {% block footer_scripts %}
|
||||
<script src="{{ url_for('static', filename='settings.js') }}"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user