mirror of
https://github.com/sassanix/Warracker.git
synced 2026-05-06 16:39:18 -05:00
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:
+76
-1
@@ -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 (769–820px): 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 slide‑out 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 (769–820px, 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
@@ -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
@@ -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
@@ -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
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
})();
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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
@@ -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 (769–820px):
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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');
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user