From 0b6b45b1d7bc0fa8539d0dce635e35f2f637563d Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sat, 17 Jan 2026 15:25:13 -0500 Subject: [PATCH] feat: Introduce Shaarli Professional Theme with a modern sidebar layout and light/dark mode. --- shaarli-pro/css/style.css | 59 ++++++++++++++ shaarli-pro/includes.html | 21 ++--- shaarli-pro/js/script.js | 158 ++++++++++++++++++++++++++------------ 3 files changed, 177 insertions(+), 61 deletions(-) diff --git a/shaarli-pro/css/style.css b/shaarli-pro/css/style.css index 8ebec5b..891caf0 100644 --- a/shaarli-pro/css/style.css +++ b/shaarli-pro/css/style.css @@ -3113,3 +3113,62 @@ select:focus { .header-action-btn.has-active-filter { background: rgba(239, 68, 68, 0.2); } + +/* Filter Info Banner */ +.filter-info-banner { + display: flex; + align-items: center; + justify-content: space-between; + background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); + color: white; + padding: 0.75rem 1.25rem; + border-radius: 8px; + margin: 1rem 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.filter-info-banner.empty-results { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); +} + +.filter-info-content { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.95rem; +} + +.filter-info-content i { + font-size: 1.25rem; + opacity: 0.9; +} + +.filter-info-content strong { + background: rgba(255, 255, 255, 0.25); + padding: 0.15rem 0.5rem; + border-radius: 4px; + font-weight: 600; +} + +.filter-clear-btn { + display: flex; + align-items: center; + gap: 0.35rem; + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: white; + padding: 0.4rem 0.75rem; + border-radius: 6px; + cursor: pointer; + font-size: 0.85rem; + font-weight: 500; + transition: background 0.2s ease; +} + +.filter-clear-btn:hover { + background: rgba(255, 255, 255, 0.35); +} + +.filter-clear-btn i { + font-size: 1rem; +} diff --git a/shaarli-pro/includes.html b/shaarli-pro/includes.html index 322e41c..42f3dcc 100644 --- a/shaarli-pro/includes.html +++ b/shaarli-pro/includes.html @@ -4,8 +4,7 @@ - + @@ -26,16 +25,18 @@ {/if} {if="file_exists('tpl/shaarli-pro/extra.html')"} {include="extra"} -{/if} \ No newline at end of file +{/if} diff --git a/shaarli-pro/js/script.js b/shaarli-pro/js/script.js index 177b0f6..948fc13 100644 --- a/shaarli-pro/js/script.js +++ b/shaarli-pro/js/script.js @@ -402,38 +402,69 @@ document.addEventListener('DOMContentLoaded', () => { }); // Handle filter toggle switches + // Logic: + // - Visibility: all (neither checked), private (only private checked), public (only public checked) + // - Untagged: can be combined with any visibility function applyFilters() { const isPrivate = filterPrivate?.checked || false; const isPublic = filterPublic?.checked || false; const isUntagged = filterUntagged?.checked || false; let basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : ''; - let url = basePath + '/'; + + // Get current filter state from server-side rendered variables + const currentVisibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : ''; + const currentUntagged = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) || false; console.log('[Filter] Applying filters - private:', isPrivate, 'public:', isPublic, 'untagged:', isUntagged); + console.log('[Filter] Current state - visibility:', currentVisibility, 'untagged:', currentUntagged); - // Build the URL based on selected filters using the correct Shaarli admin paths - if (isPrivate && isUntagged) { - url = basePath + '/admin/visibility/private?searchtags='; - } else if (isPublic && isUntagged) { - url = basePath + '/admin/visibility/public?searchtags='; - } else if (isPrivate) { - url = basePath + '/admin/visibility/private'; + let url = basePath + '/'; + + // Determine desired visibility + let desiredVisibility = 'all'; + if (isPrivate) { + desiredVisibility = 'private'; } else if (isPublic) { - url = basePath + '/admin/visibility/public'; - } else if (isUntagged) { - url = basePath + '/?searchtags='; + desiredVisibility = 'public'; + } + + // Build URL based on desired state + if (desiredVisibility === 'private') { + url = basePath + '/admin/visibility/private'; + } else if (desiredVisibility === 'public') { + url = basePath + '/admin/visibility/public'; + } else { + // visibility = all - need to clear visibility if it was set + if (currentVisibility && currentVisibility !== '') { + url = basePath + '/admin/visibility/all'; + } else if (isUntagged !== currentUntagged) { + // Untagged state changed - /untagged-only works as toggle + url = basePath + '/untagged-only'; + } else { + url = basePath + '/'; + } } - // else: no filter, url stays as basePath + '/' console.log('[Filter] Navigating to:', url); window.location.href = url; } - // Clear all filters and go to home + // Clear all filters - go back to showing all bookmarks function clearAllFilters() { const basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : ''; - window.location.href = basePath + '/'; + const currentVisibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : ''; + const currentUntagged = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) || false; + + // If visibility is set, clear it first + if (currentVisibility && currentVisibility !== '') { + window.location.href = basePath + '/admin/visibility/all'; + } else if (currentUntagged) { + // Toggle untagged off using /untagged-only + window.location.href = basePath + '/untagged-only'; + } else { + window.location.href = basePath + '/'; + } } // Private and public are mutually exclusive @@ -455,24 +486,21 @@ document.addEventListener('DOMContentLoaded', () => { applyFilters(); }); - // Initialize filter states from URL (detect current filter state from page URL) + // Initialize filter states from server-side variables (set in includes.html) (function initFilterStates() { console.log('[Filter Debug] ========================================'); - // Detect filter state from current URL path - const currentPath = window.location.pathname; - const currentSearch = window.location.search; + // Get filter state from server-side rendered variables + const visibility = (typeof shaarli !== 'undefined' && shaarli.visibility) ? shaarli.visibility : ''; + const untaggedonly = (typeof shaarli !== 'undefined' && shaarli.untaggedonly) ? shaarli.untaggedonly : false; - // Detect visibility filter from URL path - let isPrivateActive = currentPath.includes('/visibility/private') || currentPath.includes('/admin/visibility/private'); - let isPublicActive = currentPath.includes('/visibility/public') || currentPath.includes('/admin/visibility/public'); + // Detect active filters from server variables + let isPrivateActive = visibility === 'private'; + let isPublicActive = visibility === 'public'; + let isUntaggedActive = untaggedonly === true; - // Detect untagged filter - let isUntaggedActive = currentPath.includes('/untagged-only') || - (currentSearch.includes('searchtags=') && !currentSearch.match(/searchtags=[^&]+/)); - - console.log('[Filter Debug] URL path:', currentPath); - console.log('[Filter Debug] URL search:', currentSearch); + console.log('[Filter Debug] shaarli.visibility:', visibility); + console.log('[Filter Debug] shaarli.untaggedonly:', untaggedonly); console.log('[Filter Debug] isPrivateActive:', isPrivateActive); console.log('[Filter Debug] isPublicActive:', isPublicActive); @@ -512,45 +540,66 @@ document.addEventListener('DOMContentLoaded', () => { // Create and display the filter info banner if (hasActiveFilter && !document.getElementById('filter-info-banner')) { - // Get result count from page - let resultCount = ''; + // Get result count from page - try multiple sources + let resultCount = 0; const pagingStats = document.querySelector('.paging-stats strong:last-child'); + const linkCount = document.querySelectorAll('.link-outer').length; + if (pagingStats) { - resultCount = pagingStats.textContent; + resultCount = parseInt(pagingStats.textContent) || 0; + } else if (linkCount > 0) { + resultCount = linkCount; } - // Build the message with result count like "X results with status private" - let filterParts = []; + // Check if no results (empty state) + const emptyState = document.querySelector('.empty-state'); + const isEmptyResults = emptyState !== null || resultCount === 0; + + // Build the message like the examples: + // "5 results without any tag" + // "5 results with status private without any tag" + // "1 result with status public without any tag" + let statusPart = ''; + let untaggedPart = ''; if (isPrivateActive) { - filterParts.push('with status private'); + statusPart = 'private'; } else if (isPublicActive) { - filterParts.push('with status public'); + statusPart = 'public'; } if (isUntaggedActive) { - if (filterParts.length > 0) { - filterParts.push('and untagged'); - } else { - filterParts.push('untagged'); + untaggedPart = 'without any tag'; + } + + // Build the message + let message = ''; + if (isEmptyResults) { + message = 'Nothing found.'; + } else { + const resultWord = resultCount === 1 ? 'result' : 'results'; + if (statusPart && untaggedPart) { + message = `${resultCount} ${resultWord} with status ${statusPart} ${untaggedPart}`; + } else if (statusPart) { + message = `${resultCount} ${resultWord} with status ${statusPart}`; + } else if (untaggedPart) { + message = `${resultCount} ${resultWord} ${untaggedPart}`; } } - let message = resultCount ? resultCount + ' results ' + filterParts.join(' ') : 'Showing ' + filterParts.join(' ') + ' links'; - - console.log('[Filter Debug] filterParts:', filterParts); + console.log('[Filter Debug] resultCount:', resultCount); + console.log('[Filter Debug] isEmptyResults:', isEmptyResults); console.log('[Filter Debug] message:', message); - if (filterParts.length > 0) { + if (message) { // Get base path safely const basePath = (typeof shaarli !== 'undefined' && shaarli.basePath) ? shaarli.basePath : ''; const banner = document.createElement('div'); banner.id = 'filter-info-banner'; - banner.className = 'filter-info-banner'; + banner.className = 'filter-info-banner' + (isEmptyResults ? ' empty-results' : ''); banner.innerHTML = `
- ${message}