608 lines
23 KiB
HTML
608 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>News</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
|
|
<script>
|
|
|
|
function getQueryString(pageNumber, itemsNumber, sources, searches, statuses){
|
|
// Query parameters. If input is null, get most recent value
|
|
let queryParams = new URLSearchParams(window.location.search);
|
|
// page
|
|
if (pageNumber == null) pageNumber = queryParams.get("page") ?? 1;
|
|
queryParams.set("page", pageNumber);
|
|
// items
|
|
if (itemsNumber == null) itemsNumber = queryParams.get("items") ?? 15;
|
|
queryParams.set("items", itemsNumber);
|
|
// sources
|
|
if (sources == null) sources = queryParams.get("sources") ?? "all";
|
|
queryParams.set("sources", sources);
|
|
// searches
|
|
if (searches == null) searches = queryParams.get("searches") ?? "all";
|
|
queryParams.set("searches", searches);
|
|
// status
|
|
if (statuses == null) statuses = queryParams.get("status") ?? "all";
|
|
queryParams.set("status", statuses);
|
|
|
|
// Encoding fix: %2C -> ,
|
|
let queryParamsString = queryParams.toString();
|
|
while (queryParamsString.includes("%2C")) {
|
|
queryParamsString = queryParamsString.replace("%2C", ",");
|
|
}
|
|
return queryParamsString;
|
|
}
|
|
|
|
function loadPage(pageNumber, itemsNumber, sources, searches, statuses) {
|
|
$("#item-list").fadeTo(100, 0.5); // Smooth fade effect
|
|
$("#loading").show();
|
|
|
|
queryParamsString = getQueryString(pageNumber, itemsNumber, sources, searches, statuses);
|
|
|
|
$.ajax({
|
|
url: "?" + queryParamsString,
|
|
type: "GET",
|
|
headers: { "X-Requested-With": "XMLHttpRequest" },
|
|
success: function (data) {
|
|
$("#item-list").fadeTo(0, 1).html(data.items_html); // Restore opacity smoothly
|
|
$("#loading").hide();
|
|
// Update URL without reloading
|
|
window.history.pushState({}, "", "?" + queryParamsString);
|
|
}
|
|
});
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
// Pagination
|
|
////////////////////////////////////////////////////////////////////////////
|
|
$(document).on("click", ".pagination a", function (event) {
|
|
event.preventDefault();
|
|
let page = $(this).attr("data-page");
|
|
loadPage(pageNumber=page, itemsNumber=null, sources=null, searches=null, statuses=null);
|
|
});
|
|
|
|
$(document).ready(function () {
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
// Filter updates
|
|
////////////////////////////////////////////////////////////////////////////
|
|
const sourcesToggleAll = $("#toggle-all-sources");
|
|
const sourcesCheckboxes = $(".source-checkbox");
|
|
const searchesToggleAll = $("#toggle-all-searches");
|
|
const searchesCheckboxes = $(".search-checkbox");
|
|
const statusesToggleAll = $("#toggle-all-status");
|
|
const statusCheckboxes = $(".status-checkbox");
|
|
|
|
function updateFilters() {
|
|
// Get selected sources
|
|
if (sourcesToggleAll.prop("checked")) {
|
|
selectedSources = "all";
|
|
}
|
|
else {
|
|
if (sourcesCheckboxes.filter(":checked").length > 0 ){
|
|
selectedSources = sourcesCheckboxes.filter(":checked").map(function () {
|
|
return $(this).val();
|
|
}).get().join(",");
|
|
}
|
|
else {
|
|
selectedSources = "none";
|
|
}
|
|
|
|
}
|
|
|
|
// Get selected searches
|
|
if (searchesToggleAll.prop("checked")) {
|
|
selectedSearches = "all";
|
|
}
|
|
else {
|
|
if (searchesCheckboxes.filter(":checked").length > 0 ){
|
|
selectedSearches = searchesCheckboxes.filter(":checked").map(function () {
|
|
return $(this).val();
|
|
}).get().join(",");
|
|
}
|
|
else {
|
|
selectedSearches = "none";
|
|
}
|
|
}
|
|
|
|
// Get selected URL statuses
|
|
if (statusesToggleAll.prop("checked")) {
|
|
selectedStatuses = "all";
|
|
}
|
|
else {
|
|
if (statusCheckboxes.filter(":checked").length > 0 ){
|
|
selectedStatuses = statusCheckboxes.filter(":checked").map(function () {
|
|
return $(this).val();
|
|
}).get().join(",");
|
|
}
|
|
else {
|
|
selectedStatuses = "none";
|
|
}
|
|
}
|
|
|
|
// Get selected items per page
|
|
let selectedItems = $("input[name='items']:checked").val();
|
|
|
|
// Update pagination and reload data
|
|
loadPage(1, selectedItems, selectedSources, selectedSearches, selectedStatuses);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
// Change triggers
|
|
////////////////////////////////////////////////////////////////////////////
|
|
// Sources
|
|
sourcesToggleAll.on("change", function () {
|
|
sourcesCheckboxes.prop("checked", sourcesToggleAll.prop("checked"));
|
|
updateFilters();
|
|
});
|
|
sourcesCheckboxes.on("change", function () {
|
|
sourcesToggleAll.prop("checked", sourcesCheckboxes.length === sourcesCheckboxes.filter(":checked").length);
|
|
updateFilters();
|
|
});
|
|
// Searches
|
|
searchesToggleAll.on("change", function () {
|
|
searchesCheckboxes.prop("checked", searchesToggleAll.prop("checked"));
|
|
updateFilters();
|
|
});
|
|
searchesCheckboxes.on("change", function () {
|
|
searchesToggleAll.prop("checked", searchesCheckboxes.length === searchesCheckboxes.filter(":checked").length);
|
|
updateFilters();
|
|
});
|
|
// Status
|
|
statusesToggleAll.on("change", function () {
|
|
statusCheckboxes.prop("checked", statusesToggleAll.prop("checked"));
|
|
updateFilters();
|
|
});
|
|
statusCheckboxes.on("change", function () {
|
|
// If all checkboxes are checked, mark "Toggle All" as checked
|
|
statusesToggleAll.prop("checked", statusCheckboxes.length === statusCheckboxes.filter(":checked").length);
|
|
updateFilters();
|
|
});
|
|
|
|
// Items change trigger update
|
|
$(".items").on("change", updateFilters);
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
// Default values
|
|
////////////////////////////////////////////////////////////////////////////
|
|
// Sources
|
|
sourcesCheckboxes.each(function () { $(this).prop("checked", true); });
|
|
sourcesToggleAll.prop("checked", true);
|
|
// Searches
|
|
searchesCheckboxes.each(function () { $(this).prop("checked", true); });
|
|
searchesToggleAll.prop("checked", true);
|
|
// Statuses
|
|
statusCheckboxes.each(function () { $(this).prop("checked", true); });
|
|
statusesToggleAll.prop("checked", true);
|
|
// Items
|
|
// $("input[name='items'][value='" + 15 + "']").prop("checked", true);
|
|
// loadPage(pageNumber=page, itemsNumber=null, sources=null, searches=null, statuses=null);
|
|
});
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
// Theme logic
|
|
////////////////////////////////////////////////////////////////////////////
|
|
function setTheme(mode) {
|
|
document.documentElement.setAttribute("data-theme", mode);
|
|
document.documentElement.setAttribute("data-bs-theme", mode);
|
|
localStorage.setItem("theme", mode);
|
|
document.getElementById("theme-icon").innerHTML = mode === "dark" ? "🌞" : "🌙";
|
|
document.body.classList.toggle("dark-mode", mode === "dark");
|
|
}
|
|
|
|
function toggleTheme() {
|
|
let currentTheme = document.documentElement.getAttribute("data-theme");
|
|
setTheme(currentTheme === "dark" ? "light" : "dark");
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
let savedTheme = localStorage.getItem("theme") ||
|
|
(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
setTheme(savedTheme);
|
|
// Local browser timestamp aware for ts_fetch print
|
|
document.querySelectorAll(".timestamp").forEach(function (el) {
|
|
const ts = el.getAttribute("data-ts");
|
|
if (ts) {
|
|
const options = {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
hour12: false // Use 24-hour format
|
|
}; // "en-GB" for DD-MM-YYYY
|
|
const localDate = new Date(ts).toLocaleString("en-GB", options); // Adjust to browser's timezone
|
|
el.innerHTML = `${localDate}`;
|
|
}
|
|
});
|
|
});
|
|
////////////////////////////////////////////////////////////////////////////
|
|
</script>
|
|
|
|
<style>
|
|
/* Content Area */
|
|
#content {
|
|
margin-left: 170px; /* Match sidebar width */
|
|
min-width: calc(100vw - 170px); /* Ensure it doesn't shrink into the sidebar */
|
|
width: calc(100vw - 170px); /* Expands based on screen size */
|
|
padding: 20px;
|
|
overflow-x: auto; /* Prevent content from being squeezed */
|
|
transition: margin-left 0.3s ease;
|
|
}
|
|
|
|
/* Sidebar Styles */
|
|
#sidebar {
|
|
height: 100vh;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 170px; /* Default width */
|
|
background-color: var(--bg-color);
|
|
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
|
padding: 15px;
|
|
transition: width 0.3s ease;
|
|
/* Enable scrolling */
|
|
overflow-y: auto;
|
|
max-height: 100vh;
|
|
}
|
|
|
|
#sidebar .nav-link {
|
|
color: var(--text-color);
|
|
}
|
|
|
|
#sidebar .nav-link:hover {
|
|
background-color: var(--pagination-hover-bg);
|
|
}
|
|
|
|
/* ============================= */
|
|
/* Responsive Enhancements */
|
|
/* ============================= */
|
|
@media (min-width: 1200px) {
|
|
.table {
|
|
width: 95%; /* Allows table to take more space */
|
|
margin: 0 auto; /* Centers the table */
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
#sidebar {
|
|
width: 70px; /* Collapse sidebar to smaller width */
|
|
/*padding: 10px;*/
|
|
}
|
|
|
|
#content {
|
|
margin-left: 70px; /* Adjust margin to match collapsed sidebar */
|
|
min-width: calc(100vw - 70px); /* Prevent overlap */
|
|
/*padding: 10px;*/
|
|
}
|
|
|
|
/* Adjust table for small screens */
|
|
.table-responsive {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.table th,
|
|
.table td {
|
|
white-space: nowrap; /* Prevent text wrapping in cells */
|
|
}
|
|
|
|
.table a {
|
|
word-break: break-word; /* Ensure long URLs break properly */
|
|
}
|
|
}
|
|
|
|
/* ============================= */
|
|
/* Global Styles */
|
|
/* ============================= */
|
|
body {
|
|
background-color: var(--bg-color);
|
|
color: var(--text-color);
|
|
transition: background-color 0.3s, color 0.3s;
|
|
}
|
|
|
|
/* ============================= */
|
|
/* Light & Dark Mode Variables */
|
|
/* ============================= */
|
|
:root {
|
|
--bg-color: #ffffff;
|
|
--text-color: #212529;
|
|
--table-bg: #ffffff;
|
|
--table-text: #000000;
|
|
--table-border: #dee2e6;
|
|
--link-color: #007bff;
|
|
--pagination-bg: #ffffff;
|
|
--pagination-border: #dee2e6;
|
|
--pagination-hover-bg: #f8f9fa;
|
|
--pagination-active-bg: #007bff;
|
|
--pagination-active-text: #ffffff;
|
|
--button-bg: #f8f9fa;
|
|
--button-border: #ced4da;
|
|
--button-text: #212529;
|
|
}
|
|
|
|
[data-theme="dark"] {
|
|
--bg-color: #121212;
|
|
--text-color: #e0e0e0;
|
|
--table-bg: #1e1e1e;
|
|
--table-text: #ffffff;
|
|
--table-border: #2c2c2c;
|
|
--link-color: #9ec5fe;
|
|
--pagination-bg: #1e1e1e;
|
|
--pagination-border: #444;
|
|
--pagination-hover-bg: #333;
|
|
--pagination-active-bg: #007bff;
|
|
--pagination-active-text: #ffffff;
|
|
--button-bg: #1e1e1e;
|
|
--button-border: #444;
|
|
--button-text: #e0e0e0;
|
|
}
|
|
|
|
/* ============================= */
|
|
/* Table Styling */
|
|
/* ============================= */
|
|
.table-responsive {
|
|
width: 100%; /* Ensure it spans the full width of its container */
|
|
max-width: 100%;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.table {
|
|
background-color: var(--table-bg);
|
|
color: var(--table-text);
|
|
border: 1px solid var(--table-border);
|
|
transition: background-color 0.3s, color 0.3s;
|
|
|
|
width: 100%; /* Ensures it takes full width of its container */
|
|
table-layout: auto; /* Allows columns to adjust dynamically */
|
|
/*white-space: nowrap;*/ /* Prevents text wrapping in cells */
|
|
}
|
|
|
|
.table th,
|
|
.table td {
|
|
border-color: var(--table-border);
|
|
}
|
|
|
|
.table thead {
|
|
background-color: var(--pagination-active-bg);
|
|
color: var(--pagination-active-text);
|
|
}
|
|
|
|
[data-theme="dark"] .table {
|
|
background-color: var(--table-bg);
|
|
color: var(--table-text);
|
|
}
|
|
|
|
[data-theme="dark"] .table th,
|
|
[data-theme="dark"] .table td {
|
|
border-color: var(--table-border);
|
|
}
|
|
|
|
[data-theme="dark"] .table thead {
|
|
background-color: #333;
|
|
color: #fff;
|
|
}
|
|
|
|
th:nth-child(1), td:nth-child(1) { width: 50%; } /* URL column */
|
|
th:nth-child(2), td:nth-child(2) { width: 27.5%; } /* Fetch Date */
|
|
th:nth-child(3), td:nth-child(3) { width: 10%; } /* Sources */
|
|
th:nth-child(4), td:nth-child(4) { width: 10%; } /* Searches */
|
|
th:nth-child(5), td:nth-child(5) { width: 2.5%; } /* Status */
|
|
|
|
/* ============================= */
|
|
/* Pagination Styling */
|
|
/* ============================= */
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 10px 0;
|
|
}
|
|
|
|
.pagination .page-link {
|
|
background-color: var(--pagination-bg);
|
|
border-color: var(--pagination-border);
|
|
color: var(--text-color);
|
|
padding: 10px 14px;
|
|
margin: 0 5px;
|
|
border-radius: 8px;
|
|
transition: background-color 0.3s, color 0.3s, transform 0.2s;
|
|
}
|
|
|
|
.pagination .page-link:hover {
|
|
background-color: var(--pagination-hover-bg);
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.pagination .active .page-link {
|
|
background-color: var(--pagination-active-bg);
|
|
color: var(--pagination-active-text);
|
|
border-color: var(--pagination-active-bg);
|
|
}
|
|
|
|
/* ============================= */
|
|
/* Theme Toggle Button */
|
|
/* ============================= */
|
|
.theme-toggle-btn {
|
|
background-color: var(--button-bg);
|
|
border: 1px solid var(--button-border);
|
|
color: var(--button-text);
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
font-size: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: background-color 0.3s, color 0.3s, transform 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.theme-toggle-btn:hover {
|
|
background-color: var(--pagination-hover-bg);
|
|
transform: rotate(20deg);
|
|
}
|
|
|
|
.theme-toggle-btn:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
/* ============================= */
|
|
/* Loading Spinner Styling */
|
|
/* ============================= */
|
|
#loading {
|
|
position: fixed;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 1050;
|
|
display: none;
|
|
}
|
|
|
|
.spinner-border {
|
|
width: 4rem;
|
|
height: 4rem;
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Left Sidebar -->
|
|
<div id="sidebar" class="d-flex flex-column">
|
|
<ul class="nav flex-column">
|
|
|
|
<!-- Theme Toggle Button -->
|
|
<div class="nav-item">
|
|
<button onclick="toggleTheme()" class="theme-toggle-btn">
|
|
<span id="theme-icon">🌙</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- URLs per page -->
|
|
<div class="nav-item mt-3">
|
|
<strong>URLs per page</strong>
|
|
<div class="card-body">
|
|
<!-- Individual Status Checkboxes -->
|
|
{% for url_per_page in list_urls_per_page %}
|
|
<div class="items-form-check">
|
|
<input class="form-check-input items" type="radio" name="items" id="value-{{ url_per_page }}" value="{{ url_per_page }}">
|
|
<label class="form-check-label" for="value-{{ url_per_page }}">{{ url_per_page }}</label>
|
|
</div>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="2" class="text-center">No options available.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status -->
|
|
<div class="nav-item mt-3">
|
|
<strong>Select status</strong>
|
|
<form id="status-filter-form">
|
|
<!-- Toggle All Checkbox -->
|
|
<div class="status-form-check">
|
|
<input class="form-check-input" type="checkbox" id="toggle-all-status">
|
|
<label class="form-check-label fw-bold" for="toggle-all-status">
|
|
Toggle all
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Individual Status Checkboxes -->
|
|
{% for status in list_status %}
|
|
<div class="status-form-check">
|
|
<input class="form-check-input status-checkbox" type="checkbox" value="{{ status }}" id="status-{{ status }}">
|
|
<label class="form-check-label" for="status-{{ status }}">
|
|
{{ status }}
|
|
</label>
|
|
</div>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="2" class="text-center">No statuses available.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Sources -->
|
|
<div class="nav-item mt-3">
|
|
<strong>Select sources</strong>
|
|
<form id="source-filter-form">
|
|
<!-- Toggle All Checkbox -->
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="toggle-all-sources">
|
|
<label class="form-check-label fw-bold" for="toggle-all-sources">
|
|
Toggle all
|
|
</label>
|
|
</div>
|
|
<!-- Individual Source Checkboxes -->
|
|
{% for source in sources %}
|
|
<div class="form-check">
|
|
<input class="form-check-input source-checkbox" type="checkbox" value="{{ source.id }}" id="source-{{ source.id }}">
|
|
<label class="form-check-label" for="source-{{ source.id }}">
|
|
{{ source.source }}
|
|
</label>
|
|
</div>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="2" class="text-center">No sources available.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Searches -->
|
|
<div class="nav-item mt-3">
|
|
<strong>Select searches</strong>
|
|
<form id="search-filter-form">
|
|
<!-- Toggle All Checkbox -->
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="toggle-all-searches">
|
|
<label class="form-check-label fw-bold" for="toggle-all-searches">
|
|
Toggle all
|
|
</label>
|
|
</div>
|
|
<!-- Individual Search Checkboxes -->
|
|
{% for search in searches %}
|
|
<div class="form-check">
|
|
<input class="form-check-input search-checkbox" type="checkbox" value="{{ search.id }}" id="search-{{ search.id }}">
|
|
<label class="form-check-label" for="search-{{ search.id }}">
|
|
[{{ search.type }}] {{ search.search }}
|
|
</label>
|
|
</div>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="2" class="text-center">No search available.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</form>
|
|
</div>
|
|
|
|
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Main Content Area -->
|
|
<div id="content" class="main-content">
|
|
<div class="container mt-4">
|
|
|
|
<!-- Table -->
|
|
<div id="item-list">
|
|
{% include 'urls_partial.html' %}
|
|
</div>
|
|
<!-- Loading... -->
|
|
<div id="loading" class="text-center mt-3" style="display:none;">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|