-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
10760/add global book filter #11331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
10760/add global book filter #11331
Conversation
for more information, see https://pre-commit.ci
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements a global book filter system that allows users to filter books by type (readable/preview/all), language, and publication date range. The filter preferences persist across page loads using localStorage and are synchronized with backend cookies to maintain consistency.
- Added client-side preference management with localStorage persistence and cookie synchronization
- Created a slide-out filter panel UI integrated into the top navigation bar
- Modified carousel components to respect global filter preferences when loading book data
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| static/js/preferences.js | Core preference management functions for getting/setting filters and cookie synchronization |
| static/js/preferences-handler.js | DOM event handlers for the filter panel UI interactions |
| openlibrary/templates/site/alert.html | Added filter panel UI with styling and removed language dropdown from top bar |
| openlibrary/plugins/upstream/account.py | Backend endpoint to handle preference updates and set cookies |
| openlibrary/plugins/openlibrary/js/carousel/Carousel.js | Modified carousel to use global preferences for filtering book data |
| openlibrary/i18n/messages.pot | Updated translation references after removing language dropdown |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| Math.max(1800, Math.min(2025, isNaN(startYear) ? 1900 : startYear)), | ||
| Math.max(1800, Math.min(2025, isNaN(endYear) ? 2025 : endYear)) |
Copilot
AI
Oct 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The year range validation logic uses hardcoded values (1800, 2025, 1900) that appear in multiple places. Consider defining these as constants at the top of the file for better maintainability.
|
Looking at the javascript CI: |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
Hi @mekarpeles, I’ve implemented the suggested fixes from my last PR. While testing by loading some books, it looks like they aren’t reflecting the updated preference logic. Could you please let me know if there are additional steps I should take, or if there might be a conflict between the new implementation and the existing codebase? |
| $ supported_languages = get_supported_languages() | ||
| $ active_ui_lang = supported_languages.get(lang) or supported_languages.get('en') | ||
|
|
||
| <style> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine approach for prototyping
Before merge, we'll want to move these styles to the appropriate location with /static/less :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @mekarpeles, let me read through the documentation as well as separate our Language option and our filter.
|
Hey @mekarpeles, I have migrated this <style> to the proper CSS files and here's the result for now:
Thank you so much! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 19 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return { | ||
| mode: parsed.global?.mode || 'all', | ||
| language: parsed.global?.language || 'all', | ||
| date: parsed.global?.date || [1900, 2025] |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded year range [1900, 2025] should be made a constant for maintainability and consistency. This value appears multiple times throughout the codebase (lines 18, 21, 46, 51-52, 65, 100) and will need annual updates. Consider defining const DEFAULT_YEAR_RANGE = [1900, new Date().getFullYear()] at the top of the file.
| <script type="module"> | ||
| import '/static/js/preferences-handler.js'; | ||
|
|
||
| const filterTrigger = document.getElementById('filter-panel-trigger'); |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Inconsistent naming: The CSS class is filter-panel-trigger (line 24) but the variable name in JavaScript is filterTrigger (line 78). While this works, for better searchability across the codebase, consider using a consistent hyphenated or camelCase naming convention.
| const initialBackendParams = mapPreferencesToBackend(initialPrefs); | ||
| handleGlobalFilterChange(initialBackendParams); | ||
| } |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The carousel initialization logic unconditionally calls handleGlobalFilterChange on initial load (line 149), which will immediately clear and reload the carousel even if no preferences are set. This could cause unnecessary API calls and poor user experience. Consider only applying filters if they differ from defaults or if the carousel hasn't already loaded data.
| const initialBackendParams = mapPreferencesToBackend(initialPrefs); | |
| handleGlobalFilterChange(initialBackendParams); | |
| } | |
| const defaultPrefs = {}; // Define default preferences as an empty object or as appropriate | |
| const initialBackendParams = mapPreferencesToBackend(initialPrefs); | |
| const defaultBackendParams = mapPreferencesToBackend(defaultPrefs); | |
| // Only reload if preferences differ from defaults or carousel is empty | |
| if ( | |
| JSON.stringify(initialBackendParams) !== JSON.stringify(defaultBackendParams) || | |
| this.slick.$slides.length === 0 | |
| ) { | |
| handleGlobalFilterChange(initialBackendParams); | |
| } |
static/css/components/alert.less
Outdated
| @white: #fff; | ||
| @border: #ccc; | ||
| @shadow: rgba(0, 0, 0, 0.15); | ||
| @header-bg: #f8f9fa; | ||
| @header-border: #e9ecef; | ||
| @text-dark: #333; | ||
| @text-mid: #666; | ||
| @placeholder: #999; | ||
| @primary: #4a90e2; | ||
| @primary-hover: #357abd; | ||
| @focus-shadow: rgba(74, 144, 226, 0.2); | ||
| @focus-shadow-strong: rgba(74, 144, 226, 0.3); | ||
| @z-index-panel: 1000; | ||
| @black: #000; |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Hardcoded color values are defined as LESS variables but should use the project's existing color system for consistency. Duplicating color definitions (like @white, @black, @border, etc.) across files can lead to inconsistencies. Consider importing and reusing existing color variables from the project's design system instead of redefining them.
| @white: #fff; | |
| @border: #ccc; | |
| @shadow: rgba(0, 0, 0, 0.15); | |
| @header-bg: #f8f9fa; | |
| @header-border: #e9ecef; | |
| @text-dark: #333; | |
| @text-mid: #666; | |
| @placeholder: #999; | |
| @primary: #4a90e2; | |
| @primary-hover: #357abd; | |
| @focus-shadow: rgba(74, 144, 226, 0.2); | |
| @focus-shadow-strong: rgba(74, 144, 226, 0.3); | |
| @z-index-panel: 1000; | |
| @black: #000; | |
| @import (reference) "../../design-system/colors.less"; | |
| @shadow: rgba(0, 0, 0, 0.15); | |
| @focus-shadow: rgba(74, 144, 226, 0.2); | |
| @focus-shadow-strong: rgba(74, 144, 226, 0.3); | |
| @z-index-panel: 1000; |
| <div class="language-component header-dropdown iabar-mobile"> | ||
| <summary> |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing opening <details> tag: The code shows a closing </details> tag on line 22, but the corresponding opening tag appears to have been removed in the changes (line 14 shows a <div> instead). The <summary> element on line 15 must be inside a <details> element. Add <details> after line 14 or restore the removed opening tag.
| logger.info("Parsed preferences data: %s", d) | ||
| except Exception as e: | ||
| logger.error("Failed to process preferences update: %s", str(e)) | ||
| return json.dumps({"error": "Failed to process request"}) |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handler returns a JSON string directly instead of using delegate.RawText with proper content type like the success case does (lines 808-811). This inconsistency could cause issues with the frontend expecting JSON.
Suggested fix:
return delegate.RawText(
json.dumps({"error": "Failed to process request"}),
content_type="application/json"
)| return json.dumps({"error": "Failed to process request"}) | |
| return delegate.RawText( | |
| json.dumps({"error": "Failed to process request"}), | |
| content_type="application/json" | |
| ) |
| import 'slick-carousel'; | ||
| import '../../../../../static/css/components/carousel--js.less'; | ||
| import { buildPartialsUrl } from '../utils.js'; | ||
| import { getGlobalPreferences, mapPreferencesToBackend } from '../../../../../../openlibrary/static/js/preferences.js' |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import path '../../../../../../openlibrary/static/js/preferences.js' is extremely fragile and appears incorrect. It goes up 7 levels and then into openlibrary/static, which suggests the file structure understanding may be wrong. The path should likely be '../../../../../static/js/preferences.js' or use an absolute path/alias. This will cause a runtime error when the module cannot be found.
| import { getGlobalPreferences, mapPreferencesToBackend } from '../../../../../../openlibrary/static/js/preferences.js' | |
| import { getGlobalPreferences, mapPreferencesToBackend } from '../../../../../static/js/preferences.js' |
| filterPanel.classList.add('hidden'); | ||
| filterTrigger.setAttribute('aria-expanded', 'false'); | ||
| } | ||
| }); |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing keyboard support: The filter panel can be closed by clicking outside (lines 101-107), but there's no keyboard equivalent (e.g., pressing Escape key). This creates an accessibility barrier for keyboard-only users. Consider adding an event listener for the Escape key to close the panel.
| }); | |
| }); | |
| // Close panel with Escape key for accessibility | |
| document.addEventListener('keydown', (e) => { | |
| // Only close if panel is open and Escape is pressed | |
| if (!filterPanel.classList.contains('hidden') && (e.key === 'Escape' || e.key === 'Esc')) { | |
| filterPanel.classList.remove('show'); | |
| filterPanel.classList.add('hidden'); | |
| filterTrigger.setAttribute('aria-expanded', 'false'); | |
| filterTrigger.focus(); | |
| } | |
| }); |
| }).then(res => res.json()).then(() => { | ||
| // 3. Trigger local UI update (carousel reload) | ||
| updateAllCarousels(); |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling: The fetch request lacks error handling for network failures or non-2xx responses. If the request fails, carousels will reload with new preferences even though the backend cookies weren't set, causing inconsistency between localStorage and server state.
Suggested fix:
fetch('/account/preferences', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...prefs, redirect: false })
}).then(res => {
if (!res.ok) throw new Error('Failed to save preferences');
return res.json();
}).then(() => {
updateAllCarousels();
}).catch(err => {
console.error('Error saving preferences:', err);
// Consider showing user feedback
});| }).then(res => res.json()).then(() => { | |
| // 3. Trigger local UI update (carousel reload) | |
| updateAllCarousels(); | |
| }).then(res => { | |
| if (!res.ok) throw new Error('Failed to save preferences'); | |
| return res.json(); | |
| }).then(() => { | |
| // 3. Trigger local UI update (carousel reload) | |
| updateAllCarousels(); | |
| }).catch(err => { | |
| console.error('Error saving preferences:', err); | |
| // Optionally, show user feedback here |
| # Add a POST redirect for prefs from global filter | ||
| class account_preferences(delegate.page): | ||
| path = "/account/preferences" | ||
| encoding = "json" | ||
|
|
||
| def POST(self): | ||
| logger.info("Received preferences update request") | ||
| try: | ||
| raw_data = web.data() | ||
| logger.info("Raw request data: %s", raw_data) | ||
| d = json.loads(raw_data) | ||
| logger.info("Parsed preferences data: %s", d) | ||
| except Exception as e: | ||
| logger.error("Failed to process preferences update: %s", str(e)) | ||
| return json.dumps({"error": "Failed to process request"}) | ||
| prefs = { | ||
| 'mode': d.get('mode', 'all'), | ||
| 'language': d.get('language', 'en'), | ||
| 'date': d.get('date', [1900, 2025]), | ||
| } | ||
|
|
||
| # Transform to backend format | ||
| backend_prefs = { | ||
| 'formats': ( | ||
| 'has_fulltext' | ||
| if prefs['mode'] == 'fulltext' | ||
| else 'ebook_access' if prefs['mode'] == 'preview' else None | ||
| ), | ||
| 'first_publish_year': prefs['date'], | ||
| } | ||
| if prefs['language'] != 'all': | ||
| backend_prefs['languages'] = [prefs['language']] | ||
|
|
||
| expires = 3600 * 24 * 365 | ||
| web.setcookie('ol_mode', prefs['mode'], expires=expires) | ||
| web.setcookie('ol_lang', prefs['language'], expires=expires) | ||
| web.setcookie('ol_date', ",".join(map(str, prefs['date'])), expires=expires) | ||
|
|
||
| if d.get('redirect', True): | ||
| raise web.seeother("/account") | ||
| else: | ||
| return delegate.RawText( | ||
| json.dumps({'status': 'ok', 'backend_prefs': backend_prefs}), | ||
| content_type="application/json", | ||
| ) | ||
|
|
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new account_preferences class lacks test coverage. Since this repository has comprehensive test coverage for the account module (see openlibrary/plugins/upstream/tests/test_account.py), this new API endpoint should have tests covering successful requests, error handling, cookie setting, and both redirect and non-redirect scenarios.
|
Hi @mekarpeles,
Answering these 3 questions will allow me to continue updating the backend pipeline for our filtering system. For now, here's what we have so far: Thank you so much for your response! |



Closes #10760
This PR aims to create a global filter with the following flow: UI change -> Save to localStorage for persistence across page loads
-> Data are sent to backend -> Backend sets cookies -> On success, trigger carousels reload -> Carousels read preferences from localStorage -> Carousels use preferences to fetch filtered data from Solr.
Technical
This implementation already confirms that UI interaction and cookies work correctly. However, since I haven't tested it on carousels with loaded books yet, I cannot confirm that carousels are able to load properly.
P/S: I sincerely apologize for my inactivity over the past month since I was away for military service. If you could kindly share the instructions on how to load books for testing, I’d really appreciate it. Thank you so much!