Add Favorites and Remember Filters

This commit is contained in:
Joel Krauska
2025-10-07 14:04:14 -07:00
parent beefb4c5df
commit adda666a39
2 changed files with 221 additions and 2 deletions

View File

@@ -43,6 +43,22 @@
#share-button:active {
background-color: #3d8b40;
}
#reset-filters-button {
margin-left: 10px;
padding: 5px 15px;
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
#reset-filters-button:hover {
background-color: #da190b;
}
#reset-filters-button:active {
background-color: #c41e0d;
}
.blinking-tooltip {
background: white;
color: black;
@@ -60,6 +76,7 @@
</div>
<div style="text-align: center; margin-top: 5px;">
<button id="share-button" onclick="shareCurrentView()">🔗 Share This View</button>
<button id="reset-filters-button" onclick="resetFiltersToDefaults()">↺ Reset Filters To Defaults</button>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
@@ -193,6 +210,78 @@
map.fitBounds(bayAreaBounds);
}
// ---- LocalStorage for Filter Preferences ----
const FILTER_STORAGE_KEY = 'meshview_map_filters';
function getDefaultFilters() {
return {
routersOnly: false,
channels: {}
};
}
function saveFiltersToLocalStorage() {
const filters = {
routersOnly: document.getElementById("filter-routers-only").checked,
channels: {}
};
channels.forEach(channel => {
let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
let checkbox = document.getElementById(filterId);
if (checkbox) {
filters.channels[channel] = checkbox.checked;
}
});
localStorage.setItem(FILTER_STORAGE_KEY, JSON.stringify(filters));
console.log('Filters saved to localStorage:', filters);
}
function loadFiltersFromLocalStorage() {
try {
const stored = localStorage.getItem(FILTER_STORAGE_KEY);
if (stored) {
const filters = JSON.parse(stored);
console.log('Filters loaded from localStorage:', filters);
return filters;
}
} catch (error) {
console.error('Error loading filters from localStorage:', error);
}
return null;
}
function resetFiltersToDefaults() {
localStorage.removeItem(FILTER_STORAGE_KEY);
console.log('Filters reset to defaults');
// Reset routers only filter
document.getElementById("filter-routers-only").checked = false;
// Reset all channel filters to checked (default)
channels.forEach(channel => {
let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
let checkbox = document.getElementById(filterId);
if (checkbox) {
checkbox.checked = true;
}
});
updateMarkers();
// Show feedback to user
const button = document.getElementById('reset-filters-button');
const originalText = button.textContent;
button.textContent = '✓ Filters Reset!';
button.style.backgroundColor = '#2196F3';
setTimeout(() => {
button.textContent = originalText;
button.style.backgroundColor = '#f44336';
}, 2000);
}
// ---- Filters ----
let filterContainer = document.getElementById("filter-container");
channels.forEach(channel => {
@@ -203,6 +292,22 @@
label.innerHTML = `<input type="checkbox" class="filter-checkbox" id="${filterId}" checked> ${channel}`;
filterContainer.appendChild(label);
});
// Load saved filters from localStorage
const savedFilters = loadFiltersFromLocalStorage();
if (savedFilters) {
// Apply routers only filter
document.getElementById("filter-routers-only").checked = savedFilters.routersOnly || false;
// Apply channel filters
channels.forEach(channel => {
let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
let checkbox = document.getElementById(filterId);
if (checkbox && savedFilters.channels.hasOwnProperty(channel)) {
checkbox.checked = savedFilters.channels[channel];
}
});
}
function updateMarkers() {
let showRoutersOnly = document.getElementById("filter-routers-only").checked;
@@ -213,9 +318,15 @@
let marker = markerById[node.id];
if (marker) marker.setStyle({ fillOpacity: shouldShow ? 1 : 0 });
});
// Save filters to localStorage whenever they change
saveFiltersToLocalStorage();
}
document.querySelectorAll(".filter-checkbox").forEach(input => input.addEventListener("change", updateMarkers));
// Apply initial filters (from localStorage or defaults)
updateMarkers();
// ---- Edges ----
var edgeLayer = L.layerGroup().addTo(map);

View File

@@ -84,6 +84,36 @@ select, .export-btn, .search-box, .clear-btn {
font-weight: bold;
color: white;
}
.favorite-star {
cursor: pointer;
font-size: 1.2em;
user-select: none;
transition: color 0.2s;
}
.favorite-star:hover {
transform: scale(1.2);
}
.favorite-star.active {
color: #ffd700;
}
.favorites-btn {
background-color: #ffd700;
color: #000;
border: none;
}
.favorites-btn:hover {
background-color: #ffed4e;
}
.favorites-btn.active {
background-color: #ff6b6b;
color: white;
}
{% endblock %}
{% block body %}
@@ -106,6 +136,7 @@ select, .export-btn, .search-box, .clear-btn {
<option value="">All Firmware</option>
</select>
<button class="favorites-btn" id="favorites-btn">⭐ Show Favorites</button>
<button class="export-btn" id="export-btn">Export CSV</button>
<button class="clear-btn" id="clear-btn">Clear Filters</button>
</div>
@@ -127,6 +158,7 @@ select, .export-btn, .search-box, .clear-btn {
<th>Last Longitude <span class="sort-icon"></span></th>
<th>Channel <span class="sort-icon"></span></th>
<th>Last Update <span class="sort-icon"></span></th>
<th>Favorite</th>
</tr>
</thead>
<tbody id="node-table-body">
@@ -139,11 +171,38 @@ select, .export-btn, .search-box, .clear-btn {
let allNodes = [];
let sortColumn = "short_name"; // default sorted column
let sortAsc = true; // default ascending
let showOnlyFavorites = false;
// Declare headers and keyMap BEFORE any function that uses them
const headers = document.querySelectorAll("thead th");
const keyMap = ["short_name","long_name","hw_model","firmware","role","last_lat","last_long","channel","last_update"];
// LocalStorage functions for favorites
function getFavorites() {
const favorites = localStorage.getItem('nodelist_favorites');
return favorites ? JSON.parse(favorites) : [];
}
function saveFavorites(favorites) {
localStorage.setItem('nodelist_favorites', JSON.stringify(favorites));
}
function toggleFavorite(nodeId) {
let favorites = getFavorites();
const index = favorites.indexOf(nodeId);
if (index > -1) {
favorites.splice(index, 1);
} else {
favorites.push(nodeId);
}
saveFavorites(favorites);
applyFilters();
}
function isFavorite(nodeId) {
return getFavorites().includes(nodeId);
}
document.addEventListener("DOMContentLoaded", async function() {
const tbody = document.getElementById("node-table-body");
const roleFilter = document.getElementById("role-filter");
@@ -154,6 +213,7 @@ document.addEventListener("DOMContentLoaded", async function() {
const countSpan = document.getElementById("node-count");
const exportBtn = document.getElementById("export-btn");
const clearBtn = document.getElementById("clear-btn");
const favoritesBtn = document.getElementById("favorites-btn");
try {
const response = await fetch("/api/nodes?days_active=3");
@@ -174,6 +234,31 @@ document.addEventListener("DOMContentLoaded", async function() {
searchBox.addEventListener("input", applyFilters);
exportBtn.addEventListener("click", exportToCSV);
clearBtn.addEventListener("click", clearFilters);
favoritesBtn.addEventListener("click", toggleFavoritesFilter);
// Use event delegation for star clicks
tbody.addEventListener("click", (e) => {
if (e.target.classList.contains('favorite-star')) {
const nodeId = parseInt(e.target.getAttribute('data-node-id'));
// Get current favorites
let favorites = getFavorites();
const index = favorites.indexOf(nodeId);
const isNowFavorite = index === -1; // Will it be a favorite after toggle?
// Update the star immediately for instant feedback
if (isNowFavorite) {
e.target.classList.add('active');
e.target.textContent = '★';
} else {
e.target.classList.remove('active');
e.target.textContent = '☆';
}
// Save to localStorage
toggleFavorite(nodeId);
}
});
headers.forEach((th, index) => {
th.addEventListener("click", () => {
@@ -212,6 +297,18 @@ document.addEventListener("DOMContentLoaded", async function() {
});
}
function toggleFavoritesFilter() {
showOnlyFavorites = !showOnlyFavorites;
if (showOnlyFavorites) {
favoritesBtn.textContent = "⭐ Show All";
favoritesBtn.classList.add("active");
} else {
favoritesBtn.textContent = "⭐ Show Favorites";
favoritesBtn.classList.remove("active");
}
applyFilters();
}
function applyFilters() {
const searchTerm = searchBox.value.trim().toLowerCase();
@@ -227,7 +324,9 @@ document.addEventListener("DOMContentLoaded", async function() {
(node.node_id && node.node_id.toString().includes(searchTerm)) ||
(node.id && node.id.toString().includes(searchTerm));
return roleMatch && channelMatch && hwMatch && firmwareMatch && searchMatch;
const favoriteMatch = !showOnlyFavorites || isFavorite(node.node_id);
return roleMatch && channelMatch && hwMatch && firmwareMatch && searchMatch && favoriteMatch;
});
if (sortColumn) {
@@ -241,10 +340,14 @@ document.addEventListener("DOMContentLoaded", async function() {
function renderTable(nodes) {
tbody.innerHTML = "";
if (!nodes.length) {
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center; color:white;">No nodes found</td></tr>';
tbody.innerHTML = '<tr><td colspan="10" style="text-align:center; color:white;">No nodes found</td></tr>';
} else {
nodes.forEach(node => {
const row = document.createElement("tr");
const isFav = isFavorite(node.node_id);
const starClass = isFav ? 'favorite-star active' : 'favorite-star';
const starIcon = isFav ? '★' : '☆';
row.innerHTML = `
<td>${node.short_name || "N/A"}</td>
<td><a href="/packet_list/${node.node_id}">${node.long_name || "N/A"}</a></td>
@@ -255,7 +358,9 @@ document.addEventListener("DOMContentLoaded", async function() {
<td>${node.last_long ? (node.last_long / 1e7).toFixed(7) : "N/A"}</td>
<td>${node.channel || "N/A"}</td>
<td>${node.last_update ? new Date(node.last_update).toLocaleString() : "N/A"}</td>
<td style="text-align:center;"><span class="${starClass}" data-node-id="${node.node_id}">${starIcon}</span></td>
`;
tbody.appendChild(row);
});
}
@@ -270,6 +375,9 @@ document.addEventListener("DOMContentLoaded", async function() {
searchBox.value = "";
sortColumn = "short_name";
sortAsc = true;
showOnlyFavorites = false;
favoritesBtn.textContent = "⭐ Show Favorites";
favoritesBtn.classList.remove("active");
renderTable(allNodes);
updateSortIcons();
}