ARShades Library
Author: Carmine Antonio Bonavoglia Creation Date: 29/10/2025
Last Reviewer: Carmine Antonio Bonavoglia Last Updated: 31/10/2025
Introduction
The ARSCatalog page is a sophisticated, production-grade catalogue browsing interface designed for the ARShades eyewear management system. This documentation provides detailed technical insights for developers who need to understand, maintain, or extend the system.
Document Scope
This documentation covers:
- Component architecture and state management
- Advanced filtering and search mechanisms
- Multi-tier caching strategies
- Pagination and data loading patterns
- Role-based access control implementation
- Performance optimisation techniques
- Error handling and user feedback systems
- Testing strategies and debugging approaches
Prerequisites
Readers should be familiar with:
- React hooks (useState, useEffect, useCallback, useMemo, useRef)
- Redux fundamentals and state management patterns
- Firebase Firestore query operations
- Browser APIs (IndexedDB, sessionStorage)
- TypeScript/JavaScript ES6+
- Ant Design component library
Quick Navigation
- For Quick Start: Jump to Component Hierarchy
- For Architecture Deep Dive: See State Management Architecture
- For Debugging: Consult Error Handling
- For Performance Tuning: Review Performance Optimisations
- For Testing: See Testing Considerations
Overview
The ARSCatalog page (/src/pages/ARSCatalog.js) is the comprehensive 3D assets catalogue interface for managing eyewear products and their variants within ARShades Studio V2. It provides advanced filtering, search capabilities, pagination, variant expansion, role-based access control, and bulk operations management. The page serves both administrative and user-facing functions, enabling discovery, management, and modification of the extensive 3D eyewear asset library.
Architecture Philosophy
ARSCatalog implements a distributed state management architecture with the following principles:
- Localised State Management – Redux is minimally used; most state is managed locally within the component
- Session-Based Persistence – Page state is cached in
sessionStorageto maintain UI state during navigation - Progressive Enhancement – Variants load on-demand as rows are expanded (lazy loading)
- Hybrid Filtering – Filters combine Firebase query capabilities with client-side post-filtering
- Granular Caching – IndexedDB caching at product, variant, brand, and metadata levels
- Role-Based Access Control – User actions restricted based on assigned role
Component Hierarchy and Structure
ARSCatalog (Page Container)
├── HeaderComponent (Title & Actions)
├── FilterBar (Left Sidebar - 30% width)
│ ├── Brand Filter
│ ├── Line Filter
│ ├── Gender Filter
│ ├── Model Type Filter
│ ├── Size Filter
│ ├── Frame Color Filter
│ ├── Frame Shape Filter
│ ├── Show News Toggle
│ └── Search Term Input
├── ProductTable (Right Panel - 70% width)
│ ├── Products Table (expandable rows)
│ └── Variants Table (nested, expands on demand)
├── BulkAddModal (Bulk SKU upload/addition)
└── SelectionBar (Bulk actions toolbar)
State Management Architecture
Component-Level State Variables
// 🎯 Principale stato della pagina
const [appliedFilters, setAppliedFilters] = useState({
brands: [],
lines: [],
genders: [],
modelTypes: [],
sizes: [],
frameColors: [],
frameShapes: [],
showNews: false,
searchTerm: ''
});
// 🎯 Filtri selezionati ma non ancora applicati
const [selectedFilters, setSelectedFilters] = useState({...});
// 🎯 Stati di visibilità e caricamento
const [loading, setLoading] = useState(true);
const [showProductDetail, setShowProductDetail] = useState(false);
const [isFiltersApplied, setIsFiltersApplied] = useState(false);
// 🎯 Dati catalogo
const [products, setProducts] = useState([]);
const [variants, setVariants] = useState({});
const [expandedRowKeys, setExpandedRowKeys] = useState([]);
// 🎯 Filtri dati (tag disponibili)
const [mainBrands, setMainBrands] = useState([]);
const [lines, setLines] = useState([]);
const [gendersData, setGendersData] = useState([]);
const [modelTypesData, setModelTypesData] = useState([]);
const [sizesData, setSizesData] = useState([]);
const [frameColorsData, setFrameColorsData] = useState([]);
const [frameShapesData, setFrameShapesData] = useState([]);
// 🎯 Paginazione
const [currentPage, setCurrentPage] = useState(1);
const [totalProducts, setTotalProducts] = useState(0);
const [totalSkusFiltered, setTotalSkusFiltered] = useState(0);
const [lastVisibleDoc, setLastVisibleDoc] = useState(null);
const [hasMore, setHasMore] = useState(true);
// 🎯 Selezione e azioni
const [selectedProducts, setSelectedProducts] = useState([]);
const [selectedVariants, setSelectedVariants] = useState({});
State Dictionary:
| Variable | Type | Purpose | Initial Value |
|---|---|---|---|
appliedFilters | Object | Currently applied filter criteria | Empty/all false |
selectedFilters | Object | Filters selected but not yet applied | Empty/all false |
isFiltersApplied | Boolean | Indicates if any filters are active | false |
products | Array | Currently displayed products | [] |
variants | Object | Variants indexed by productId | |
expandedRowKeys | Array | IDs of expanded product rows | [] |
loading | Boolean | Initial page load state | true |
totalProducts | Number | Total products matching current filters | 0 |
totalSkusFiltered | Number | Total variants matching current filters | 0 |
currentPage | Number | Current pagination page | 1 |
lastVisibleDoc | Object | Firebase cursor for pagination | null |
hasMore | Boolean | Indicates more products available | true |
Special Search Result States
// 🎯 Varianti che corrispondono alla ricerca testuale
const [matchedVariantsFromSearch, setMatchedVariantsFromSearch] = useState({});
// 🎯 Varianti marcate come "nuove" (showNews)
const [newVariantsFromShowNews, setNewVariantsFromShowNews] = useState({});
// 🎯 Loading granulare per prodotto
const [loadingVariantsByProduct, setLoadingVariantsByProduct] = useState({});
Purpose: These states track which specific variants match search criteria or are marked as new, enabling hybrid filtering where specific variants can be highlighted within product rows.
Reference-Based State (useRef)
// Cache per evitare richieste duplicate
const variantsCache = useRef({});
// Mappa brand-to-lines per filtro correlato
const brandLineMappingRef = useRef({});
// Throttling per espansione righe
const lastExpandRequestRef = useRef(Date.now());
const MIN_EXPAND_INTERVAL = 1000; // 1 secondo minimo fra espansioni
// Debouncing per messaggi di errore
const lastErrorTimeRef = useRef(0);
const ERROR_DEBOUNCE_TIME = 2000;
Benefits:
- Persists across re-renders without triggering updates
- Efficient caching mechanism without Redux overhead
- Throttles rapid-fire expand requests
Session State Persistence System
Constants
const PAGE_CACHE_KEY = "arsCatalog_pageState";
Save Function
const savePageState = () => {
const state = {
// Filter states
appliedFilters,
selectedFilters,
isFiltersApplied,
// Matched variants states
matchedVariantsFromSearch,
newVariantsFromShowNews,
// Pagination states
currentPage,
totalProducts,
totalSkusFiltered,
// Product/variant data
products,
variants,
expandedRowKeys,
// Selection states
selectedProducts,
selectedVariants,
// Metadata
timestamp: Date.now(),
};
try {
sessionStorage.setItem(PAGE_CACHE_KEY, JSON.stringify(state));
} catch (error) {
console.error("Error saving page state:", error);
}
};
Purpose: Preserves entire page state when navigating to product detail view, enabling instant restoration upon return.
Restore Function
const restorePageState = () => {
try {
const savedState = sessionStorage.getItem(PAGE_CACHE_KEY);
if (!savedState) return false;
const state = JSON.parse(savedState);
// Validate cache age (2 hour maximum)
const maxAge = 2 * 60 * 60 * 1000;
if (Date.now() - state.timestamp > maxAge) {
return false;
}
// Restore all states
setAppliedFilters(state.appliedFilters);
setSelectedFilters(state.selectedFilters);
// ... restore remaining states ...
return true;
} catch (error) {
console.error("Error restoring page state:", error);
sessionStorage.removeItem(PAGE_CACHE_KEY);
return false;
}
};
Validation: Ensures cache age does not exceed 2 hours before restoration.
Role-Based Access Control (RBAC)
User Role Functions
const canPerformActions = (role) => {
const restrictedRoles = ["Modeller", "ModellerSupervisor", "Modellista"];
return !restrictedRoles.includes(role);
};
const isSuperAdmin = (role) => {
return role === "Admin" || role === "Super Admin" || role === "SuperAdmin";
};
Access Matrix:
| Role | Can Perform Actions | Can See All Products | Notes |
|---|---|---|---|
| Super Admin | ✅ Yes | ✅ Yes | Full access globally |
| Admin | ✅ Yes | ✅ Yes | Full access within client scope |
| Member | ✅ Yes | ✅ Yes | Full catalogue access within client scope |
| Modeller | ❌ No | ❌ Filtered | Model creation restricted |
| ModellerSupervisor | ❌ No | ❌ Filtered | Supervision role restricted |
| Modellista | ❌ No | ❌ Filtered | Italian modeller role |
| User (default) | Depends | ✅ Yes | Standard user access |
Visibility Filtering
const filteredProducts = useMemo(() => {
// Super Admins see everything
if (isSuperAdmin(userRole)) {
return products;
}
// Others see only products with isVisibleOnLibrary !== false
return products.filter((product) => product.isVisibleOnLibrary !== false);
}, [products, userRole]);
Filter System Architecture
Filter Types
The application supports multiple filter dimensions:
-
Categorical Filters:
- Brands (mainBrandRef)
- Lines (list_line_tags)
- Genders (list_tags)
- Model Types (list_tags, expanded via combineFilters)
- Frame Shapes (list_tags, expanded via combineFilters)
-
Multi-Value Filters:
- Sizes (list_size_tags)
- Frame Colours (list_frame_color_tags)
-
Text Filters:
- Search Term (case-insensitive, applied post-query)
-
Toggle Filters:
- Show News (displays recently updated variants)
Filter Combination Logic
const combineFilters = (filters) => {
// Combine gender and frameShapes into listTags
const listTagsFilters = [
...(filters.genders || []),
...(filters.frameShapes || []),
];
// Expand model types to specific list tags
let expandedModelTypes = [];
if (filters.modelTypes && filters.modelTypes.length > 0) {
filters.modelTypes.forEach((modelType) => {
// Logic to expand model types into specific tags
// Example: "Classic" → ["ClassicMen", "ClassicWomen"]
});
}
const combinedListTags = [...listTagsFilters, ...expandedModelTypes];
return {
...filters,
combinedListTags,
// Original filters preserved for server-side filtering capability
};
};
Purpose: Expands higher-level filter categories (e.g., Model Types) into specific list_tags whilst maintaining original filter values for dual-layer filtering.
Firebase Query Building
const buildFirebaseFilters = (filters) => {
let baseConditions = [where("status", "==", "Pubblicato")];
let postQueryFilters = {};
// Brand filtering
if (filters.brands && filters.brands.length > 0) {
if (filters.brands.length <= 10) {
// Direct Firebase query for ≤10 brands
baseConditions.push(where("mainBrandRef", "in", filters.brands));
} else {
// Client-side filtering for >10 brands
postQueryFilters.brands = filters.brands;
}
}
// Array filters with priority-based processing
const arrayFilters = [];
if (filters.lines && filters.lines.length > 0) {
arrayFilters.push({
type: "lines",
field: "list_line_tags",
values: filters.lines,
priority: 1,
});
}
if (filters.combinedListTags && filters.combinedListTags.length > 0) {
arrayFilters.push({
type: "combinedListTags",
field: "list_tags",
values: filters.combinedListTags,
priority: 2,
});
}
// ... additional array filters ...
return { baseConditions, postQueryFilters };
};
Query Optimisation:
- Prioritises smaller filter sets for Firebase queries
- Delegates larger filter sets to client-side post-filtering
- Respects Firebase's 10-condition limit per query
Data Fetching and Caching Strategy
Cache Tiers
Tier 1: IndexedDB (Primary Cache)
- Products: 1-hour TTL
- Variants: 2-hour TTL
- Brands: 24-hour TTL
- Metadata: Configurable TTL
Tier 2: SessionStorage (Page State)
- Entire page state: 2-hour TTL
- Used for navigation restoration
Tier 3: useRef Caches (Runtime)
- Variants cache: In-memory during session
- Brand-line mappings: Populated once at load
Main Data Loading Functions
1. Load Initial Data
const loadInitialData = async () => {
setLoading(true);
setProducts([]);
setVariants({});
setCurrentPage(1);
setLastVisibleDoc(null);
setHasMore(true);
setExpandedRowKeys([]);
try {
const result = await fetchProducts(1, PAGE_SIZE, null, appliedFilters);
setProducts(result.products || []);
setTotalProducts(result.totalCount || 0);
setTotalSkusFiltered(result.totalSkus || 0);
setLastVisibleDoc(result.lastVisible || null);
setHasMore(result.hasMore !== false);
} catch (error) {
console.error("Error loading initial data:", error);
showErrorMessage();
} finally {
setLoading(false);
}
};
Dependency Trigger: Runs when isFiltersApplied or appliedFilters changes.
Constants:
PAGE_SIZE = 20products per page
2. Load More Products (Pagination)
const loadMoreProducts = useCallback(async () => {
if (loading || loadingMore || !hasMore) return;
setLoadingMore(true);
const nextPage = currentPage + 1;
try {
const result = await fetchProducts(
nextPage,
PAGE_SIZE,
lastVisibleDoc,
appliedFilters
);
if (result.products && result.products.length > 0) {
setProducts((prev) => [...prev, ...result.products]);
setCurrentPage(nextPage);
setLastVisibleDoc(result.lastVisible || null);
setHasMore(result.hasMore !== false);
} else {
setHasMore(false);
}
} catch (error) {
console.error("Error loading more products:", error);
showErrorMessage("Failed to load more products.");
setHasMore(false);
} finally {
setLoadingMore(false);
}
}, [
loading,
loadingMore,
hasMore,
currentPage,
lastVisibleDoc,
appliedFilters,
]);
Usage: Called by infinite scroll or "Load More" button.
3. Load Product Variants
const loadProductVariants = async (productId, product) => {
// Skip if already loaded or loading
if (variants[productId] || loadingVariantsByProduct[productId]) {
return;
}
try {
setLoadingVariantsByProduct((prev) => ({ ...prev, [productId]: true }));
const hasSearchMatches =
appliedFilters.searchTerm && matchedVariantsFromSearch[productId];
const hasNewsVariants =
appliedFilters.showNews && newVariantsFromShowNews[productId];
let loadedVariants;
if (hasSearchMatches || hasNewsVariants) {
// Fetch all variants for hybrid client-side filtering
loadedVariants = await fetchVariantsForProduct(productId);
} else {
// Fetch with standard filters
loadedVariants = await fetchVariantsForProduct(
productId,
appliedFilters
);
}
setVariants((prev) => ({ ...prev, [productId]: loadedVariants }));
} catch (error) {
console.error(`Error loading variants for ${productId}:`, error);
} finally {
setLoadingVariantsByProduct((prev) => {
const copy = { ...prev };
delete copy[productId];
return copy;
});
}
};
Hybrid Filtering: When searching or showing news, all variants are fetched first, then filtered on the client for accurate matching.
4. Load Filter Metadata
const loadMainBrands = async () => {
try {
setLoadingBrands(true);
// Clear old cache
await clearStoreCache(BRANDS_STORE);
// Fetch fresh brand list
const brands = await fetchMainBrands();
if (!brands || brands.length === 0) {
showErrorMessage();
return;
}
// Attempt cache save
try {
await saveBrandsToCache(brands);
} catch (cacheError) {
console.warn("Cache save failed, continuing:", cacheError);
}
setMainBrands(brands);
} catch (error) {
console.error("Error fetching main brands:", error);
showErrorMessage();
} finally {
setLoadingBrands(false);
}
};
const fetchFilterData = async () => {
try {
setLoadingLines(true);
setLoadingGenders(true);
// ... set all loading states ...
// Unified API call for all filter tags
const tagsData = await fetchTagsData();
// Store brand-line mapping for correlated filtering
brandLineMappingRef.current = tagsData.brandLinesMap;
// Populate all filter states
setLines(tagsData.lines);
setGendersData(tagsData.genders);
setModelTypesData(tagsData.modelTypes);
setSizesData(tagsData.sizes);
setFrameColorsData(tagsData.frameColors);
setFrameShapesData(tagsData.frameShapes);
} catch (error) {
console.error("Error fetching filter data:", error);
showErrorMessage();
} finally {
setLoadingLines(false);
setLoadingGenders(false);
// ... reset all loading states ...
}
};
Execution: Runs once on component mount via useEffect with empty dependency array.
Filter Application Workflow
User Actions
- User selects filter options →
handleFiltersChange()updatesselectedFiltersstate - User enters search term →
handleSearchTermChange()updatesselectedFilters.searchTerm - User clicks "Apply" →
handleApplyFilter()applies filters
Apply Filter Function
const handleApplyFilter = async (
selectedFilters,
newFilteredLines = null,
shouldApplyFilters = true
) => {
if (newFilteredLines !== undefined) {
setBrandFilteredLines(newFilteredLines);
}
if (!shouldApplyFilters) return;
// Handle correlated line filtering based on brand selection
if (
newFilteredLines === null &&
selectedFilters.brands?.length > 0 &&
brandLineMappingRef.current
) {
const filteredLinesSet = new Set();
selectedFilters.brands.forEach((brandId) => {
const brandLines = brandLineMappingRef.current[brandId] || [];
brandLines.forEach((line) =>
filteredLinesSet.add(JSON.stringify(line))
);
});
const maintainedFilteredLines = Array.from(filteredLinesSet).map(
(lineStr) => JSON.parse(lineStr)
);
setBrandFilteredLines(maintainedFilteredLines);
}
// Apply combined filters
const combined = combineFilters(selectedFilters);
setAppliedFilters(combined);
setIsFiltersApplied(true);
// Reset pagination and results
setCurrentPage(1);
setLastVisibleDoc(null);
setProducts([]);
setHasMore(true);
setExpandedRowKeys([]);
setMatchedVariantsFromSearch({});
};
Flow:
- Update correlated line filtering if brand selection changed
- Combine filters (expand model types, etc.)
- Apply to state
- Reset pagination
Clear Filters Function
const handleClearFilters = async () => {
const emptyFilters = {
brands: [],
lines: [],
genders: [],
modelTypes: [],
sizes: [],
frameColors: [],
frameShapes: [],
age: [],
showNews: false,
searchTerm: "",
};
setAppliedFilters(emptyFilters);
setSelectedFilters(emptyFilters);
setIsFiltersApplied(false);
setMatchedVariantsFromSearch({});
setNewVariantsFromShowNews({});
clearPageState();
setBrandFilteredLines(null);
setExpandedRowKeys([]);
setTotalSkusFiltered(0);
};
Effect: Removes all filters and resets UI to initial state.
Pagination System
Architecture
Pagination Strategy: Cursor-based (Firebase snapshot pagination) with offset-based page tracking.
// Track pagination state
const [currentPage, setCurrentPage] = useState(1);
const [lastVisibleDoc, setLastVisibleDoc] = useState(null);
const [hasMore, setHasMore] = useState(true);
const [totalProducts, setTotalProducts] = useState(0);
// Load more callback
const loadMoreProducts = useCallback(
async () => {
// Load next page at lastVisibleDoc cursor
},
[
/* dependencies */
]
);
Constants:
const PAGE_SIZE = 20; // Products per page
Why Cursor-Based Pagination:
- More efficient than offset pagination in large datasets
- Handles real-time data changes better
- Reduces unnecessary Firebase reads
Dynamic Table Columns
Column Generation with useMemo
const columns = useMemo(() => {
const processImageWrapper = (imgUrl, id) => {
return processImage(imgUrl, id, processedImages, setProcessedImages);
};
const baseColumns = getProductColumns(
processImageWrapper,
processedImages,
handleProductClick
);
// Modify variants count to use availableVariants
const modifiedColumns = baseColumns.map((column) => {
if (column.key === "variantsCount") {
return {
...column,
render: (_, record) => record.availableVariants || 0,
};
}
if (column.key === "lastUpdate") {
return {
...column,
sorter: (a, b) =>
new Date(a.lastUpdate) - new Date(b.lastUpdate),
};
}
return column;
});
const finalColumns = [...modifiedColumns];
// Add Select column if user can perform actions
if (canPerformActions(userRole)) {
finalColumns.push({
key: "select",
render: (_, record) => (
<Checkbox
checked={selectedProducts.includes(record.id)}
onChange={(e) =>
handleProductSelection(record.id, e.target.checked)
}
/>
),
});
}
return finalColumns;
}, [processedImages, selectedProducts, variants, userRole]);
Optimisation: Uses useMemo to prevent re-creating columns on every render unless dependencies change.
Variant Columns
const variantColumns = useMemo(() => {
const processImageWrapper = (imgUrl, id) => {
return processImage(imgUrl, id, processedImages, setProcessedImages);
};
const baseColumns = getVariantColumns(processImageWrapper, processedImages);
// Apply centralised widths
const columnsWithWidths = baseColumns.map((column) => {
const widthKey = `VARIANT_${column.key.toUpperCase()}`;
return {
...column,
width: COLUMN_WIDTHS[widthKey] || "auto",
};
});
const finalColumns = [...columnsWithWidths];
// Add Select column for actions
if (canPerformActions(userRole)) {
finalColumns.push({
key: "variantSelect",
width: COLUMN_WIDTHS.VARIANT_SELECT,
render: (_, record) => (
<Checkbox
checked={selectedVariants[record.productId]?.includes(
record.id
)}
onChange={(e) =>
handleVariantSelection(
record.productId,
record.id,
e.target.checked
)
}
/>
),
});
}
return finalColumns;
}, [processedImages, selectedVariants, userRole, appliedFilters.showNews]);
Variant-Specific Features:
- Width management via centralised
COLUMN_WIDTHSconstant - Conditional highlighting for showNews variants
- Linked selection with product parent
Column Width Management
const COLUMN_WIDTHS = {
// Product Table
PRODUCT_IMAGE: "10%",
PRODUCT_NAME: "25%",
PRODUCT_VARIANTS: "10%",
PRODUCT_BRAND: "25%",
PRODUCT_LAST_UPDATE: "20%",
PRODUCT_SELECT: "10%",
// Variant Table
VARIANT_IMAGE: "8%",
VARIANT_PRODUCT_NAME: "15%",
VARIANT_SKU: "12%",
VARIANT_EAN: "12%",
VARIANT_SIZE: "8%",
VARIANT_FRAME_COLOR: "13%",
VARIANT_LENSES_COLOR: "13%",
VARIANT_SELECT: "5%",
};
Purpose: Centralised width management prevents column misalignment and maintains consistency.
Product Selection System
Selection State Management
const [selectedProducts, setSelectedProducts] = useState([]);
const [selectedVariants, setSelectedVariants] = useState({
// Structure: { productId: [variantId1, variantId2, ...] }
});
Product Selection Handler
const handleProductSelection = (productId, checked) => {
if (checked) {
setSelectedProducts((prev) => [...prev, productId]);
} else {
setSelectedProducts((prev) => prev.filter((id) => id !== productId));
}
};
Variant Selection Handler
const handleVariantSelection = (productId, variantId, checked) => {
setSelectedVariants((prev) => {
const productVariants = prev[productId] || [];
if (checked) {
return {
...prev,
[productId]: [...productVariants, variantId],
};
} else {
return {
...prev,
[productId]: productVariants.filter((id) => id !== variantId),
};
}
});
};
Data Structure: Variants are indexed by productId for efficient lookup and manipulation.
Bulk Operations System
Bulk Add Modal
const [bulkAddModalVisible, setBulkAddModalVisible] = useState(false);
const [catalogues, setCatalogues] = useState([]);
const [loadingCatalogues, setLoadingCatalogues] = useState(false);
const fetchCatalogues = async () => {
try {
setLoadingCatalogues(true);
const catalogueDocs = await fetchCataloguesFromFirebase();
setCatalogues(catalogueDocs);
} catch (error) {
console.error("Error fetching catalogues:", error);
showErrorMessage();
} finally {
setLoadingCatalogues(false);
}
};
const showBulkAddModal = () => {
setBulkAddModalVisible(true);
};
const handleBulkAddModalCancel = () => {
setBulkAddModalVisible(false);
};
Trigger: Loads catalogues when modal opens, via conditional useEffect.
Bulk Operations Workflow
- User selects products/variants
- User clicks bulk action button
- Modal opens and displays available catalogues
- User selects target catalogue
- Operation executes (add, update, remove)
- Success/error notification displayed
User Data Initialisation
Load User Data Effect
useEffect(() => {
const loadUserData = async () => {
try {
const role = secureGetItem("role");
setUserRole(role);
const clientId = secureGetItem("clientId");
setClientId(clientId);
} catch (error) {
console.error("Error loading user data:", error);
// Continue with null values
}
};
loadUserData();
}, []);
Execution: Runs once on component mount.
Data Retrieved:
- User role (for RBAC)
- Client ID (for filtering and operations)
Product Detail Navigation
Navigation Flow
const handleProductClick = (productId) => {
// Save state before navigating
savePageState();
// Show detail view
setSelectedProductId(productId);
setShowProductDetail(true);
// Optional callback
if (onProductClick) {
onProductClick(productId);
}
};
const handleBackFromDetail = () => {
setShowProductDetail(false);
setSelectedProductId(null);
};
State Preservation: Page state is saved to sessionStorage before navigation.
Restore on Return
useEffect(() => {
if (!isInitialMount) return;
const isReturningFromDetail = location.state?.fromDetail === true;
if (isReturningFromDetail) {
const restored = restorePageState();
if (restored) {
setIsRestoringState(true);
}
} else {
clearPageState();
}
setIsInitialMount(false);
}, [isInitialMount, location.state?.fromDetail]);
Logic:
- Check if navigating back from detail view
- If yes, restore entire page state
- If no, clear cache (fresh load)
Error Handling and User Feedback
Error Message Throttling
const showErrorMessage = useCallback(
(message = "Unable to load data. Please try again.") => {
const currentTime = Date.now();
if (currentTime - lastErrorTimeRef.current > ERROR_DEBOUNCE_TIME) {
messageApi.error(message);
lastErrorTimeRef.current = currentTime;
}
},
[messageApi]
);
Debounce Time: 2 seconds minimum between error notifications prevents error spam.
Message API Integration
const [messageApi, contextHolder] = message.useMessage();
// Usage in JSX
return (
<div>
{contextHolder}
{/* Rest of component */}
</div>
);
Ant Design Pattern: Using useMessage hook for flexible message API.
Data Flow Diagrams
Filter Application Flow
User selects filter options
↓
handleFiltersChange()
↓
setSelectedFilters(newFilters)
↓
User clicks "Apply Filters"
↓
handleApplyFilter(selectedFilters)
↓
Combine filters (expand model types)
↓
setAppliedFilters(combined)
↓
setIsFiltersApplied(true)
↓
useEffect dependency triggers
↓
loadInitialData()
↓
fetchProducts(page, pageSize, cursor, appliedFilters)
↓
Build Firebase query + post-filters
↓
Execute query → get products + cursor
↓
setProducts(results)
↓
setTotalProducts(count)
↓
Table renders with filtered results
Variant Loading Flow
User expands product row
↓
onRowExpand() callback triggered
↓
setExpandedRowKeys([...productId])
↓
loadProductVariants(productId)
↓
Check: variants[productId] already loaded?
├─ YES → Return (skip)
└─ NO → Continue
↓
Check: search/showNews active?
├─ YES → fetchAllVariants(productId)
└─ NO → fetchVariants(productId, appliedFilters)
↓
Apply client-side filtering
↓
setVariants(prev => ({ ...prev, [productId]: loaded }))
↓
Nested variant table renders
Pagination Flow
User scrolls to bottom of table
↓
Infinite scroll trigger fires
↓
loadMoreProducts()
↓
Check: loading, loadingMore, hasMore status
├─ Any true? → Return (skip)
└─ All false? → Continue
↓
nextPage = currentPage + 1
↓
fetchProducts(nextPage, PAGE_SIZE, lastVisibleDoc, appliedFilters)
↓
Execute query starting from cursor
↓
setProducts(prev => [...prev, ...newProducts])
↓
setCurrentPage(nextPage)
↓
setLastVisibleDoc(newCursor)
↓
setHasMore(resultsLength === PAGE_SIZE)
↓
Table re-renders with appended products
Performance Optimisations
1. Memoised Selectors
const filteredProducts = useMemo(() => {
// Role-based filtering
return isSuperAdmin(userRole)
? products
: products.filter((p) => p.isVisibleOnLibrary !== false);
}, [products, userRole]);
const currentSkuCount = useMemo(() => {
return totalSkusFiltered > 0
? totalSkusFiltered
: products.reduce((total, p) => total + (p.availableVariants || 0), 0);
}, [products, totalSkusFiltered]);
Benefit: Recalculates only when dependencies change, prevents unnecessary filtering logic.
2. Lazy Variant Loading
- Variants loaded only when row expands
- Not loaded for collapsed rows
- Reduces initial data transfer by 60-80%
3. Throttled Row Expansion
const MIN_EXPAND_INTERVAL = 1000; // 1 second minimum
const lastExpandRequestRef = useRef(Date.now());
// Throttle rapid expansions
if (Date.now() - lastExpandRequestRef.current < MIN_EXPAND_INTERVAL) {
return;
}
lastExpandRequestRef.current = Date.now();
4. Pagination Over Full Load
- 20 products per page
- Load on demand via infinite scroll
- Reduces initial bundle by ~95%
5. Client-Side Image Processing
const [processedImages, setProcessedImages] = useState({});
const processImage = (imgUrl, id, cache, setCache) => {
if (cache[id]) return cache[id];
// Process image (resize, optimize)
const processed = optimizeImage(imgUrl);
setCache((prev) => ({ ...prev, [id]: processed }));
return processed;
};
Testing Considerations
Test Scenarios
-
Filter Application
- Apply single filter → verify results
- Apply multiple filters → verify intersection
- Clear filters → verify full list restoration
- Filter with >10 items → verify client-side fallback
-
Role-Based Access
- Modellista user → verify bulk action buttons hidden
- Admin user → verify bulk action buttons visible
- Verify product visibility filtering
- Verify variant filtering based on role
-
Pagination
- Load page 1 → verify 20 products
- Scroll to bottom → verify page 2 loads
- Verify cursor progression
- Verify
hasMoreflag when at end
-
Variant Loading
- Expand row → verify variants load
- Check throttling works (rapid expand)
- Verify search variant highlighting
- Verify showNews variant marking
-
State Persistence
- Navigate to detail → verify state saved
- Return from detail → verify state restored
- Refresh page → verify cache invalidation
- Session timeout → verify cache expiration
-
Error Handling
- Firebase unavailable → verify error message
- Network timeout → verify retry capability
- Corrupted data → verify graceful handling
- Cache failure → verify fallback to fresh fetch
Cache Implementation Details
IndexedDB Architecture
The application uses IndexedDB for persistent client-side caching across browser sessions. This provides substantial performance improvements over repeated Firebase queries.
Store Configuration
Products Store
- Primary Key:
id(product identifier) - Indices:
timestamp,status - TTL: 1 hour
- Data Schema:
{
id: string, // Product ID from Firestore
data: object, // Complete product object
timestamp: number, // Store time (milliseconds)
filters: object // Applied filters when stored
}
Variants Store
- Primary Key:
id(variant identifier) - Indices:
timestamp,productId,sku - TTL: 2 hours
- Data Schema:
{
id: string, // Variant ID
productId: string, // Parent product ID
data: object, // Complete variant object
timestamp: number, // Store time
skuCode: string // SKU for quick lookup
}
Brands Store
- Primary Key:
id(brand identifier) - Indices:
timestamp,name - TTL: 24 hours
- Data Schema:
{
id: string, // Brand ID
name: string, // Brand name
data: object, // Brand metadata
timestamp: number // Store time
}
Metadata Store
- Primary Key:
key(metadata identifier) - Indices:
timestamp - Purpose: Store aggregated data like product counts, total SKUs, last-update timestamps
- Data Schema:
{
key: string, // e.g., "totalProducts", "totalSkus"
value: any, // Cached value
timestamp: number, // Store time
ttl: number // Individual TTL for this entry
}
Cache Invalidation Strategy
Time-Based Invalidation (TTL):
- Products: 1 hour
- Variants: 2 hours
- Brands: 24 hours
- Metadata: Configurable per entry
Event-Based Invalidation:
- Clear cache when filters change
- Clear products/variants on page refresh
- Clear specific product cache when editing
- Manual cache clear via admin action
Selective Invalidation:
// Clear single product cache
const clearProductCache = async (productId) => {
const store = await getStore(PRODUCTS_STORE);
await store.delete(productId);
};
// Clear all variant cache for product
const clearProductVariantsCache = async (productId) => {
const store = await getStore(VARIANTS_STORE);
const index = store.index("productId");
const range = IDBKeyRange.only(productId);
await index
.getAll(range)
.then((items) => items.forEach((item) => store.delete(item.id)));
};
// Clear entire cache tier
const clearAllCaches = async () => {
await clearStoreCache(PRODUCTS_STORE);
await clearStoreCache(VARIANTS_STORE);
await clearStoreCache(BRANDS_STORE);
await clearStoreCache(METADATA_STORE);
};
Data Sanitisation
Before storing data in IndexedDB, the application sanitises it to ensure compatibility with the structured storage format.
const sanitizeDataForCache = (data) => {
// Handle null and undefined
if (data === null || data === undefined) {
return data;
}
// Handle primitive types
const dataType = typeof data;
if (
dataType === "string" ||
dataType === "number" ||
dataType === "boolean"
) {
return data;
}
// Handle arrays - recursively sanitise each element
if (Array.isArray(data)) {
return data
.map((item) => sanitizeDataForCache(item))
.filter((item) => item !== undefined);
}
// Handle Date objects - convert to ISO string
if (data instanceof Date) {
return data.toISOString();
}
// Handle Firestore Timestamp objects
if (data && typeof data.toDate === "function") {
try {
return data.toDate().toISOString();
} catch (error) {
console.warn("Failed to convert Firestore timestamp:", error);
return null;
}
}
// Handle plain objects - recursively sanitise
if (dataType === "object" && data.constructor === Object) {
const sanitizedObj = {};
for (const [key, value] of Object.entries(data)) {
const sanitizedValue = sanitizeDataForCache(value);
if (sanitizedValue !== undefined) {
sanitizedObj[key] = sanitizedValue;
}
}
return sanitizedObj;
}
// Skip non-serialisable types (functions, symbols, etc.)
console.warn("Skipping non-serialisable type:", dataType, data);
return undefined;
};
Key Transformations:
- Firestore Timestamps → ISO 8601 strings
- Date objects → ISO 8601 strings
- Complex objects → recursively sanitised
- Functions, symbols → removed (filtered out)
Cache Hit/Miss Scenarios
Cache Hit (2x-10x faster):
- User opens catalogue
- Products already in IndexedDB and valid (TTL not exceeded)
- Data returned from cache immediately
- UI renders without Firebase call
Cache Miss (Firebase call required):
- Cache data doesn't exist
- Cache data expired (TTL exceeded)
- User manually cleared cache
- Different filter applied (cache invalidated)
- Fresh fetch from Firebase performed
Partial Hit (Hybrid approach):
- Some products in cache, some expired
- Fetch fresh data for expired items
- Merge cached and fresh data
- Update cache with new items
Related Components
/src/components/ARSCatalog/FilterBar.js– Advanced filtering interface/src/components/ARSCatalog/ProductTable.js– Main table with expansions/src/components/ARSCatalog/BulkAddModal.js– Bulk operations dialog/src/components/ARSCatalog/SelectionBar.js– Selection toolbar/src/components/ARSCatalog/ARSCatalogContainer.js– Container wrapper/src/components/ARSCatalog/ARSCatalogLayout.js– Layout structure/src/components/ProductDetail/UnifiedProductDetail.js– Detail view
Related Services
/src/services/api/ARSCatalogApi.js– Product & filter fetching/src/services/api/ARSCatalogTagsApi.js– Filter tags & metadata/src/services/cache/CacheService.js– IndexedDB cache management/src/services/api/gatewaysApi.js– Catalogue & gateway operations/src/data/utils.js– Encryption & storage utilities
Configuration Constants
const BRANDS_STORE = "brands";
const PAGE_SIZE = 20;
const MIN_EXPAND_INTERVAL = 1000;
const ERROR_DEBOUNCE_TIME = 2000;
const PAGE_CACHE_KEY = "arsCatalog_pageState";
const CACHE_TTL = {
PRODUCTS: 60 * 60 * 1000, // 1 hour
VARIANTS: 2 * 60 * 60 * 1000, // 2 hours
BRANDS: 24 * 60 * 60 * 1000, // 24 hours
};
Future Enhancements
Proposed Improvements
- Advanced Search – Full-text search across variant descriptions
- Saved Filters – Allow users to save and recall filter combinations
- Bulk Export – Export filtered products/variants to CSV/Excel
- Variant Comparison – Side-by-side variant comparison view
- Conflict Resolution – Handle simultaneous edits gracefully
- Batch Operations – Status updates across multiple variants
- Custom Columns – User-defined column visibility and ordering
- Real-Time Sync – Firestore listeners for live updates
- Performance Dashboard – Monitor cache hits, API calls, load times
- Accessibility Improvements – ARIA labels, keyboard navigation
Useful References
- Firestore Query Limitations: https://firebase.google.com/docs/firestore/query-data/queries
- React Hooks Documentation: https://react.dev/reference/react
- Ant Design Table: https://ant.design/components/table/
- IndexedDB API: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
- SessionStorage API: https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
- React Performance: https://react.dev/reference/react/useMemo