This update improves filter syncing, OIDC settings, and mobile UX, with fixes for archived items, menus, and the login page.

* Major usability and reliability improvements across frontend and backend
* Filters (Status, Tag, Vendor, Type, Search, Sort) now persist across views and sync via API for cross-device consistency
* Archived warranties correctly appear under “All” without affecting other filters
* Enhanced OIDC system with admin group support, secure secret handling, and improved attribute synchronization
* New responsive mobile hamburger menu and tablet-specific UI enhancements
* Fixed archived item styling, menu initialization, and login page layout issues
* Includes minor UX refinements, PyJWT compatibility updates, and deprecation clean-ups
This commit is contained in:
sassanix
2025-10-06 20:31:04 -03:00
parent a3f0c7264f
commit 4b377f4259
19 changed files with 878 additions and 51 deletions
+76 -1
View File
@@ -1,6 +1,81 @@
# Changelog
## 0.10.1.13 - 2025-09-27
## 0.10.1.14 - 2025-10-06
### Enhanced
- Filters persist like view settings and survive navigation/view changes:
- Persist filters (Status, Tag, Vendor, Type, Search, Sort) to localStorage and, when authenticated, sync to API preferences (`saved_filters`) for cross-device consistency.
- Skip API writes on initial page load (mirrors view preference behavior) to avoid noisy saves.
- Switching views now re-applies the full filter set via `applyFilters()` instead of resetting results.
- _Files: `frontend/script.js`, `frontend/index.html`, `backend/auth_routes.py`, `backend/migrations/046_add_saved_filters_column.sql`_
- All Status includes archived warranties (personal scope): When Status is set to "All" (and not in Global View), archived warranties are fetched and merged into the list to give a complete view. Archived items are flagged and rendered with the correct labeling and actions.
- _Files: `frontend/script.js`_
- Index page filter persistence and reset refinements:
- Restore saved Search input on load (value, clear button visibility, and active state styling).
- Persist Search alongside Status, Tag, Vendor, and Type when applying filters.
- Clear resets filters to defaults, removes saved `warrantyFilters` and `warrantySortBy`, clears Search UI, and closes the Filter popover.
- _Files: `frontend/index.html`_
- PyJWT compatibility: Updated authentication handling to support PyJWT 2.10.
- _Files: `backend/auth_utils.py`, `backend/oidc_handler.py`_
- Deprecated datetime usage: Replaced deprecated `utcnow()` calls with timezone-aware alternatives.
- _Files: `backend/*`_
- OIDC-managed user settings: Hide/disable settings managed by OIDC to avoid conflicting edits.
- _Files: `backend/oidc_handler.py`, `frontend/settings-new.html`, `frontend/settings-new.js`_
- OIDC attribute synchronization: Sync key OIDC attributes on login for consistency.
- _Files: `backend/oidc_handler.py`, `backend/auth_utils.py`_
- UX: Made the entire user menu item clickable (not just the text).
- _Files: `frontend/index.html`, `frontend/style.css`_
- Mobile UX: Hide user menu button on mobile and use hamburger as the sole trigger; when authenticated, the mobile menu uses the username as the section title for clarity. No desktop changes.
- _Files: `frontend/mobile-header.css`, `frontend/script.js`_
- Login page tablet logo (769820px): Show the Warracker logo/title above the login form on iPad Air and similar tablet widths to match mobile branding.
- _Files: `frontend/style.css`_
### Added
- OIDC admin via groups: Allow determining admin status from configured OIDC group membership.
- _Files: `backend/oidc_handler.py`, `backend/auth_utils.py`, `backend/config.py`_
- OIDC admin group setting: Added configurable OIDC admin group in site settings.
- _Files: `backend/config.py`, `backend/app.py`_
- Configurable upload folder: Make the upload folder path configurable.
- _Files: `backend/config.py`, `backend/file_routes.py`_
- Secrets from files: Support reading sensitive settings from `*_FILE` paths.
- _Files: `backend/config.py`_
- Secure default secret: Attempt to generate a secure secret at runtime if none is provided.
- _Files: `backend/config.py`, `backend/app.py`_
- Mobile hamburger menu (≤768px): Added modern slideout panel with overlay for Index, Status, Settings, and About. Dynamically clones nav links and user/auth actions into the panel, locks body scroll while open, and closes on overlay or link click. Desktop layout unaffected.
- _Files: `frontend/mobile-header.css`, `frontend/index.html`, `frontend/status.html`, `frontend/settings-new.html`, `frontend/about.html`, `frontend/script.js`, `frontend/mobile-menu.js`_
### Fixed
- Archived styling parity under All: Archived items displayed under "All" now use the same neutral styling as the dedicated Archived view (per-card `.warranty-card.archived` styles).
- _Files: `frontend/style.css`, `frontend/script.js`_
- Exclude archived from non-archived filters: Archived warranties no longer appear under specific status filters (Active, Expiring, Expired). They show only under "All" and "Archived".
- _Files: `frontend/script.js`_
- Re-merge archived after filter changes: Switching away from Archived and back to All reliably reloads and re-merges archived items to keep the view consistent.
- _Files: `frontend/script.js`_
- OIDC reload: Fixed OIDC client reload by moving `init_oidc_client` to an appropriate lifecycle.
- _Files: `backend/oidc_handler.py`, `backend/app.py`_
- OIDC userinfo: Stopped relying on token `userinfo` claim; use the userinfo endpoint as source of truth.
- _Files: `backend/oidc_handler.py`_
- Settings page mobile menu toggle: Fixed initialization by isolating the hamburger logic into a dedicated script to avoid duplicate identifier errors in the global script, ensuring the menu opens/closes correctly.
- _Files: `frontend/mobile-menu.js`, `frontend/settings-new.html`, `frontend/script.js`_
- Login page header/menu regression: Restored standalone login page by removing header/hamburger and mobile menu assets.
- _Files: `frontend/login.html`_
- Login button label corrected: Button now reads just "Login"; page title uses a separate translation key to avoid suffix leaking into the button. Introduced `auth.login_title` and updated the login page to reference it.
- _Files: `locales/en/translation.json`, `frontend/login.html`_
- Status dashboard tablet layout (769820px, iPad Air): Summary cards now display in two rows with Active and Expiring Soon on the first row, and Expired and Total on the second row for clearer hierarchy at this width.
- _Files: `frontend/style.css`_
### Credit
- OIDC and configuration improvements contributed by @tecosaur in PR #138.
## 0.10.1.13 - 2025-09-29
### Added
- Turkish language support added to all pages (index, about, status, settings) with comprehensive translations for UI elements, messages, and system text.
+11 -4
View File
@@ -9,6 +9,7 @@ from email.mime.text import MIMEText
import os
import pytz
import psycopg2
from psycopg2.extras import Json
# Use relative imports for project modules
try:
@@ -849,7 +850,7 @@ def get_preferences():
cursor.execute("""
SELECT up.email_notifications, up.default_view, up.theme, up.expiring_soon_days,
up.notification_frequency, up.notification_time, up.timezone, up.currency_symbol, up.date_format, up.notification_channel, up.apprise_notification_time, up.apprise_notification_frequency, up.apprise_timezone, up.currency_position, up.paperless_view_in_app,
u.preferred_language
u.preferred_language, up.saved_filters
FROM user_preferences up
JOIN users u ON up.user_id = u.id
WHERE up.user_id = %s
@@ -874,7 +875,8 @@ def get_preferences():
'apprise_timezone': preferences_data[12] if preferences_data[12] else 'UTC',
'currency_position': preferences_data[13] if preferences_data[13] else 'left',
'paperless_view_in_app': preferences_data[14] if len(preferences_data) > 14 and preferences_data[14] is not None else False,
'preferred_language': preferences_data[15] if len(preferences_data) > 15 and preferences_data[15] else 'en'
'preferred_language': preferences_data[15] if len(preferences_data) > 15 and preferences_data[15] else 'en',
'saved_filters': preferences_data[16] if len(preferences_data) > 16 and preferences_data[16] is not None else None
}
else:
# Create default preferences for user, but get language from users table
@@ -968,6 +970,7 @@ def update_preferences():
apprise_timezone = data.get('apprise_timezone')
paperless_view_in_app = data.get('paperless_view_in_app')
preferred_language = data.get('preferred_language')
saved_filters = data.get('saved_filters')
if default_view and default_view not in ['grid', 'list', 'table']:
return jsonify({'message': 'Invalid default view'}), 400
@@ -1048,6 +1051,9 @@ def update_preferences():
if paperless_view_in_app is not None:
update_fields.append("paperless_view_in_app = %s")
update_values.append(paperless_view_in_app)
if saved_filters is not None:
update_fields.append("saved_filters = %s")
update_values.append(Json(saved_filters))
if update_fields:
update_query = f"UPDATE user_preferences SET {', '.join(update_fields)} WHERE user_id = %s"
@@ -1073,7 +1079,7 @@ def update_preferences():
cursor.execute("""
SELECT up.email_notifications, up.default_view, up.theme, up.expiring_soon_days,
up.notification_frequency, up.notification_time, up.timezone, up.currency_symbol, up.date_format, up.notification_channel, up.apprise_notification_time, up.apprise_notification_frequency, up.apprise_timezone, up.currency_position, up.paperless_view_in_app,
u.preferred_language
u.preferred_language, up.saved_filters
FROM user_preferences up
JOIN users u ON up.user_id = u.id
WHERE up.user_id = %s
@@ -1098,7 +1104,8 @@ def update_preferences():
'apprise_timezone': preferences_data[12] if preferences_data[12] else 'UTC',
'currency_position': preferences_data[13] if preferences_data[13] else 'left',
'paperless_view_in_app': preferences_data[14] if len(preferences_data) > 14 and preferences_data[14] is not None else False,
'preferred_language': preferences_data[15] if len(preferences_data) > 15 and preferences_data[15] else 'en'
'preferred_language': preferences_data[15] if len(preferences_data) > 15 and preferences_data[15] else 'en',
'saved_filters': preferences_data[16] if len(preferences_data) > 16 and preferences_data[16] is not None else None
}
else:
preferences = {
@@ -0,0 +1,23 @@
-- Add saved_filters column to user_preferences table to store filter preferences
-- This allows filters to persist across devices for authenticated users
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'user_preferences'
AND column_name = 'saved_filters'
) THEN
ALTER TABLE user_preferences
ADD COLUMN saved_filters JSONB DEFAULT NULL;
RAISE NOTICE 'Added saved_filters column to user_preferences table';
ELSE
RAISE NOTICE 'saved_filters column already exists in user_preferences table';
END IF;
END $$;
-- Add comment for documentation
COMMENT ON COLUMN user_preferences.saved_filters IS 'JSON object storing user filter preferences (status, tag, vendor, warranty_type, search, sortBy)';
+6 -2
View File
@@ -25,6 +25,7 @@
<script src="theme-loader.js?v=20250119001"></script> <!-- Apply theme early -->
<script src="include-auth-new.js?v=20250119001"></script> <!-- Handles auth state display -->
<script src="fix-auth-buttons-loader.js?v=20250119001"></script> <!-- Fixes auth button display timing -->
<script src="script.js?v=20250119001" defer></script>
<!-- i18next Local Scripts -->
<script src="js/lib/i18next.min.js?v=20250119001"></script>
<script src="js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
@@ -275,6 +276,7 @@
</div>
<!-- Group for right-aligned elements -->
<div class="header-right-group">
<button class="mobile-menu-toggle" aria-label="Toggle menu"><i class="fas fa-bars"></i></button>
<div id="authContainer" class="auth-buttons">
<a href="login.html" class="auth-btn login-btn">
<i class="fas fa-sign-in-alt"></i> Login
@@ -311,6 +313,8 @@
</div> <!-- End header-right-group -->
</div>
</header>
<div class="mobile-menu-panel" id="mobileMenuPanel"></div>
<div class="mobile-menu-overlay" id="mobileMenuOverlay"></div>
<!-- Main Content Area -->
<div class="container" style="padding: 0;">
@@ -319,7 +323,7 @@
<!-- Hero Section -->
<div class="about-hero">
<h1><i class="fas fa-shield-alt"></i> Warracker</h1>
<div class="version" id="versionDisplay" data-i18n="about.version">Version v0.10.1.13</div>
<div class="version" id="versionDisplay" data-i18n="about.version">Version v0.10.1.14</div>
<p class="description" data-i18n="about.description">
A comprehensive warranty management system designed to help you track, organize, and manage all your product warranties in one secure, user-friendly platform.
</p>
@@ -417,7 +421,7 @@
// Update version display dynamically
const versionDisplay = document.getElementById('versionDisplay');
if (versionDisplay && window.i18next) {
const currentVersion = '0.10.1.13'; // This should match version-checker.js
const currentVersion = '0.10.1.14'; // This should match version-checker.js
versionDisplay.textContent = window.i18next.t('about.version') + ' v' + currentVersion;
}
+31 -2
View File
@@ -115,6 +115,7 @@
</div>
<!-- Group for right-aligned elements -->
<div class="header-right-group">
<button class="mobile-menu-toggle" aria-label="Toggle menu"><i class="fas fa-bars"></i></button>
<div id="authContainer" class="auth-buttons">
<a href="login.html" class="auth-btn login-btn">
<i class="fas fa-sign-in-alt"></i> <span data-i18n="auth.login">Login</span>
@@ -151,6 +152,8 @@
</div> <!-- End header-right-group -->
</div>
</header>
<div class="mobile-menu-panel" id="mobileMenuPanel"></div>
<div class="mobile-menu-overlay" id="mobileMenuOverlay"></div>
<!-- Main Content -->
<div class="container">
@@ -1525,6 +1528,14 @@
if (tagFilter && savedFilters.tag) tagFilter.value = savedFilters.tag;
if (vendorFilter && savedFilters.vendor) vendorFilter.value = savedFilters.vendor;
if (warrantyTypeFilter && savedFilters.warranty_type) warrantyTypeFilter.value = savedFilters.warranty_type;
// Restore search field if present
var searchInput = document.getElementById('searchWarranties');
var clearSearchBtn = document.getElementById('clearSearch');
if (searchInput && savedFilters.search) {
searchInput.value = savedFilters.search;
if (clearSearchBtn) clearSearchBtn.style.display = 'flex';
if (searchInput.parentElement) searchInput.parentElement.classList.add('active-search');
}
}
} catch (e) { /* no-op */ }
@@ -1533,14 +1544,16 @@
[statusFilter, tagFilter, vendorFilter, warrantyTypeFilter].forEach(function (el) {
if (el) el.dispatchEvent(new Event('change', { bubbles: true }));
});
// Persist filters
// Persist filters including search
try {
var prefix = (window.getPreferenceKeyPrefix ? window.getPreferenceKeyPrefix() : 'user_');
var searchInput = document.getElementById('searchWarranties');
var filtersToSave = {
status: statusFilter ? statusFilter.value : 'all',
tag: tagFilter ? tagFilter.value : 'all',
vendor: vendorFilter ? vendorFilter.value : 'all',
warranty_type: warrantyTypeFilter ? warrantyTypeFilter.value : 'all'
warranty_type: warrantyTypeFilter ? warrantyTypeFilter.value : 'all',
search: searchInput ? searchInput.value : ''
};
localStorage.setItem(prefix + 'warrantyFilters', JSON.stringify(filtersToSave));
} catch (e) { /* no-op */ }
@@ -1555,10 +1568,26 @@
if (tagFilter) tagFilter.value = 'all';
if (vendorFilter) vendorFilter.value = 'all';
if (warrantyTypeFilter) warrantyTypeFilter.value = 'all';
// Clear search field and its UI state
var searchInput = document.getElementById('searchWarranties');
var clearSearchBtn = document.getElementById('clearSearch');
if (searchInput) {
searchInput.value = '';
if (searchInput.parentElement) searchInput.parentElement.classList.remove('active-search');
}
if (clearSearchBtn) clearSearchBtn.style.display = 'none';
[statusFilter, tagFilter, vendorFilter, warrantyTypeFilter].forEach(function (el) {
if (el) el.dispatchEvent(new Event('change', { bubbles: true }));
});
// Clear persisted filter preferences on reset (including search and sort)
try {
var prefix = (window.getPreferenceKeyPrefix ? window.getPreferenceKeyPrefix() : 'user_');
localStorage.removeItem(prefix + 'warrantyFilters');
localStorage.removeItem(prefix + 'warrantySortBy');
} catch (e) { /* no-op */ }
updateFilterIndicator();
if (filterPopover) filterPopover.classList.remove('active');
});
}
+12 -2
View File
@@ -4,7 +4,7 @@
<script src="auth-redirect.js?v=20250119001" data-protected="false"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="auth.login">Login - Warracker</title>
<title data-i18n="auth.login_title">Login - Warracker</title>
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png?v=2">
@@ -12,14 +12,25 @@
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css?v=20250119004">
<script src="theme-loader.js?v=20250119001"></script>
<script>
// If user is authenticated and has a theme in user_info (from previous sessions), reflect it early
try {
const user = JSON.parse(localStorage.getItem('user_info') || '{}');
if (user && user.theme) {
document.documentElement.setAttribute('data-theme', user.theme);
}
} catch (_) {}
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="js/lib/i18next.min.js?v=20250119001"></script>
<script src="js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
<script src="js/lib/i18nextBrowserLanguageDetector.min.js?v=20250119001"></script>
<script src="js/i18n.js?v=20250119001"></script>
<script src="registration-status.js?v=20250119001"></script>
</head>
<body class="login-page-body">
<div class="login-wrapper">
<div class="login-showcase">
@@ -46,7 +57,6 @@
<div class="mobile-top-logo-title"><span data-i18n="app_title">Warracker</span></div>
</div>
<div class="auth-container">
<h2 class="auth-title" data-i18n="auth.login">Login</h2>
<div id="authMessage" class="auth-message"></div>
<form id="loginForm" class="auth-form">
<div class="form-group">
+96
View File
@@ -1156,3 +1156,99 @@
padding: 2px 5px !important;
}
}
/* ===== MOBILE HAMBURGER MENU - BASE ===== */
.mobile-menu-toggle {
display: none;
}
/* ===== MOBILE HAMBURGER MENU ===== */
@media (max-width: 768px) {
header .nav-links {
display: none !important;
}
/* Hide user menu button on mobile; hamburger remains */
header .user-menu, header #userMenuBtn, header .user-btn {
display: none !important;
}
.mobile-menu-toggle {
display: block !important;
background: none !important;
border: none !important;
color: var(--text-color) !important;
font-size: 1.5rem !important;
cursor: pointer !important;
padding: 8px !important;
}
.mobile-menu-panel {
position: fixed !important;
top: 0 !important;
right: -280px !important;
width: 280px !important;
height: 100% !important;
background-color: var(--card-bg) !important;
box-shadow: -2px 0 15px rgba(0,0,0,0.2) !important;
transition: right 0.3s ease-in-out !important;
z-index: 1100 !important;
padding: 20px !important;
overflow-y: auto !important;
}
.mobile-menu-panel.is-open {
right: 0 !important;
}
.mobile-menu-overlay {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
background-color: rgba(0, 0, 0, 0.5) !important;
opacity: 0 !important;
visibility: hidden !important;
transition: opacity 0.3s ease-in-out, visibility 0.3s !important;
z-index: 1099 !important;
}
.mobile-menu-overlay.is-open {
opacity: 1 !important;
visibility: visible !important;
}
body.mobile-menu-active {
overflow: hidden !important;
}
.mobile-menu-panel .section-title {
font-weight: 600 !important;
margin: 10px 0 !important;
color: var(--text-color) !important;
opacity: 0.8 !important;
}
.mobile-menu-panel .menu-list {
display: flex !important;
flex-direction: column !important;
gap: 6px !important;
margin: 0 0 12px 0 !important;
padding: 0 !important;
}
.mobile-menu-panel a,
.mobile-menu-panel .user-menu-item > * {
display: block !important;
padding: 10px 8px !important;
color: var(--text-color) !important;
text-decoration: none !important;
border-radius: 6px !important;
}
.mobile-menu-panel a:hover,
.mobile-menu-panel .user-menu-item:hover {
background-color: var(--hover-bg) !important;
}
}
+146
View File
@@ -0,0 +1,146 @@
(function() {
function buildMobileMenuContent(panel, header) {
if (!panel || !header) return;
if (panel.getAttribute('data-built') === 'true') return;
panel.innerHTML = '';
const createSection = (titleText) => {
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = titleText;
panel.appendChild(title);
};
const appendLinks = (links) => {
if (!links || links.length === 0) return;
const list = document.createElement('div');
list.className = 'menu-list';
links.forEach(a => {
try {
const cloned = a.cloneNode(true);
if (cloned.id) cloned.id = '';
list.appendChild(cloned);
} catch (_) {}
});
panel.appendChild(list);
};
const isAuthenticated = !!(function(){ try { return localStorage.getItem('auth_token'); } catch(_) { return null; } })();
let displayName = '';
const headerDisplayNameEl = header.querySelector('#userDisplayName');
if (headerDisplayNameEl && headerDisplayNameEl.textContent) {
displayName = headerDisplayNameEl.textContent.trim();
}
if (!displayName) {
try {
const userInfo = JSON.parse(localStorage.getItem('user_info') || 'null');
if (userInfo) displayName = userInfo.first_name || userInfo.username || 'Account';
} catch(_) {}
}
if (!displayName) displayName = 'Account';
const navLinksContainer = header.querySelector('.nav-links');
const navAnchors = navLinksContainer ? Array.from(navLinksContainer.querySelectorAll('a')) : [];
if (navAnchors.length) {
createSection('Navigation');
appendLinks(navAnchors);
}
const authContainer = header.querySelector('#authContainer');
const authAnchors = (!isAuthenticated && authContainer) ? Array.from(authContainer.querySelectorAll('a')) : [];
if (!isAuthenticated && authAnchors.length) {
createSection('Account');
appendLinks(authAnchors);
}
const userDropdown = header.querySelector('#userMenuDropdown');
const userMenuAnchors = userDropdown ? Array.from(userDropdown.querySelectorAll('a')) : [];
if (userMenuAnchors.length) {
createSection(displayName || 'Menu');
appendLinks(userMenuAnchors);
const logoutSource = header.querySelector('#logoutMenuItem');
if (logoutSource) {
const logoutLink = document.createElement('a');
logoutLink.href = '#';
logoutLink.id = 'mobileLogoutLink';
logoutLink.innerHTML = '<i class="fas fa-sign-out-alt"></i> <span>Logout</span>';
logoutLink.addEventListener('click', async function(e) {
e.preventDefault();
try {
if (window.auth && typeof window.auth.logout === 'function') {
await window.auth.logout();
} else {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
window.location.href = 'login.html';
}
} catch (_) {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
window.location.href = 'login.html';
}
});
const list = document.createElement('div');
list.className = 'menu-list';
list.appendChild(logoutLink);
panel.appendChild(list);
}
}
panel.setAttribute('data-built', 'true');
}
function initializeMobileMenu() {
const header = document.querySelector('header');
if (!header) return;
const toggleBtn = header.querySelector('.mobile-menu-toggle');
const panel = document.getElementById('mobileMenuPanel');
const overlay = document.getElementById('mobileMenuOverlay');
if (!toggleBtn || !panel || !overlay) return;
if (toggleBtn.getAttribute('data-mm-bound') === '1') return;
toggleBtn.setAttribute('data-mm-bound', '1');
const openMenu = () => {
buildMobileMenuContent(panel, header);
panel.classList.add('is-open');
overlay.classList.add('is-open');
document.body.classList.add('mobile-menu-active');
};
const closeMenu = () => {
panel.classList.remove('is-open');
overlay.classList.remove('is-open');
document.body.classList.remove('mobile-menu-active');
};
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
const isOpen = panel.classList.contains('is-open');
if (isOpen) closeMenu(); else openMenu();
});
overlay.addEventListener('click', function() {
closeMenu();
});
panel.addEventListener('click', function(e) {
const link = e.target.closest('a');
if (link && link.href) {
closeMenu();
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && panel.classList.contains('is-open')) {
closeMenu();
}
});
}
window.initializeMobileMenu = initializeMobileMenu;
document.addEventListener('DOMContentLoaded', initializeMobileMenu);
})();
+8
View File
@@ -19,6 +19,8 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Load header fix styles to ensure consistent header styling -->
<link rel="stylesheet" href="header-fix.css?v=20250119001">
<!-- Mobile Header specific styles -->
<link rel="stylesheet" href="mobile-header.css?v=20250119002">
<!-- i18next Local Scripts -->
<script src="js/lib/i18next.min.js?v=20250119001"></script>
<script src="js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
@@ -27,6 +29,7 @@
<script src="js/i18n.js?v=20250119001"></script>
<!-- Load fix for auth buttons -->
<script src="fix-auth-buttons-loader.js?v=20250119001"></script>
<script src="script.js?v=20250119001" defer></script>
<style>
.auth-container {
max-width: 500px;
@@ -285,8 +288,13 @@
<i class="fas fa-shield-alt"></i>
<h1>Warracker</h1>
</div>
<div class="header-right-group">
<button class="mobile-menu-toggle" aria-label="Toggle menu"><i class="fas fa-bars"></i></button>
</div>
</div>
</header>
<div class="mobile-menu-panel" id="mobileMenuPanel"></div>
<div class="mobile-menu-overlay" id="mobileMenuOverlay"></div>
<!-- Main Content -->
<div class="container">
+8
View File
@@ -16,6 +16,8 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Load header fix styles to ensure consistent header styling -->
<link rel="stylesheet" href="header-fix.css?v=20250119001">
<!-- Mobile Header specific styles -->
<link rel="stylesheet" href="mobile-header.css?v=20250119002">
<!-- i18next Local Scripts -->
<script src="js/lib/i18next.min.js?v=20250119001"></script>
<script src="js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
@@ -24,6 +26,7 @@
<script src="js/i18n.js?v=20250119001"></script>
<!-- Load fix for auth buttons -->
<script src="fix-auth-buttons-loader.js?v=20250119001"></script>
<script src="script.js?v=20250119001" defer></script>
<style>
.auth-container {
max-width: 400px;
@@ -104,8 +107,13 @@
<i class="fas fa-shield-alt"></i>
<h1><a href="index.html" style="color: inherit; text-decoration: none; cursor: pointer;">Warracker</a></h1>
</div>
<div class="header-right-group">
<button class="mobile-menu-toggle" aria-label="Toggle menu"><i class="fas fa-bars"></i></button>
</div>
</div>
</header>
<div class="mobile-menu-panel" id="mobileMenuPanel"></div>
<div class="mobile-menu-overlay" id="mobileMenuOverlay"></div>
<!-- Main Content -->
<div class="container">
+8
View File
@@ -16,6 +16,8 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Load header fix styles to ensure consistent header styling -->
<link rel="stylesheet" href="header-fix.css?v=20250119001">
<!-- Mobile Header specific styles -->
<link rel="stylesheet" href="mobile-header.css?v=20250119002">
<!-- i18next Local Scripts -->
<script src="js/lib/i18next.min.js?v=20250119001"></script>
<script src="js/lib/i18nextHttpBackend.min.js?v=20250119001"></script>
@@ -24,6 +26,7 @@
<script src="js/i18n.js?v=20250119001"></script>
<!-- Load fix for auth buttons -->
<script src="fix-auth-buttons-loader.js?v=20250119001"></script>
<script src="script.js?v=20250119001" defer></script>
<style>
.auth-container {
max-width: 400px;
@@ -161,8 +164,13 @@
<i class="fas fa-shield-alt"></i>
<h1><a href="index.html" style="color: inherit; text-decoration: none; cursor: pointer;">Warracker</a></h1>
</div>
<div class="header-right-group">
<button class="mobile-menu-toggle" aria-label="Toggle menu"><i class="fas fa-bars"></i></button>
</div>
</div>
</header>
<div class="mobile-menu-panel" id="mobileMenuPanel"></div>
<div class="mobile-menu-overlay" id="mobileMenuOverlay"></div>
<!-- Main Content -->
<div class="container">
+312 -24
View File
@@ -1,3 +1,160 @@
// ===== MOBILE HAMBURGER MENU (kept for other pages) =====
(function() {
function buildMobileMenuContent(panel, header) {
if (!panel || !header) return;
// Build only once
if (panel.getAttribute('data-built') === 'true') return;
panel.innerHTML = '';
const createSection = (titleText) => {
const title = document.createElement('div');
title.className = 'section-title';
title.textContent = titleText;
panel.appendChild(title);
};
const appendLinks = (links) => {
if (!links || links.length === 0) return;
const list = document.createElement('div');
list.className = 'menu-list';
links.forEach(a => {
try {
const cloned = a.cloneNode(true);
// Remove ids to avoid duplicates
if (cloned.id) cloned.id = '';
list.appendChild(cloned);
} catch (e) {
// Skip problematic nodes
}
});
panel.appendChild(list);
};
// Resolve auth state and display name
const isAuthenticated = !!(function(){ try { return localStorage.getItem('auth_token'); } catch(_) { return null; } })();
let displayName = '';
const headerDisplayNameEl = header.querySelector('#userDisplayName');
if (headerDisplayNameEl && headerDisplayNameEl.textContent) {
displayName = headerDisplayNameEl.textContent.trim();
}
if (!displayName) {
try {
const userInfo = JSON.parse(localStorage.getItem('user_info') || 'null');
if (userInfo) displayName = userInfo.first_name || userInfo.username || 'Account';
} catch(_) {}
}
if (!displayName) displayName = 'Account';
// Nav links
const navLinksContainer = header.querySelector('.nav-links');
const navAnchors = navLinksContainer ? Array.from(navLinksContainer.querySelectorAll('a')) : [];
if (navAnchors.length) {
createSection('Navigation');
appendLinks(navAnchors);
}
// Auth buttons if present (only when NOT authenticated)
const authContainer = header.querySelector('#authContainer');
const authAnchors = (!isAuthenticated && authContainer) ? Array.from(authContainer.querySelectorAll('a')) : [];
if (!isAuthenticated && authAnchors.length) {
createSection('Account');
appendLinks(authAnchors);
}
// User menu dropdown items if present
const userDropdown = header.querySelector('#userMenuDropdown');
const userMenuAnchors = userDropdown ? Array.from(userDropdown.querySelectorAll('a')) : [];
if (userMenuAnchors.length) {
// Use username as section title when available
createSection(displayName || 'Menu');
appendLinks(userMenuAnchors);
// Add Logout if available
const logoutSource = header.querySelector('#logoutMenuItem');
if (logoutSource) {
const logoutLink = document.createElement('a');
logoutLink.href = '#';
logoutLink.id = 'mobileLogoutLink';
logoutLink.innerHTML = '<i class="fas fa-sign-out-alt"></i> <span>Logout</span>';
logoutLink.addEventListener('click', async function(e) {
e.preventDefault();
try {
if (window.auth && typeof window.auth.logout === 'function') {
await window.auth.logout();
} else {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
window.location.href = 'login.html';
}
} catch (_) {
localStorage.removeItem('auth_token');
localStorage.removeItem('user_info');
window.location.href = 'login.html';
}
});
const list = document.createElement('div');
list.className = 'menu-list';
list.appendChild(logoutLink);
panel.appendChild(list);
}
}
panel.setAttribute('data-built', 'true');
}
function initializeMobileMenu() {
const header = document.querySelector('header');
if (!header) return;
const toggleBtn = header.querySelector('.mobile-menu-toggle');
const panel = document.getElementById('mobileMenuPanel');
const overlay = document.getElementById('mobileMenuOverlay');
if (!toggleBtn || !panel || !overlay) return;
// Prevent double-binding
if (toggleBtn.getAttribute('data-mm-bound') === '1') return;
toggleBtn.setAttribute('data-mm-bound', '1');
const openMenu = () => {
buildMobileMenuContent(panel, header);
panel.classList.add('is-open');
overlay.classList.add('is-open');
document.body.classList.add('mobile-menu-active');
};
const closeMenu = () => {
panel.classList.remove('is-open');
overlay.classList.remove('is-open');
document.body.classList.remove('mobile-menu-active');
};
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
const isOpen = panel.classList.contains('is-open');
if (isOpen) closeMenu(); else openMenu();
});
overlay.addEventListener('click', function() {
closeMenu();
});
panel.addEventListener('click', function(e) {
const link = e.target.closest('a');
if (link && link.href) {
closeMenu();
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && panel.classList.contains('is-open')) {
closeMenu();
}
});
}
// Expose and initialize
window.initializeMobileMenu = initializeMobileMenu;
document.addEventListener('DOMContentLoaded', initializeMobileMenu);
})();
console.log('[SCRIPT VERSION] 20250529-005 - Added CSS cache busting for consistent styling across domains');
console.log('[DEBUG] script.js loaded and running');
@@ -7,6 +164,7 @@ console.log('[DEBUG] script.js loaded and running');
let warranties = [];
let warrantiesLoaded = false; // Track if warranties have been loaded from API
let lastLoadedArchived = false; // Track if the current warranties array came from archived endpoint
let lastLoadedIncludesArchived = false; // Track if the current warranties list includes archived items
let currentTabIndex = 0;
let tabContents = []; // Initialize as empty array
let editMode = false;
@@ -372,6 +530,38 @@ function setTheme(isDark) {
}
}
// Persist theme to API similar to view/filters
async function saveThemePreference(isDark, saveToApi = true) {
try {
// Always persist locally first
setTheme(isDark);
if (saveToApi && window.auth && window.auth.isAuthenticated && window.auth.isAuthenticated()) {
const token = window.auth.getToken();
if (token) {
try {
console.log('[Theme] Saving theme preference to API:', isDark ? 'dark' : 'light');
const response = await fetch('/api/auth/preferences', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ theme: isDark ? 'dark' : 'light' })
});
if (!response.ok) {
console.warn('[Theme] Failed to save theme to API:', response.status);
}
} catch (err) {
console.error('[Theme] Error saving theme to API:', err);
}
}
}
} catch (e) {
console.warn('Failed to save theme preference', e);
}
}
// Initialization logic on DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
// Register Service Worker
@@ -1495,9 +1685,16 @@ async function switchView(viewType, saveToApi = true) { // Added saveToApi param
tableViewHeader.classList.toggle('visible', viewType === 'table');
}
// Re-render warranties only if warrantiesList exists AND warranties have been loaded from API
// Re-apply current filters after view change to preserve user's selection
try {
// Persist current filters before render (defensive no-op if unchanged)
// Skip API save on initial load to mirror view settings behavior
saveFilterPreferences(false);
} catch (_) {}
if (warrantiesList && warrantiesLoaded) {
renderWarranties(filterWarranties()); // Assuming filterWarranties() returns the correct array
// Reapply full filter set (status, tag, vendor, type, search, sort)
applyFilters();
}
// Update header dropdown label if present
@@ -1555,7 +1752,7 @@ function loadViewPreference() {
// Dark mode toggle
if (darkModeToggle) { // Add check for darkModeToggle
darkModeToggle.addEventListener('change', (e) => {
setTheme(e.target.checked);
saveThemePreference(e.target.checked);
});
}
@@ -1753,6 +1950,8 @@ function processWarrantyData(warranty) {
// Create a copy of the warranty object to avoid modifying the original
const processedWarranty = { ...warranty };
// Flag archived items when merged into All view
processedWarranty.is_archived = !!(warranty.__isArchived || warranty.is_archived);
// Ensure product_name exists
if (!processedWarranty.product_name) {
@@ -2029,12 +2228,41 @@ async function loadWarranties(isAuthenticated) { // Added isAuthenticated parame
console.error('[DEBUG] API did not return an array! Data:', data);
}
// Update isGlobalView to match the loaded data
isGlobalView = shouldUseGlobalView;
console.log(`[DEBUG] Set isGlobalView to: ${isGlobalView}`);
// Optionally merge archived items into the "All" view (only in personal scope)
let combinedData = Array.isArray(data) ? data : [];
lastLoadedIncludesArchived = false;
if (!shouldUseGlobalView && !isArchivedView && currentFilters && currentFilters.status === 'all') {
try {
const archivedUrl = `${baseUrl}/api/warranties/archived`;
const archivedResp = await fetch(archivedUrl, options);
if (archivedResp.ok) {
const archivedData = await archivedResp.json();
const archivedMarked = Array.isArray(archivedData)
? archivedData.map(w => ({ ...w, __isArchived: true }))
: [];
combinedData = combinedData.concat(archivedMarked);
lastLoadedIncludesArchived = true;
console.log(`[DEBUG] Merged ${archivedMarked.length} archived warranties into All view`);
} else {
// Log but do not block rendering of non-archived warranties
let errInfo = '';
try {
const errJson = await archivedResp.json();
errInfo = JSON.stringify(errJson);
} catch (_) {}
console.warn('[DEBUG] Failed to load archived warranties for All view:', archivedResp.status, errInfo);
}
} catch (mergeErr) {
console.warn('[DEBUG] Error while merging archived into All:', mergeErr);
}
}
// Process each warranty to calculate status and days remaining
warranties = Array.isArray(data) ? data.map(warranty => processWarrantyData(warranty)) : [];
warranties = Array.isArray(combinedData) ? combinedData.map(warranty => processWarrantyData(warranty)) : [];
lastLoadedArchived = isArchivedView;
console.log('[DEBUG] Final warranties array:', warranties);
console.log('[DEBUG] Total warranties loaded:', warranties.length);
@@ -2281,7 +2509,8 @@ async function renderWarranties(warrantiesToRender) {
const isLifetime = warranty.is_lifetime;
let statusClass = warranty.status || 'unknown';
let statusText = warranty.statusText || 'Unknown Status';
if (isArchivedView) {
// If showing archived-only view or item itself is archived within All view
if (isArchivedView || warranty.is_archived) {
const archivedTextRaw = window.i18next ? window.i18next.t('warranties.archived') : 'Archived';
const archivedLabel = archivedTextRaw && archivedTextRaw !== 'warranties.archived' ? archivedTextRaw : 'Archived';
statusClass = 'archived';
@@ -2410,7 +2639,7 @@ async function renderWarranties(warrantiesToRender) {
<button class="${claimsButtonClass}" title="${claimsTitle}" data-id="${warranty.id}">
<i class="fas fa-clipboard-list"></i>
</button>
${isArchivedView ? `
${ (isArchivedView || warranty.is_archived) ? `
<button class="action-btn unarchive-btn" title="Unarchive" data-id="${warranty.id}">
<i class="fas fa-box-open"></i>
</button>
@@ -2438,7 +2667,8 @@ async function renderWarranties(warrantiesToRender) {
`;
const cardElement = document.createElement('div');
cardElement.className = `warranty-card ${statusClass === 'expired' ? 'expired' : statusClass === 'expiring' ? 'expiring-soon' : 'active'}`;
const isArchivedItem = isArchivedView || warranty.is_archived;
cardElement.className = `warranty-card ${isArchivedItem ? 'archived' : (statusClass === 'expired' ? 'expired' : statusClass === 'expiring' ? 'expiring-soon' : 'active')}`;
// Claims button styling will be handled in the action buttons HTML generation
@@ -2813,11 +3043,19 @@ function applyFilters() {
// If leaving archived view, reload normal source
loadWarranties(true);
return;
} else if (currentFilters.status === 'all' && !lastLoadedIncludesArchived) {
// Ensure All view re-merges archived items after switching away and back
loadWarranties(true);
return;
}
// Filter warranties based on currentFilters
const filtered = warranties.filter(warranty => {
// Status filter
// Exclude archived items from specific status views (only show in 'all' or 'archived')
if (warranty.is_archived && currentFilters.status !== 'all' && currentFilters.status !== 'archived') {
return false;
}
// Status filter: allow archived items to pass in All view
if (currentFilters.status !== 'all' && currentFilters.status !== 'archived' && warranty.status !== currentFilters.status) {
return false;
}
@@ -2874,29 +3112,50 @@ function applyFilters() {
}
// --- Persist and restore filters/sort ---
function saveFilterPreferences() {
async function saveFilterPreferences(saveToApi = true) {
try {
const prefix = getPreferenceKeyPrefix();
const filtersToSave = {
status: currentFilters.status || 'all',
tag: currentFilters.tag || 'all',
vendor: currentFilters.vendor || 'all',
warranty_type: currentFilters.warranty_type || 'all'
warranty_type: currentFilters.warranty_type || 'all',
search: currentFilters.search || '',
sortBy: currentFilters.sortBy || 'expiration'
};
localStorage.setItem(`${prefix}warrantyFilters`, JSON.stringify(filtersToSave));
// Save to API if authenticated and saveToApi is true
if (saveToApi && window.auth && window.auth.isAuthenticated()) {
const token = window.auth.getToken();
if (token) {
try {
console.log('[Filters] Saving filter preferences to API:', filtersToSave);
const response = await fetch('/api/auth/preferences', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ saved_filters: filtersToSave })
});
if (response.ok) {
console.log('[Filters] Successfully saved filter preferences to API');
} else {
console.warn('[Filters] Failed to save filter preferences to API:', response.status);
}
} catch (error) {
console.error('[Filters] Error saving filter preferences to API:', error);
}
}
}
} catch (e) {
console.warn('Failed to save filter preferences', e);
}
}
function saveSortPreference() {
try {
const prefix = getPreferenceKeyPrefix();
localStorage.setItem(`${prefix}warrantySortBy`, currentFilters.sortBy || 'expiration');
} catch (e) {
console.warn('Failed to save sort preference', e);
}
}
// Deprecated: Sort preference is now saved as part of saveFilterPreferences
function loadFilterAndSortPreferences() {
try {
@@ -2905,16 +3164,28 @@ function loadFilterAndSortPreferences() {
if (savedFiltersRaw) {
const savedFilters = JSON.parse(savedFiltersRaw);
if (savedFilters && typeof savedFilters === 'object') {
console.log('[Filters] Loading saved filters from localStorage:', savedFilters);
currentFilters.status = savedFilters.status || currentFilters.status;
currentFilters.tag = savedFilters.tag || currentFilters.tag;
currentFilters.vendor = savedFilters.vendor || currentFilters.vendor;
currentFilters.warranty_type = savedFilters.warranty_type || currentFilters.warranty_type;
currentFilters.search = savedFilters.search || '';
currentFilters.sortBy = savedFilters.sortBy || currentFilters.sortBy;
// Restore search input UI state
const searchInput = document.getElementById('searchWarranties');
const clearSearchBtn = document.getElementById('clearSearch');
if (searchInput && savedFilters.search) {
searchInput.value = savedFilters.search;
// Show clear button if search has value
if (clearSearchBtn) {
clearSearchBtn.style.display = 'flex';
}
// Add active search class
searchInput.parentElement.classList.add('active-search');
}
}
}
const savedSort = localStorage.getItem(`${prefix}warrantySortBy`);
if (savedSort) {
currentFilters.sortBy = savedSort;
}
} catch (e) {
console.warn('Failed to load filter/sort preferences', e);
}
@@ -5490,6 +5761,7 @@ function setupUIEventListeners() {
const tagFilter = document.getElementById('tagFilter');
const sortBySelect = document.getElementById('sortBy');
const vendorFilter = document.getElementById('vendorFilter'); // Added vendor filter select
const warrantyTypeFilter = document.getElementById('warrantyTypeFilter'); // Added warranty type filter select
if (searchInput) {
// Debounce logic: only apply filters after user stops typing for 300ms
@@ -5510,6 +5782,7 @@ function setupUIEventListeners() {
if (searchDebounceTimeout) clearTimeout(searchDebounceTimeout);
searchDebounceTimeout = setTimeout(() => {
applyFilters();
saveFilterPreferences();
}, 300);
});
}
@@ -5523,6 +5796,7 @@ function setupUIEventListeners() {
searchInput.parentElement.classList.remove('active-search');
searchInput.focus();
applyFilters();
saveFilterPreferences();
}
});
}
@@ -5563,10 +5837,12 @@ function setupUIEventListeners() {
sortBySelect.addEventListener('change', () => {
currentFilters.sortBy = sortBySelect.value;
applyFilters();
saveSortPreference();
saveFilterPreferences();
});
}
// Note: Clear Filters button handler is in index.html inline script to close popover
// View switcher event listeners
const gridViewBtn = document.getElementById('gridViewBtn');
const listViewBtn = document.getElementById('listViewBtn');
@@ -7327,6 +7603,18 @@ async function loadAndApplyUserPreferences(isAuthenticated) { // Added isAuthent
localStorage.setItem(`${prefix}defaultView`, apiPrefs.default_view);
console.log(`[Prefs Loader] Saved ${prefix}defaultView: ${apiPrefs.default_view}`);
}
// Apply theme from API and sync to localStorage
if (apiPrefs.theme) {
const isDark = apiPrefs.theme === 'dark';
// Apply without triggering API save to avoid loops
await saveThemePreference(isDark, false);
console.log(`[Prefs Loader] Applied theme from API: ${apiPrefs.theme}`);
}
// Save filter preferences from API to localStorage
if (apiPrefs.saved_filters) {
localStorage.setItem(`${prefix}warrantyFilters`, JSON.stringify(apiPrefs.saved_filters));
console.log(`[Prefs Loader] Saved ${prefix}warrantyFilters:`, apiPrefs.saved_filters);
}
if (apiPrefs.expiring_soon_days !== undefined) {
localStorage.setItem(`${prefix}expiringSoonDays`, apiPrefs.expiring_soon_days);
// Also update the global variable used by processWarrantyData
+6
View File
@@ -41,6 +41,9 @@
<!-- Temporary debug script -->
<script src="js/i18n-debug.js?v=20250119001"></script>
<!-- Mobile menu logic isolated to avoid collisions on settings -->
<script src="mobile-menu.js?v=20250119002" defer></script>
<!-- Immediate authentication check script -->
<script>
@@ -115,6 +118,7 @@
</div>
<!-- Group for right-aligned elements -->
<div class="header-right-group">
<button class="mobile-menu-toggle" aria-label="Toggle menu"><i class="fas fa-bars"></i></button>
<div id="userMenu" class="user-menu" style="display: none;">
<button class="user-btn" id="userMenuBtn">
<i class="fas fa-user-circle"></i>
@@ -148,6 +152,8 @@
</div> <!-- End header-right-group -->
</div>
</header>
<div class="mobile-menu-panel" id="mobileMenuPanel"></div>
<div class="mobile-menu-overlay" id="mobileMenuOverlay"></div>
<!-- Main Content -->
<div class="container">
+5
View File
@@ -25,6 +25,8 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Load header fix styles to ensure consistent header styling -->
<link rel="stylesheet" href="header-fix.css?v=20250119001">
<!-- Mobile Header specific styles -->
<link rel="stylesheet" href="mobile-header.css?v=20250119002">
<!-- Chart.js for visualizations -->
<script src="chart.js?v=20250119001"></script>
<!-- i18next Local Scripts -->
@@ -320,6 +322,7 @@
</div>
<!-- Group for right-aligned elements -->
<div class="header-right-group">
<button class="mobile-menu-toggle" aria-label="Toggle menu"><i class="fas fa-bars"></i></button>
<div id="authContainer" class="auth-buttons">
<a href="login.html" class="auth-btn login-btn">
<i class="fas fa-sign-in-alt"></i> Login
@@ -356,6 +359,8 @@
</div> <!-- End header-right-group -->
</div>
</header>
<div class="mobile-menu-panel" id="mobileMenuPanel"></div>
<div class="mobile-menu-overlay" id="mobileMenuOverlay"></div>
<!-- Main Content -->
<div class="container">
+42 -1
View File
@@ -266,6 +266,37 @@
}
}
// --- Status filter persistence for Status Page ---
function saveStatusFilterPreference(value) {
try {
const prefix = getPreferenceKeyPrefix();
const key = `${prefix}statusPageStatusFilter`;
if (!value || value === 'all') {
// Treat 'all' as reset; clear saved preference
localStorage.removeItem(key);
console.log(`Cleared saved status filter (key=${key})`);
} else {
localStorage.setItem(key, value);
console.log(`Saved status filter '${value}' (key=${key})`);
}
} catch (error) {
console.error('Error saving status filter preference:', error);
}
}
function loadStatusFilterPreference() {
try {
const prefix = getPreferenceKeyPrefix();
const key = `${prefix}statusPageStatusFilter`;
const saved = localStorage.getItem(key);
console.log(`Loaded status filter preference: ${saved} (key=${key})`);
return saved || 'all';
} catch (error) {
console.error('Error loading status filter preference:', error);
return 'all';
}
}
function showViewSwitcher() {
const viewSwitcher = document.getElementById('viewSwitcher');
if (viewSwitcher) {
@@ -1286,7 +1317,17 @@
// Setup event listeners for status page specific controls
if (refreshDashboardBtn) refreshDashboardBtn.addEventListener('click', refreshDashboard);
if (searchWarranties) searchWarranties.addEventListener('input', filterAndSortWarranties);
if (statusFilter) statusFilter.addEventListener('change', filterAndSortWarranties);
// Restore saved status filter selection on load
if (statusFilter) {
const savedStatus = loadStatusFilterPreference();
if (savedStatus && statusFilter.value !== savedStatus) {
statusFilter.value = savedStatus;
}
statusFilter.addEventListener('change', function() {
saveStatusFilterPreference(statusFilter.value);
filterAndSortWarranties();
});
}
if (exportBtn) {
exportBtn.addEventListener('click', function() {
console.log("Status page export button clicked (status.js IIFE).");
+71 -9
View File
@@ -267,6 +267,14 @@ header .container {
.panel-header h2 { font-size: 1.25rem; }
}
/* Hide only Filter and Sort text below 479px (keep icons and indicator visible) */
@media (max-width: 479px) {
.filter-controls #filterBtn span:not(#filterIndicator),
.filter-controls #sortBtn span {
display: none;
}
}
@media (max-width: 375px) {
.panel-header h2 { font-size: 1.15rem; }
}
@@ -778,12 +786,16 @@ input.invalid {
}
/* Neutral styling for archived view */
.archived-view .warranty-card {
/* Neutral styling for archived view, and per-card when archived appears in All */
.archived-view .warranty-card,
.warranty-card.archived {
border-left: 4px solid var(--medium-gray);
}
.table-view.archived-view .warranty-card,
.archived-view .table-view .warranty-card {
.archived-view .table-view .warranty-card,
.table-view .warranty-card.archived,
.warranty-card.archived .table-view .warranty-card {
border-left: none;
background-color: rgba(128, 128, 128, 0.08);
}
@@ -799,7 +811,8 @@ input.invalid {
}
/* Neutralize header background in archived view */
.archived-view .warranty-card .warranty-header {
.archived-view .warranty-card .warranty-header,
.warranty-card.archived .warranty-header {
background-color: rgba(128, 128, 128, 0.06);
}
@@ -1814,6 +1827,22 @@ tr.status-expiring {
}
}
/* Tablet layout for iPad Air (769820px):
Row 1: Active | Expiring Soon
Row 2: Expired | Total */
@media (min-width: 769px) and (max-width: 820px) {
.status-cards {
grid-template-columns: repeat(2, 1fr);
grid-template-areas:
"active expiring"
"expired total";
}
.status-card[data-status="active"] { grid-area: active; }
.status-card[data-status="expiring"] { grid-area: expiring; }
.status-card[data-status="expired"] { grid-area: expired; }
.status-card[data-status="total"] { grid-area: total; }
}
/* Error message on status page */
.error-message {
text-align: center;
@@ -3408,33 +3437,39 @@ input.invalid {
}
/* Neutralize top header bar in archived view (all layouts) */
.archived-view .warranty-card .product-name-header {
.archived-view .warranty-card .product-name-header,
.warranty-card.archived .product-name-header {
background-color: var(--medium-gray) !important;
color: var(--text-color);
}
/* Ensure table view also stays neutral in archived view */
.archived-view.table-view .warranty-card .product-name-header,
.archived-view .table-view .warranty-card .product-name-header {
.archived-view .table-view .warranty-card .product-name-header,
.table-view .warranty-card.archived .product-name-header {
background-color: rgba(128, 128, 128, 0.12) !important;
color: var(--text-color);
}
/* Ensure heading text and action icons are readable in archived view */
.archived-view .warranty-card .product-name-header .warranty-title {
.archived-view .warranty-card .product-name-header .warranty-title,
.warranty-card.archived .product-name-header .warranty-title {
color: var(--text-color) !important;
}
.archived-view .warranty-card .warranty-actions .action-btn {
.archived-view .warranty-card .warranty-actions .action-btn,
.warranty-card.archived .warranty-actions .action-btn {
color: var(--text-color) !important;
}
.archived-view .warranty-card .warranty-actions .action-btn:hover {
.archived-view .warranty-card .warranty-actions .action-btn:hover,
.warranty-card.archived .warranty-actions .action-btn:hover {
background-color: rgba(0, 0, 0, 0.08);
color: var(--text-color) !important;
}
:root[data-theme="dark"] .archived-view .warranty-card .warranty-actions .action-btn:hover {
:root[data-theme="dark"] .archived-view .warranty-card .warranty-actions .action-btn:hover,
:root[data-theme="dark"] .warranty-card.archived .warranty-actions .action-btn:hover {
background-color: rgba(255, 255, 255, 0.12);
}
@@ -5827,3 +5862,30 @@ html.dark-mode .warracker-footer a:hover {
color: var(--text-color);
}
}
/* Show top logo on iPad Air/tablets up to 820px */
@media (min-width: 769px) and (max-width: 820px) {
.login-page-body .login-form-container {
flex-direction: column;
align-items: center;
padding-bottom: 32px;
}
.login-page-body .mobile-top-logo {
display: flex;
justify-content: center;
align-items: center;
margin: 20px 0 10px 0;
flex-direction: column;
}
.login-page-body .mobile-top-logo img {
width: 120px;
height: 120px;
object-fit: contain;
border-radius: 16px;
}
.login-page-body .mobile-top-logo-title {
margin-top: 6px;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-color);
}
}
+12 -2
View File
@@ -9,13 +9,23 @@
// console.log(`Theme applied on load: ${theme}`);
}
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
// Default to light theme initially
let isDarkMode = false;
// Check localStorage for the standardized 'darkMode' key
// Priority: cookie 'theme' -> localStorage 'darkMode' -> system preference
const themeCookie = getCookie('theme');
const savedTheme = localStorage.getItem('darkMode');
if (savedTheme !== null) {
if (themeCookie === 'dark' || themeCookie === 'light') {
isDarkMode = themeCookie === 'dark';
} else if (savedTheme !== null) {
// Use the saved theme preference
isDarkMode = savedTheme === 'true';
} else {
+1 -1
View File
@@ -1,6 +1,6 @@
// Version checker for Warracker
document.addEventListener('DOMContentLoaded', () => {
const currentVersion = '0.10.1.13'; // Current version of the application
const currentVersion = '0.10.1.14'; // Current version of the application
const updateStatus = document.getElementById('updateStatus');
const updateLink = document.getElementById('updateLink');
const versionDisplay = document.getElementById('versionDisplay');
+4 -3
View File
@@ -9,6 +9,7 @@
},
"auth": {
"login": "Login",
"login_title": "Login - Warracker",
"logout": "Logout",
"register": "Register",
"username": "Username",
@@ -213,7 +214,7 @@
"add_serial_number": "Add another serial number"
},
"settings": {
"title": "Settings",
"title": "Warracker - Settings",
"account_settings": "Account Settings",
"preferences": "Preferences",
"language": "Language",
@@ -602,7 +603,7 @@
"tr": "Türkçe"
},
"status": {
"title": "System Status",
"title": "Status - Warracker",
"dashboard_title": "Warranty Status Dashboard",
"global_dashboard_title": "Global Warranty Status Dashboard",
"overview": "Overview",
@@ -623,7 +624,7 @@
"uptime": "Uptime"
},
"about": {
"title": "About Warracker",
"title": "About - Warracker",
"description": "A comprehensive warranty management system designed to help you track, organize, and manage all your product warranties in one secure, user-friendly platform.",
"version": "Version",
"update_status": "Update Status",