Skip to main content
Version: 1.0.0

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


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 sessionStorage to 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:

VariableTypePurposeInitial Value
appliedFiltersObjectCurrently applied filter criteriaEmpty/all false
selectedFiltersObjectFilters selected but not yet appliedEmpty/all false
isFiltersAppliedBooleanIndicates if any filters are activefalse
productsArrayCurrently displayed products[]
variantsObjectVariants indexed by productId
expandedRowKeysArrayIDs of expanded product rows[]
loadingBooleanInitial page load statetrue
totalProductsNumberTotal products matching current filters0
totalSkusFilteredNumberTotal variants matching current filters0
currentPageNumberCurrent pagination page1
lastVisibleDocObjectFirebase cursor for paginationnull
hasMoreBooleanIndicates more products availabletrue

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:

RoleCan Perform ActionsCan See All ProductsNotes
Super Admin✅ Yes✅ YesFull access globally
Admin✅ Yes✅ YesFull access within client scope
Member✅ Yes✅ YesFull catalogue access within client scope
Modeller❌ No❌ FilteredModel creation restricted
ModellerSupervisor❌ No❌ FilteredSupervision role restricted
Modellista❌ No❌ FilteredItalian modeller role
User (default)Depends✅ YesStandard 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:

  1. 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)
  2. Multi-Value Filters:

    • Sizes (list_size_tags)
    • Frame Colours (list_frame_color_tags)
  3. Text Filters:

    • Search Term (case-insensitive, applied post-query)
  4. 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 = 20 products 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

  1. User selects filter optionshandleFiltersChange() updates selectedFilters state
  2. User enters search termhandleSearchTermChange() updates selectedFilters.searchTerm
  3. 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:

  1. Update correlated line filtering if brand selection changed
  2. Combine filters (expand model types, etc.)
  3. Apply to state
  4. 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_WIDTHS constant
  • 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

  1. User selects products/variants
  2. User clicks bulk action button
  3. Modal opens and displays available catalogues
  4. User selects target catalogue
  5. Operation executes (add, update, remove)
  6. 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

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:

  1. Check if navigating back from detail view
  2. If yes, restore entire page state
  3. 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

  1. 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
  2. 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
  3. Pagination

    • Load page 1 → verify 20 products
    • Scroll to bottom → verify page 2 loads
    • Verify cursor progression
    • Verify hasMore flag when at end
  4. Variant Loading

    • Expand row → verify variants load
    • Check throttling works (rapid expand)
    • Verify search variant highlighting
    • Verify showNews variant marking
  5. State Persistence

    • Navigate to detail → verify state saved
    • Return from detail → verify state restored
    • Refresh page → verify cache invalidation
    • Session timeout → verify cache expiration
  6. 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):

  1. User opens catalogue
  2. Products already in IndexedDB and valid (TTL not exceeded)
  3. Data returned from cache immediately
  4. UI renders without Firebase call

Cache Miss (Firebase call required):

  1. Cache data doesn't exist
  2. Cache data expired (TTL exceeded)
  3. User manually cleared cache
  4. Different filter applied (cache invalidated)
  5. Fresh fetch from Firebase performed

Partial Hit (Hybrid approach):

  1. Some products in cache, some expired
  2. Fetch fresh data for expired items
  3. Merge cached and fresh data
  4. Update cache with new items
  • /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
  • /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

  1. Advanced Search – Full-text search across variant descriptions
  2. Saved Filters – Allow users to save and recall filter combinations
  3. Bulk Export – Export filtered products/variants to CSV/Excel
  4. Variant Comparison – Side-by-side variant comparison view
  5. Conflict Resolution – Handle simultaneous edits gracefully
  6. Batch Operations – Status updates across multiple variants
  7. Custom Columns – User-defined column visibility and ordering
  8. Real-Time Sync – Firestore listeners for live updates
  9. Performance Dashboard – Monitor cache hits, API calls, load times
  10. Accessibility Improvements – ARIA labels, keyboard navigation

Useful References