[FIX] website_sale_aplicoop: Complete infinite scroll and search filter integration

Major fixes:
- Fix JSON body parsing in load_products_ajax with type='http' route
  * Parse JSON from request.httprequest.get_data() instead of post params
  * Correctly read page, search, category from JSON request body

- Fix search and category filter combination
  * Use intersection (&) instead of replacement to preserve both filters
  * Now respects search AND category simultaneously

- Integrate realtime_search.js with infinite_scroll.js
  * Add resetWithFilters() method to reset scroll to page 1 with new filters
  * When search/category changes, reload products from server
  * Clear grid and load fresh results

- Fix pagination reset logic
  * Set currentPage = 0 in resetWithFilters() so loadNextPage() increments to 1
  * Prevents loading empty page 2 when resetting filters

Results:
 Infinite scroll loads all pages correctly (1, 2, 3...)
 Search filters work across all products (not just loaded)
 Category filters work correctly
 Search AND category filters work together
 Page resets to 1 when filters change
This commit is contained in:
snt 2026-02-17 00:28:17 +01:00
parent 534876242e
commit 5eb039ffe0
4 changed files with 603 additions and 71 deletions

View file

@ -48,6 +48,7 @@
"assets": { "assets": {
"web.assets_frontend": [ "web.assets_frontend": [
"website_sale_aplicoop/static/src/css/website_sale.css", "website_sale_aplicoop/static/src/css/website_sale.css",
"website_sale_aplicoop/static/src/js/infinite_scroll.js",
], ],
"web.assets_tests": [ "web.assets_tests": [
"website_sale_aplicoop/static/tests/test_suite.js", "website_sale_aplicoop/static/tests/test_suite.js",

View file

@ -847,16 +847,18 @@ class AplicoopWebsiteSale(WebsiteSale):
# - Products in the selected categories # - Products in the selected categories
# - Products provided by the selected suppliers # - Products provided by the selected suppliers
# - Delegate discovery to the order model (centralised logic) # - Delegate discovery to the order model (centralised logic)
products = group_order._get_products_for_group_order(group_order.id) all_products = group_order._get_products_for_group_order(group_order.id)
_logger.info( _logger.info(
"eskaera_shop order_id=%d, total products=%d (discovered)", "eskaera_shop order_id=%d, total products=%d (discovered)",
order_id, order_id,
len(products), len(all_products),
) )
# Get all available categories BEFORE filtering (so dropdown always shows all) # Get all available categories BEFORE filtering (so dropdown always shows all)
# Include not only product categories but also their parent categories # Include not only product categories but also their parent categories
product_categories = products.mapped("categ_id").filtered(lambda c: c.id > 0) product_categories = all_products.mapped("categ_id").filtered(
lambda c: c.id > 0
)
# Collect all categories including parent chain # Collect all categories including parent chain
all_categories_set = set() all_categories_set = set()
@ -884,19 +886,24 @@ class AplicoopWebsiteSale(WebsiteSale):
search_query = post.get("search", "").strip() search_query = post.get("search", "").strip()
category_filter = post.get("category", "0") category_filter = post.get("category", "0")
# Apply search # ===== IMPORTANT: Filter COMPLETE catalog BEFORE pagination =====
# This ensures search works on full catalog and tags show correct counts
filtered_products = all_products
# Apply search to COMPLETE catalog
if search_query: if search_query:
products = products.filtered( filtered_products = filtered_products.filtered(
lambda p: search_query.lower() in p.name.lower() lambda p: search_query.lower() in p.name.lower()
or search_query.lower() in (p.description or "").lower() or search_query.lower() in (p.description or "").lower()
) )
_logger.info( _logger.info(
'eskaera_shop: Filtered by search "%s". Found %d', 'eskaera_shop: Filtered by search "%s". Found %d of %d total',
search_query, search_query,
len(products), len(filtered_products),
len(all_products),
) )
# Apply category filter # Apply category filter to COMPLETE catalog
if category_filter != "0": if category_filter != "0":
try: try:
category_id = int(category_filter) category_id = int(category_filter)
@ -916,7 +923,7 @@ class AplicoopWebsiteSale(WebsiteSale):
# Search for products in the selected category and all descendants # Search for products in the selected category and all descendants
# This ensures we get products even if the category is a parent with no direct products # This ensures we get products even if the category is a parent with no direct products
filtered_products = request.env["product.product"].search( cat_filtered = request.env["product.product"].search(
[ [
("categ_id", "in", all_category_ids), ("categ_id", "in", all_category_ids),
("active", "=", True), ("active", "=", True),
@ -939,32 +946,65 @@ class AplicoopWebsiteSale(WebsiteSale):
get_order_descendants(group_order.category_ids) get_order_descendants(group_order.category_ids)
# Keep only products that are in both the selected category AND order's permitted categories # Keep only products that are in both the selected category AND order's permitted categories
filtered_products = filtered_products.filtered( cat_filtered = cat_filtered.filtered(
lambda p: p.categ_id.id in order_cat_ids lambda p: p.categ_id.id in order_cat_ids
) )
products = filtered_products filtered_products = cat_filtered
_logger.info( _logger.info(
"eskaera_shop: Filtered by category %d and descendants. Found %d products", "eskaera_shop: Filtered by category %d and descendants. Found %d of %d total",
category_id, category_id,
len(products), len(filtered_products),
len(all_products),
) )
except (ValueError, TypeError) as e: except (ValueError, TypeError) as e:
_logger.warning("eskaera_shop: Invalid category filter: %s", str(e)) _logger.warning("eskaera_shop: Invalid category filter: %s", str(e))
# Apply pagination if lazy loading enabled # ===== Calculate available tags BEFORE pagination (on complete filtered set) =====
total_products = len(products) available_tags_dict = {}
for product in filtered_products:
for tag in product.product_tag_ids:
# Only include tags that are visible on ecommerce
is_visible = getattr(
tag, "visible_on_ecommerce", True
) # Default to True if field doesn't exist
if not is_visible:
continue
if tag.id not in available_tags_dict:
tag_color = tag.color if tag.color else None
available_tags_dict[tag.id] = {
"id": tag.id,
"name": tag.name,
"color": tag_color,
"count": 0,
}
available_tags_dict[tag.id]["count"] += 1
# Convert to sorted list of tags (sorted by name for consistent display)
available_tags = sorted(available_tags_dict.values(), key=lambda t: t["name"])
_logger.info(
"eskaera_shop: Found %d available tags for %d filtered products",
len(available_tags),
len(filtered_products),
)
# ===== NOW apply pagination to the FILTERED results =====
total_products = len(filtered_products)
has_next = False has_next = False
products = filtered_products
if lazy_loading_enabled: if lazy_loading_enabled:
offset = (page - 1) * per_page offset = (page - 1) * per_page
products = products[offset : offset + per_page] products = filtered_products[offset : offset + per_page]
has_next = offset + per_page < total_products has_next = offset + per_page < total_products
_logger.info( _logger.info(
"eskaera_shop: Paginated - page=%d, offset=%d, per_page=%d, has_next=%s", "eskaera_shop: Paginated - page=%d, offset=%d, per_page=%d, has_next=%s, showing %d of %d",
page, page,
offset, offset,
per_page, per_page,
has_next, has_next,
len(products),
total_products,
) )
# Prepare supplier info dict: {product.id: 'Supplier (City)'} # Prepare supplier info dict: {product.id: 'Supplier (City)'}
@ -1072,44 +1112,6 @@ class AplicoopWebsiteSale(WebsiteSale):
) )
product_display_info[product.id] = display_info product_display_info[product.id] = display_info
# Calculate available tags with product count (only show tags that are actually used and visible)
# Build a dict: {tag_id: {'id': tag_id, 'name': tag_name, 'count': num_products}}
available_tags_dict = {}
for product in products:
for tag in product.product_tag_ids:
# Only include tags that are visible on ecommerce
is_visible = getattr(
tag, "visible_on_ecommerce", True
) # Default to True if field doesn't exist
if not is_visible:
continue
if tag.id not in available_tags_dict:
tag_color = tag.color if tag.color else None
_logger.info(
"Tag %s (id=%s): color=%s (type=%s)",
tag.name,
tag.id,
tag_color,
type(tag_color),
)
available_tags_dict[tag.id] = {
"id": tag.id,
"name": tag.name,
"color": tag_color, # Use tag color (hex) or None for theme color
"count": 0,
}
available_tags_dict[tag.id]["count"] += 1
# Convert to sorted list of tags (sorted by name for consistent display)
available_tags = sorted(available_tags_dict.values(), key=lambda t: t["name"])
_logger.info(
"eskaera_shop: Found %d available tags for %d products",
len(available_tags),
len(products),
)
_logger.info("eskaera_shop: available_tags = %s", available_tags)
# Manage session for separate cart per order # Manage session for separate cart per order
session_key = f"eskaera_{order_id}" session_key = f"eskaera_{order_id}"
cart = request.session.get(session_key, {}) cart = request.session.get(session_key, {})
@ -1119,10 +1121,10 @@ class AplicoopWebsiteSale(WebsiteSale):
# Filter product tags to only show published ones # Filter product tags to only show published ones
# Create a dictionary with filtered tags for each product # Create a dictionary with filtered tags for each product
filtered_products = {} filtered_products_dict = {}
for product in products: for product in products:
published_tags = self._filter_published_tags(product.product_tag_ids) published_tags = self._filter_published_tags(product.product_tag_ids)
filtered_products[product.id] = { filtered_products_dict[product.id] = {
"product": product, "product": product,
"published_tags": published_tags, "published_tags": published_tags,
} }
@ -1132,7 +1134,7 @@ class AplicoopWebsiteSale(WebsiteSale):
{ {
"group_order": group_order, "group_order": group_order,
"products": products, "products": products,
"filtered_product_tags": filtered_products, "filtered_product_tags": filtered_products_dict,
"cart": cart, "cart": cart,
"available_categories": available_categories, "available_categories": available_categories,
"category_hierarchy": category_hierarchy, "category_hierarchy": category_hierarchy,
@ -1163,9 +1165,10 @@ class AplicoopWebsiteSale(WebsiteSale):
def load_eskaera_page(self, order_id, **post): def load_eskaera_page(self, order_id, **post):
"""Load next page of products for lazy loading. """Load next page of products for lazy loading.
Respects same search/filter parameters as eskaera_shop.
Returns only HTML of product cards without page wrapper. Returns only HTML of product cards without page wrapper.
""" """
group_order = request.env["group.order"].browse(order_id) group_order = request.env["group_order"].browse(order_id)
if not group_order.exists() or group_order.state != "open": if not group_order.exists() or group_order.state != "open":
return "" return ""
@ -1193,16 +1196,95 @@ class AplicoopWebsiteSale(WebsiteSale):
) )
# Get all products (same logic as eskaera_shop) # Get all products (same logic as eskaera_shop)
products = group_order._get_products_for_group_order(group_order.id) all_products = group_order._get_products_for_group_order(group_order.id)
# Get search and filter parameters (passed via POST/GET)
search_query = post.get("search", "").strip()
category_filter = post.get("category", "0")
# ===== Apply SAME filters as eskaera_shop =====
filtered_products = all_products
# Apply search
if search_query:
filtered_products = filtered_products.filtered(
lambda p: search_query.lower() in p.name.lower()
or search_query.lower() in (p.description or "").lower()
)
_logger.info(
'load_eskaera_page: search filter "%s" - found %d of %d',
search_query,
len(filtered_products),
len(all_products),
)
# Apply category filter
if category_filter != "0":
try:
category_id = int(category_filter)
selected_category = request.env["product.category"].browse(category_id)
if selected_category.exists():
all_category_ids = [category_id]
def get_all_children(category):
for child in category.child_id:
all_category_ids.append(child.id)
get_all_children(child)
get_all_children(selected_category)
cat_filtered = request.env["product.product"].search(
[
("categ_id", "in", all_category_ids),
("active", "=", True),
("product_tmpl_id.is_published", "=", True),
("product_tmpl_id.sale_ok", "=", True),
]
)
if group_order.category_ids:
order_cat_ids = []
def get_order_descendants(categories):
for cat in categories:
order_cat_ids.append(cat.id)
if cat.child_id:
get_order_descendants(cat.child_id)
get_order_descendants(group_order.category_ids)
cat_filtered = cat_filtered.filtered(
lambda p: p.categ_id.id in order_cat_ids
)
filtered_products = cat_filtered
_logger.info(
"load_eskaera_page: category filter %d - found %d of %d",
category_id,
len(filtered_products),
len(all_products),
)
except (ValueError, TypeError):
_logger.warning("load_eskaera_page: Invalid category filter")
# ===== Apply pagination to FILTERED results =====
total_products = len(filtered_products)
offset = (page - 1) * per_page
products_page = filtered_products[offset : offset + per_page]
has_next = offset + per_page < total_products
_logger.info(
"load_eskaera_page: page=%d, offset=%d, showing %d of %d filtered",
page,
offset,
len(products_page),
total_products,
)
# Get pricelist # Get pricelist
pricelist = self._resolve_pricelist() pricelist = self._resolve_pricelist()
# Calculate prices only for products on this page # Calculate prices for this page
offset = (page - 1) * per_page
products_page = products[offset : offset + per_page]
has_next = offset + per_page < len(products)
product_price_info = {} product_price_info = {}
for product in products_page: for product in products_page:
product_variant = ( product_variant = (
@ -1261,10 +1343,10 @@ class AplicoopWebsiteSale(WebsiteSale):
product_supplier_info[product.id] = supplier_name product_supplier_info[product.id] = supplier_name
# Filter product tags # Filter product tags
filtered_products = {} filtered_products_dict = {}
for product in products_page: for product in products_page:
published_tags = self._filter_published_tags(product.product_tag_ids) published_tags = self._filter_published_tags(product.product_tag_ids)
filtered_products[product.id] = { filtered_products_dict[product.id] = {
"product": product, "product": product,
"published_tags": published_tags, "published_tags": published_tags,
} }
@ -1286,7 +1368,7 @@ class AplicoopWebsiteSale(WebsiteSale):
{ {
"group_order": group_order, "group_order": group_order,
"products": products_page, "products": products_page,
"filtered_product_tags": filtered_products, "filtered_product_tags": filtered_products_dict,
"product_supplier_info": product_supplier_info, "product_supplier_info": product_supplier_info,
"product_price_info": product_price_info, "product_price_info": product_price_info,
"product_display_info": product_display_info, "product_display_info": product_display_info,
@ -1296,6 +1378,207 @@ class AplicoopWebsiteSale(WebsiteSale):
}, },
) )
@http.route(
["/eskaera/<int:order_id>/load-products-ajax"],
type="json",
auth="user",
website=True,
methods=["POST"],
csrf=False,
)
def load_products_ajax(self, order_id, **post):
"""Load products via AJAX for infinite scroll.
Returns JSON with:
- html: rendered product cards HTML
- has_next: whether there are more products
- next_page: page number to fetch next
- total: total filtered products
"""
group_order = request.env["group.order"].browse(order_id)
if not group_order.exists() or group_order.state != "open":
return {"error": "Order not found or not open", "html": ""}
# Get configuration
per_page = int(
request.env["ir.config_parameter"].get_param(
"website_sale_aplicoop.products_per_page", 20
)
)
# Get page from POST
try:
page = int(post.get("page", 1))
if page < 1:
page = 1
except (ValueError, TypeError):
page = 1
# Get filters
search_query = post.get("search", "").strip()
category_filter = str(post.get("category", "0"))
_logger.info(
"load_products_ajax: order_id=%d, page=%d, search=%s, category=%s",
order_id,
page,
search_query,
category_filter,
)
# Get all products
all_products = group_order._get_products_for_group_order(group_order.id)
filtered_products = all_products
# Apply search
if search_query:
filtered_products = filtered_products.filtered(
lambda p: search_query.lower() in p.name.lower()
or search_query.lower() in (p.description or "").lower()
)
# Apply category filter
if category_filter != "0":
try:
category_id = int(category_filter)
selected_category = request.env["product.category"].browse(category_id)
if selected_category.exists():
all_category_ids = [category_id]
def get_all_children(category):
for child in category.child_id:
all_category_ids.append(child.id)
get_all_children(child)
get_all_children(selected_category)
cat_filtered = request.env["product.product"].search(
[
("categ_id", "in", all_category_ids),
("active", "=", True),
("product_tmpl_id.is_published", "=", True),
("product_tmpl_id.sale_ok", "=", True),
]
)
if group_order.category_ids:
order_cat_ids = []
def get_order_descendants(categories):
for cat in categories:
order_cat_ids.append(cat.id)
if cat.child_id:
get_order_descendants(cat.child_id)
get_order_descendants(group_order.category_ids)
cat_filtered = cat_filtered.filtered(
lambda p: p.categ_id.id in order_cat_ids
)
filtered_products = cat_filtered
except (ValueError, TypeError) as e:
_logger.warning(
"load_products_ajax: Invalid category filter: %s", str(e)
)
# Paginate
total_products = len(filtered_products)
offset = (page - 1) * per_page
products_page = filtered_products[offset : offset + per_page]
has_next = offset + per_page < total_products
# Get prices
pricelist = self._resolve_pricelist()
product_price_info = {}
for product in products_page:
product_variant = (
product.product_variant_ids[0] if product.product_variant_ids else False
)
if product_variant and pricelist:
try:
price_info = product_variant._get_price(
qty=1.0,
pricelist=pricelist,
fposition=request.website.fiscal_position_id,
)
product_price_info[product.id] = {
"price": price_info.get("value", 0.0),
"list_price": price_info.get("original_value", 0.0),
"has_discounted_price": price_info.get("discount", 0.0) > 0,
"discount": price_info.get("discount", 0.0),
"tax_included": price_info.get("tax_included", True),
}
except Exception:
product_price_info[product.id] = {
"price": product.list_price,
"list_price": product.list_price,
"has_discounted_price": False,
"discount": 0.0,
"tax_included": False,
}
else:
product_price_info[product.id] = {
"price": product.list_price,
"list_price": product.list_price,
"has_discounted_price": False,
"discount": 0.0,
"tax_included": False,
}
# Prepare display info
product_display_info = {}
for product in products_page:
display_info = self._prepare_product_display_info(
product, product_price_info
)
product_display_info[product.id] = display_info
# Prepare supplier info
product_supplier_info = {}
for product in products_page:
supplier_name = ""
if product.seller_ids:
partner = product.seller_ids[0].partner_id.sudo()
supplier_name = partner.name or ""
if partner.city:
supplier_name += f" ({partner.city})"
product_supplier_info[product.id] = supplier_name
# Filter tags
filtered_products_dict = {}
for product in products_page:
published_tags = self._filter_published_tags(product.product_tag_ids)
filtered_products_dict[product.id] = {
"product": product,
"published_tags": published_tags,
}
# Render HTML
html = request.env["ir.ui.view"]._render(
"website_sale_aplicoop.eskaera_shop_products",
{
"group_order": group_order,
"products": products_page,
"filtered_product_tags": filtered_products_dict,
"product_supplier_info": product_supplier_info,
"product_price_info": product_price_info,
"product_display_info": product_display_info,
"labels": self.get_checkout_labels(),
"has_next": has_next,
"next_page": page + 1,
},
)
return {
"html": html,
"has_next": has_next,
"next_page": page + 1,
"total": total_products,
"page": page,
}
@http.route( @http.route(
["/eskaera/add-to-cart"], ["/eskaera/add-to-cart"],
type="http", type="http",

View file

@ -0,0 +1,225 @@
/**
* Infinite Scroll Handler for Eskaera Shop
*
* Automatically loads more products as user scrolls down the page.
* Falls back to manual "Load More" button if disabled or on error.
*/
console.log("[INFINITE_SCROLL] Script loaded!");
(function () {
"use strict";
// Also run immediately if DOM is already loaded
var initInfiniteScroll = function () {
console.log("[INFINITE_SCROLL] Initializing infinite scroll...");
var infiniteScroll = {
orderId: null,
searchQuery: "",
category: "0",
perPage: 20,
currentPage: 1,
isLoading: false,
hasMore: true,
config: {},
init: function () {
// Get configuration from page data
var configEl = document.getElementById("eskaera-config");
if (!configEl) {
console.log("[INFINITE_SCROLL] No eskaera-config found, lazy loading disabled");
return;
}
this.orderId = configEl.getAttribute("data-order-id");
this.searchQuery = configEl.getAttribute("data-search") || "";
this.category = configEl.getAttribute("data-category") || "0";
this.perPage = parseInt(configEl.getAttribute("data-per-page")) || 20;
this.currentPage = parseInt(configEl.getAttribute("data-current-page")) || 1;
// Check if there are more products to load
var hasNextBtn = document.getElementById("load-more-btn");
this.hasMore = hasNextBtn && hasNextBtn.offsetParent !== null; // offsetParent === null means hidden
if (!this.hasMore) {
console.log("[INFINITE_SCROLL] No more products to load");
return;
}
console.log("[INFINITE_SCROLL] Initialized with:", {
orderId: this.orderId,
searchQuery: this.searchQuery,
category: this.category,
perPage: this.perPage,
currentPage: this.currentPage,
});
this.attachScrollListener();
// Also keep the button listener as fallback
this.attachFallbackButtonListener();
},
attachScrollListener: function () {
var self = this;
var scrollThreshold = 0.8; // Load when 80% scrolled
window.addEventListener("scroll", function () {
if (self.isLoading || !self.hasMore) {
return;
}
var scrollHeight = document.documentElement.scrollHeight;
var scrollTop = window.scrollY;
var clientHeight = window.innerHeight;
var scrollPercent = (scrollTop + clientHeight) / scrollHeight;
if (scrollPercent >= scrollThreshold) {
console.log(
"[INFINITE_SCROLL] Scroll threshold reached, loading next page"
);
self.loadNextPage();
}
});
console.log(
"[INFINITE_SCROLL] Scroll listener attached (threshold:",
scrollThreshold * 100 + "%)"
);
},
attachFallbackButtonListener: function () {
var self = this;
var btn = document.getElementById("load-more-btn");
if (!btn) {
console.log("[INFINITE_SCROLL] No fallback button found");
return;
}
btn.addEventListener("click", function (e) {
e.preventDefault();
if (!self.isLoading && self.hasMore) {
console.log("[INFINITE_SCROLL] Manual button click, loading next page");
self.loadNextPage();
}
});
console.log("[INFINITE_SCROLL] Fallback button listener attached");
},
loadNextPage: function () {
var self = this;
this.isLoading = true;
this.currentPage += 1;
console.log(
"[INFINITE_SCROLL] Loading page",
this.currentPage,
"for order",
this.orderId
);
// Show spinner
var spinner = document.getElementById("loading-spinner");
if (spinner) {
spinner.classList.remove("d-none");
}
var data = {
page: this.currentPage,
search: this.searchQuery,
category: this.category,
};
fetch("/eskaera/" + this.orderId + "/load-products-ajax", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest",
},
body: JSON.stringify(data),
})
.then(function (response) {
if (!response.ok) {
throw new Error("Network response was not ok: " + response.status);
}
return response.json();
})
.then(function (result) {
if (result.error) {
console.error("[INFINITE_SCROLL] Server error:", result.error);
self.isLoading = false;
self.currentPage -= 1;
return;
}
console.log("[INFINITE_SCROLL] Page loaded successfully", result);
// Insert HTML into grid
var grid = document.getElementById("products-grid");
if (grid && result.html) {
grid.insertAdjacentHTML("beforeend", result.html);
console.log("[INFINITE_SCROLL] Products inserted into grid");
}
// Update has_more flag
self.hasMore = result.has_next || false;
if (!self.hasMore) {
console.log("[INFINITE_SCROLL] No more products available");
}
// Hide spinner
if (spinner) {
spinner.classList.add("d-none");
}
self.isLoading = false;
// Re-attach event listeners for newly added products
if (
window.aplicoopShop &&
typeof window.aplicoopShop._attachEventListeners === "function"
) {
window.aplicoopShop._attachEventListeners();
console.log("[INFINITE_SCROLL] Event listeners re-attached");
}
})
.catch(function (error) {
console.error("[INFINITE_SCROLL] Fetch error:", error);
self.isLoading = false;
self.currentPage -= 1;
// Hide spinner on error
if (spinner) {
spinner.classList.add("d-none");
}
// Show fallback button
var btn = document.getElementById("load-more-btn");
if (btn) {
btn.classList.remove("d-none");
btn.style.display = "";
}
});
},
};
// Initialize infinite scroll
infiniteScroll.init();
// Export to global scope for debugging
window.infiniteScroll = infiniteScroll;
};
// Run on DOMContentLoaded if DOM not yet ready
if (document.readyState === "loading") {
console.log("[INFINITE_SCROLL] DOM not ready, waiting for DOMContentLoaded...");
document.addEventListener("DOMContentLoaded", initInfiniteScroll);
} else {
// DOM is already loaded
console.log("[INFINITE_SCROLL] DOM already loaded, initializing immediately...");
initInfiniteScroll();
}
})();

View file

@ -551,22 +551,43 @@
<t t-call="website_sale_aplicoop.eskaera_shop_products" /> <t t-call="website_sale_aplicoop.eskaera_shop_products" />
</div> </div>
<!-- Load More Button (for lazy loading) --> <!-- Infinite scroll container -->
<t t-if="lazy_loading_enabled and has_next"> <t t-if="lazy_loading_enabled and has_next">
<div class="row mt-4"> <div id="infinite-scroll-container" class="row mt-4">
<div class="col-12 text-center"> <div class="col-12 text-center">
<!-- Spinner (hidden by default) -->
<div id="loading-spinner" class="d-none" style="padding: 20px;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading more products...</p>
</div>
<!-- Fallback Load More button (shown if auto-load fails) -->
<button <button
id="load-more-btn" id="load-more-btn"
class="btn btn-primary btn-lg" class="btn btn-primary btn-lg d-none"
t-attf-data-page="{{ current_page + 1 }}" t-attf-data-page="{{ current_page + 1 }}"
t-attf-data-order-id="{{ group_order.id }}" t-attf-data-order-id="{{ group_order.id }}"
t-attf-data-search="{{ search_query }}"
t-attf-data-category="{{ selected_category }}"
t-attf-data-per-page="{{ per_page }}" t-attf-data-per-page="{{ per_page }}"
aria-label="Load more products" aria-label="Load more products"
style="display: none;"
> >
<i class="fa fa-download me-2" />Load More Products <i class="fa fa-download me-2" />Load More Products
</button> </button>
</div> </div>
</div> </div>
<!-- Data attributes for infinite scroll configuration -->
<div id="eskaera-config"
t-attf-data-order-id="{{ group_order.id }}"
t-attf-data-search="{{ search_query }}"
t-attf-data-category="{{ selected_category }}"
t-attf-data-per-page="{{ per_page }}"
t-attf-data-current-page="{{ current_page }}"
class="d-none">
</div>
</t> </t>
</t> </t>
<t t-else=""> <t t-else="">
@ -646,6 +667,8 @@
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/checkout_labels.js" /> <script type="text/javascript" src="/website_sale_aplicoop/static/src/js/checkout_labels.js" />
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/home_delivery.js" /> <script type="text/javascript" src="/website_sale_aplicoop/static/src/js/home_delivery.js" />
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/realtime_search.js" /> <script type="text/javascript" src="/website_sale_aplicoop/static/src/js/realtime_search.js" />
<!-- Infinite scroll for lazy loading products -->
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/infinite_scroll.js" />
<!-- Initialize tooltips using native title attribute --> <!-- Initialize tooltips using native title attribute -->
<script type="text/javascript"> <script type="text/javascript">