Skip to main content
Version: 1.0.0

My Catalogues

Author: Carmine Antonio Bonavoglia Creation Date: 29/10/2025
Last Reviewer: Carmine Antonio Bonavoglia Last Updated: 31/10/2025

Introduction

The MyCatalogues page is a production-grade catalogue management and administration interface designed for catalogue owners and managers to create, organize, edit, and publish eyewear product catalogues within ARShades Studio V2. 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 hierarchy
  • Complete state management model (local state, refs, effects)
  • Advanced filtering and search mechanisms for products within catalogues
  • Multi-tier caching strategies (IndexedDB, sessionStorage, runtime)
  • Pagination and lazy variant loading patterns
  • Role-based access control implementation
  • Bulk operations workflows (import, batch add/remove)
  • Draft and publish state management
  • Comprehensive integration with UnifiedProductDetail shared component
  • Error handling and user feedback systems
  • Testing strategies and performance considerations

Prerequisites

Readers should be familiar with:

  • React hooks (useState, useEffect, useCallback, useMemo, useRef)
  • Redux fundamentals and state management patterns
  • Firebase Firestore query operations and real-time listeners
  • Browser APIs (IndexedDB, sessionStorage)
  • TypeScript/JavaScript ES6+
  • Ant Design component library
  • CSV parsing and validation

Key Differences from ARSCatalog

AspectARSCatalogMyCatalogues
PurposeBrowse & filter all productsCreate & manage own catalogues
Write OperationsLimited (bulk SKU adds)Extensive (CRUD + publish)
State ScopeAll productsScoped to selected catalogue
PublishingN/ACore workflow
UnifiedProductDetailRead-only viewEdit mode (role-gated)
Cache StrategyLong TTL (safety-focused)Short TTL (consistency-focused)

Quick Navigation


Overview

The MyCatalogues page (/src/pages/MyCatalogues.js) is the core administration hub for catalogue lifecycle management. It provides:

  • Catalogue Creation & Management — Create new catalogues, edit metadata, manage visibility and publishing
  • Product Administration — Add/remove products, manage variants, handle images and 3D models
  • Draft & Publish Workflow — Preview changes before publishing, version control, audit trails
  • Bulk Operations — Import products via CSV/SKU lists, batch actions, bulk assignments
  • Product Detail Editing — Full-featured product editor via shared UnifiedProductDetail component
  • Role-Based Access Control — Granular permissions for edit, publish, import operations
  • Real-Time Feedback — Progress indicators, validation messages, error recovery

Architecture Philosophy

MyCatalogues implements a write-optimised distributed state management architecture with these principles:

  • Localised State — Redux minimal; most state managed locally within component
  • Scoped State — State restricted to selected catalogue (reduces complexity)
  • Write Consistency — Cache invalidation prioritised over performance on writes
  • Optimistic Updates — UI updates before server confirmation where safe (e.g., bulk operations)
  • Session Persistence — Page state cached in sessionStorage for navigation restoration
  • Lazy Variant Loading — Variants loaded on-demand as rows expand
  • Hybrid Filtering — Combines server-side queries with client-side post-processing
  • Role-Gated UI — Critical operations (publish, edit) gated by user role

Component Hierarchy and Structure

MyCatalogues (Page Container)
├── CatalogueHeader (Title, Actions)
│ ├── Create Catalogue Button
│ ├── Import CSV Button
│ └── Export Button
├── CatalogueSelector (Left Sidebar - 25% width)
│ ├── CatalogueListFilters (by status, owner, visibility)
│ ├── CataloguesList (scrollable catalogue items)
│ │ └── CatalogueItem (selectable, status badge)
│ └── CreateCatalogueModal
├── CatalogueDetailPanel (Right Panel - 75% width)
│ ├── CatalogueToolbar (Publish, Unpublish, Preview, Settings)
│ ├── ProductsTable (expandable rows for products)
│ │ ├── ProductRow
│ │ │ ├── Product Image
│ │ │ ├── Product Info (name, SKU, variant count)
│ │ │ ├── LastUpdate timestamp
│ │ │ └── Expand Icon
│ │ │ └── VariantRows (nested, expands on demand)
│ │ │ ├── Variant Image
│ │ │ ├── Variant Specs (size, color, etc.)
│ │ │ ├── SKU / EAN
│ │ │ └── Actions (edit, delete)
│ │ └── SelectionCheckboxes (for bulk operations)
│ ├── SelectionBar (bulk actions toolbar)
│ │ ├── Bulk Remove
│ │ ├── Move to Catalogue
│ │ ├── Set Visibility
│ │ └── Export Selected
│ └── PaginationControls
├── UnifiedProductDetail (modal overlay)
│ └── Full product editor in edit mode
├── BulkAddModal (CSV/SKU import)
│ ├── File upload
│ ├── Validation results
│ ├── Conflict resolution
│ └── Confirm import
├── PublishPreviewModal (preview before publish)
│ ├── Change summary
│ ├── Affected products list
│ └── Confirm publish
└── ErrorBoundary (global error handling)

State Management Model

Component-Level State Variables

// Catalogue selection & loading
const [catalogues, setCatalogues] = useState([]);
const [selectedCatalogueId, setSelectedCatalogueId] = useState(null);
const [selectedCatalogue, setSelectedCatalogue] = useState(null);
const [loadingCatalogues, setLoadingCatalogues] = useState(true);

// Catalogue filters (on selector panel)
const [catalogueFilters, setCatalogueFilters] = useState({
status: [], // ['Draft', 'Published']
owner: [], // user IDs
visibility: [], // ['Public', 'Private']
searchTerm: "",
});

// Products within selected catalogue
const [products, setProducts] = useState([]);
const [variants, setVariants] = useState({}); // keyed by productId
const [loadingProducts, setLoadingProducts] = useState(false);
const [loadingVariantsByProduct, setLoadingVariantsByProduct] = useState({});

// Product filters (within catalogue detail)
const [productFilters, setProductFilters] = useState({
brand: [],
model: [],
sku: "",
availability: [],
lastUpdate: null,
});

// Pagination
const [currentPage, setCurrentPage] = useState(1);
const [PAGE_SIZE] = useState(20);
const [totalProducts, setTotalProducts] = useState(0);
const [lastVisibleDoc, setLastVisibleDoc] = useState(null);
const [hasMore, setHasMore] = useState(true);

// Row expansion
const [expandedRowKeys, setExpandedRowKeys] = useState([]);

// Selection & bulk operations
const [selectedProducts, setSelectedProducts] = useState([]);
const [selectedVariants, setSelectedVariants] = useState({}); // { productId: [variantIds...] }

// Modal states
const [isBulkAddVisible, setIsBulkAddVisible] = useState(false);
const [isPublishPreviewVisible, setIsPublishPreviewVisible] = useState(false);
const [isProductDetailVisible, setIsProductDetailVisible] = useState(false);
const [selectedProductForDetail, setSelectedProductForDetail] = useState(null);

// Bulk operations
const [bulkOperationInProgress, setBulkOperationInProgress] = useState(false);
const [bulkOperationProgress, setBulkOperationProgress] = useState({
current: 0,
total: 0,
});

// UI feedback
const [userRole, setUserRole] = useState(null);
const [clientId, setClientId] = useState(null);
const [messageApi, contextHolder] = message.useMessage();

State Dictionary

VariableTypePurposeInitial Value
cataloguesArrayList of user's catalogues[]
selectedCatalogueIdstringCurrently selected catalogue IDnull
productsArrayProducts in selected catalogue[]
variantsObjectVariants indexed by productId
expandedRowKeysArrayProduct IDs with expanded variant rows[]
currentPageNumberPagination cursor1
selectedProductsArrayProduct IDs selected for bulk ops[]
selectedVariantsObjectVariants per product selected
bulkOperationInProgressBooleanIndicates ongoing bulk operationfalse

Reference-Based State (useRef)

// Caches to avoid duplicate requests
const productsCacheRef = useRef(new Map()); // { cacheKey: { data, timestamp } }
const variantsCacheRef = useRef(new Map()); // { productId: { data, timestamp } }
const catalogueMetadataRef = useRef({}); // Quick lookup for catalogue properties

// Debouncing & throttling
const lastBulkOpTimeRef = useRef(0);
const MIN_BULK_OP_INTERVAL = 500; // 500ms min between bulk ops
const lastErrorTimeRef = useRef(0);
const ERROR_DEBOUNCE_TIME = 2000; // 2s min between error messages

// Cancellation & abort
const bulkOpAbortControllerRef = useRef(null); // Cancel in-flight bulk operations

Effects & Initialization

// Load user data (role, clientId) on mount
useEffect(() => {
const role = secureGetItem("role");
const clientId = secureGetItem("clientId");
setUserRole(role);
setClientId(clientId);
}, []);

// Load catalogues on mount
useEffect(() => {
loadCatalogues();
}, []);

// Load products when catalogue selected
useEffect(() => {
if (selectedCatalogueId) {
loadProductsForCatalogue();
}
}, [selectedCatalogueId]);

// Restore page state if returning from product detail
useEffect(() => {
const isReturning = location.state?.fromProductDetail === true;
if (isReturning) {
restorePageState();
} else {
clearPageState();
}
}, [location.state?.fromProductDetail]);

Session State Persistence

Similar to ARSCatalog, MyCatalogues stores entire page state to sessionStorage for navigation restoration.

Cache Key: myCatalogues_pageState

Stored Fields:

{
selectedCatalogueId,
selectedCatalogue,
products,
variants,
expandedRowKeys,
selectedProducts,
selectedVariants,
currentPage,
lastVisibleDoc,
productFilters,
catalogueFilters,
timestamp: Date.now()
}

TTL: 2 hours; cache invalidated if exceeded.

Save Trigger: Before navigating to product detail or manual save actions.


Role-Based Access Control (RBAC)

MyCatalogues enforces stricter permissions than ARSCatalog.

Permission Model

const canEditCatalogue = (role) => {
return ["Admin", "Manager", "Super Admin"].includes(role);
};

const canPublishCatalogue = (role) => {
return ["Admin", "Super Admin"].includes(role);
};

const canImportProducts = (role) => {
return ["Admin", "Super Admin"].includes(role);
};

const canEditProduct = (role) => {
return ["Admin", "Manager", "Super Admin"].includes(role);
};

const canDeleteProduct = (role) => {
return ["Admin", "Super Admin"].includes(role);
};

const isSuperAdmin = (role) => {
return ["Admin", "Super Admin"].includes(role);
};

Access Matrix

OperationAdminSuper AdminManagerModellistaModellerUser
Create Catalogue
Edit Catalogue
Publish Catalogue
Import Products (CSV)
Add Product to Catalogue
Edit Product Details
Delete Product
Bulk Operations

UI Gating

Buttons and actions are hidden/disabled based on role:

{
canPublishCatalogue(userRole) && (
<Button onClick={handlePublish}>Publish Catalogue</Button>
);
}

{
canImportProducts(userRole) && (
<Button onClick={() => setIsBulkAddVisible(true)}>Import CSV</Button>
);
}

{
canEditProduct(userRole) && (
<Button onClick={handleEditProduct}>Edit Product</Button>
);
}

Filter Architecture

Filters operate at two levels: catalogue-level and product-level.

Catalogue-Level Filters

Applied to the catalogue selector list:

const catalogueFilters = {
status: [], // 'Draft' | 'Published'
owner: [], // user IDs
visibility: [], // 'Public' | 'Private'
searchTerm: "", // by catalogue name
};

Firebase Query: Most catalogue filters are server-side since catalogues are typically small (< 100).

Product-Level Filters

Applied to products within selected catalogue:

const productFilters = {
brand: [], // brand names/IDs
model: [], // model names/IDs
sku: "", // free-text SKU search
availability: [], // 'Available' | 'Unavailable' | 'Discontinued'
lastUpdate: null, // date range (optional)
};

Strategy: Hybrid — server-side filtering on indexed fields (brand, availability), client-side post-processing on SKU search and complex tag intersections.

Filter Combination Logic

const combineFilters = (filters) => {
// Expand high-level categories into specific field queries
const expandedFilters = {
...filters,
expandedBrands: expandBrandFilters(filters.brand),
expandedModels: expandModelFilters(filters.model),
};

return expandedFilters;
};

const buildFirebaseQuery = (filters, catalogueId) => {
let baseConditions = [
where("status", "==", "Pubblicato"),
where("catalogueId", "==", catalogueId),
];

// Add brand filter if present
if (filters.expandedBrands && filters.expandedBrands.length > 0) {
if (filters.expandedBrands.length <= 10) {
baseConditions.push(
where("mainBrandRef", "in", filters.expandedBrands)
);
} else {
// Defer to client-side for large filter sets
}
}

// Add availability filter
if (filters.availability && filters.availability.length > 0) {
baseConditions.push(
where("availabilityStatus", "in", filters.availability)
);
}

return baseConditions;
};

Caching Strategy

MyCatalogues prioritises consistency over performance on writes, with shorter cache TTLs than ARSCatalog.

Cache Tiers

Tier 1: IndexedDB (Primary Persistent Cache)

  • Catalogues: 1-hour TTL
  • Products: 30-minute TTL (shorter due to write frequency)
  • Variants: 2-hour TTL
  • Metadata: Configurable per entry

Tier 2: SessionStorage (Page State)

  • Entire page state: 2-hour TTL
  • UI state preservation across navigations

Tier 3: useRef Caches (Runtime)

  • Products cache: In-memory during session
  • Variants cache: Per-product, cleared on edit
  • Bulk operation queue: Temporary queuing for batch requests

Invalidation Rules

// On product add/remove
const invalidateProductsCache = (catalogueId) => {
clearStoreCache(PRODUCTS_STORE, `catalogue_${catalogueId}`);
};

// On product edit
const invalidateProductCache = (productId) => {
clearStoreCache(PRODUCTS_STORE, productId);
variantsCacheRef.current.delete(productId);
};

// On publish
const invalidatePublishCache = (catalogueId) => {
clearStoreCache(PRODUCTS_STORE, `catalogue_${catalogueId}`);
clearStoreCache(METADATA_STORE, `catalogue_${catalogueId}`);
};

// Manual clear on import
const clearImportCache = () => {
productsCacheRef.current.clear();
variantsCacheRef.current.clear();
sessionStorage.removeItem("myCatalogues_pageState");
};

Data Sanitisation for IndexedDB

const sanitizeForCache = (data) => {
if (data === null || data === undefined) return data;

const type = typeof data;
if (["string", "number", "boolean"].includes(type)) return data;

if (Array.isArray(data)) {
return data
.map((item) => sanitizeForCache(item))
.filter((i) => i !== undefined);
}

if (data instanceof Date) return data.toISOString();

// Handle Firestore Timestamp
if (data && typeof data.toDate === "function") {
try {
return data.toDate().toISOString();
} catch {
return null;
}
}

if (type === "object" && data.constructor === Object) {
const sanitized = {};
for (const [key, value] of Object.entries(data)) {
const sanitizedValue = sanitizeForCache(value);
if (sanitizedValue !== undefined) sanitized[key] = sanitizedValue;
}
return sanitized;
}

console.warn("Skipping non-serialisable type:", type);
return undefined;
};

Pagination and Lazy Loading

Pagination Model

  • Strategy: Cursor-based (Firestore snapshot pagination)
  • Page Size: 20 products per page
  • Trigger: Infinite scroll or "Load More" button
  • State: currentPage, lastVisibleDoc, hasMore

Variant Loading

Variants are loaded lazily when product row expands:

const onRowExpand = (expanded, record) => {
if (
expanded &&
!variants[record.id] &&
!loadingVariantsByProduct[record.id]
) {
loadProductVariants(record.id);
}
};

const loadProductVariants = async (productId) => {
setLoadingVariantsByProduct((prev) => ({ ...prev, [productId]: true }));

try {
const cached = variantsCacheRef.current.get(productId);
if (cached && isCacheFresh(cached.timestamp)) {
setVariants((prev) => ({ ...prev, [productId]: cached.data }));
return;
}

const loadedVariants = await fetchVariantsForProduct(productId);
setVariants((prev) => ({ ...prev, [productId]: loadedVariants }));

// Cache for future expansions
variantsCacheRef.current.set(productId, {
data: loadedVariants,
timestamp: Date.now(),
});
} catch (error) {
messageApi.error(`Failed to load variants for product ${productId}`);
} finally {
setLoadingVariantsByProduct((prev) => {
const copy = { ...prev };
delete copy[productId];
return copy;
});
}
};

Bulk Operations and Workflows

Bulk Add from CSV

  1. File Upload & Parsing

    • User selects CSV file
    • Client-side parsing (Papa Parse library)
    • Validation: required columns (SKU, Variant Name, etc.)
  2. Conflict Detection

    • Check for duplicate SKUs within import
    • Check for existing SKUs in catalogue
    • Resolve conflicts: skip, replace, or error
  3. Preflight

    • Show summary: X products to add, Y conflicts
    • Show detailed conflict list
    • User confirms or cancels
  4. Batch Import

    • Break into batches (10-20 items per batch)
    • Submit batches with retry logic
    • Show progress bar
  5. Post-Import

    • Update products list with new items
    • Invalidate cache
    • Show success summary
    • Option to undo (within session)
const handleBulkAddFromCsv = async (file, catalogueId) => {
try {
setBulkOperationInProgress(true);

// Parse CSV
const rows = await parseCsvFile(file);
const conflicts = detectConflicts(rows, catalogueId);

if (conflicts.length > 0) {
// Show conflict resolution UI
setConflicts(conflicts);
// User resolves, then continues
return;
}

// Batch import
const batches = chunkArray(rows, 15);
let successful = 0;
let failed = 0;

for (let i = 0; i < batches.length; i++) {
setBulkOperationProgress({ current: i, total: batches.length });

try {
await apiClient.bulkAddProductsToCatalogue(
catalogueId,
batches[i]
);
successful += batches[i].length;
} catch (err) {
failed += batches[i].length;
console.error("Batch failed:", err);
}
}

// Invalidate cache
invalidateProductsCache(catalogueId);
await loadProductsForCatalogue();

messageApi.success(
`Imported ${successful} products. ${failed} failed.`
);
} catch (error) {
messageApi.error("Bulk import failed. Please try again.");
} finally {
setBulkOperationInProgress(false);
}
};

Bulk Operations: Remove Selected

  1. User selects products/variants
  2. Clicks "Remove Selected"
  3. Confirmation dialog
  4. Batch delete with progress
  5. Cache invalidation and list refresh
const handleBulkRemove = async () => {
if (
selectedProducts.length === 0 &&
Object.keys(selectedVariants).length === 0
) {
messageApi.warning("Please select items to remove.");
return;
}

Modal.confirm({
title: "Remove Selected Items?",
content: `Remove ${
selectedProducts.length
} products and ${getTotalVariants()} variants?`,
okText: "Remove",
cancelText: "Cancel",
onOk: async () => {
setBulkOperationInProgress(true);
try {
await apiClient.bulkRemoveFromCatalogue(
selectedCatalogueId,
selectedProducts,
selectedVariants
);

invalidateProductsCache(selectedCatalogueId);
setSelectedProducts([]);
setSelectedVariants({});
await loadProductsForCatalogue();

messageApi.success("Items removed successfully.");
} catch (error) {
messageApi.error("Removal failed. Please try again.");
} finally {
setBulkOperationInProgress(false);
}
},
});
};

Draft & Publish Workflow

  1. Draft Creation

    • Catalogue created with status "Draft"
    • Products can be added/modified freely
    • No public visibility
  2. Publish Preview

    • User clicks "Publish"
    • PublishPreviewModal shows:
      • Summary of changes since last publish
      • List of new/updated products
      • Option to select specific products to publish
  3. Publish Execution

    • Server updates catalogue status to "Published"
    • Products marked as published versions
    • Audit trail recorded
    • Cache invalidated
  4. Post-Publish

    • Catalogue now visible in ARShades Library
    • Further edits create new draft version
    • Users can revert to previous published version (via archive)
const handlePublish = async () => {
try {
const preview = await apiClient.getPublishPreview(selectedCatalogueId);
setPublishPreview(preview);
setIsPublishPreviewVisible(true);
} catch (error) {
messageApi.error("Failed to load publish preview.");
}
};

const confirmPublish = async (selectedProductIds) => {
try {
setIsBulkOperationInProgress(true);

await apiClient.publishCatalogue(
selectedCatalogueId,
selectedProductIds // optional: publish specific products only
);

invalidatePublishCache(selectedCatalogueId);
await loadCatalogues();
setIsPublishPreviewVisible(false);

messageApi.success("Catalogue published successfully.");
} catch (error) {
messageApi.error("Publish failed. Please try again.");
} finally {
setIsBulkOperationInProgress(false);
}
};

Integration with UnifiedProductDetail

UnifiedProductDetail.js (at #file:UnifiedProductDetail.js) is the shared product editing component used by both ARSCatalog and MyCatalogues.

Integration Pattern

const [showProductDetail, setShowProductDetail] = useState(false);
const [selectedProductForDetail, setSelectedProductForDetail] = useState(null);

const handleEditProduct = (productId) => {
savePageState(); // Save MyCatalogues state before navigating
setSelectedProductForDetail(productId);
setShowProductDetail(true);
};

const handleProductDetailClose = () => {
setShowProductDetail(false);
setSelectedProductForDetail(null);
};

const handleProductDetailSave = async (updatedProduct) => {
// Invalidate product cache
invalidateProductCache(updatedProduct.id);

// Update local state
setProducts((prev) =>
prev.map((p) => (p.id === updatedProduct.id ? updatedProduct : p))
);

messageApi.success("Product updated successfully.");
handleProductDetailClose();
};

// Render
{
showProductDetail && (
<UnifiedProductDetail
apiConfig={{
fetchProduct: arsApi.getProduct,
fetchVariants: arsApi.getVariants,
buildModelUrl: (productId, variantId) =>
`.../${productId}/${variantId}`,
buildVtoUrl: (productId, variantId) =>
`.../${productId}/${variantId}`,
}}
routeConfig={{
backRoute: "/my-catalogues",
catalogueRoute: `/my-catalogues/${selectedCatalogueId}`,
onBack: handleProductDetailClose,
}}
componentName="MyCataloguesProduct"
productId={selectedProductForDetail}
catalogueId={selectedCatalogueId}
show3DAssets={true}
onClose={handleProductDetailClose}
onSave={handleProductDetailSave}
/>
);
}

Props Configuration

PropTypeValue (MyCatalogues)Notes
apiConfigObject{ fetchProduct, fetchVariants, buildModelUrl, buildVtoUrl }Connects to MyCataloguesApi
routeConfigObject{ backRoute: "/my-catalogues", catalogueRoute: "..." }Navigation context
productIdstringSelected product IDFrom row click
catalogueIdstringSelected catalogue IDFor scoped operations
componentNamestring"MyCataloguesProduct"For logging/debugging
show3DAssetsbooleantrueEnable 3D model viewing
modestring"edit" (if canEditProduct(userRole))Or "view"
onClosefunctionhandleProductDetailCloseCallback on close
onSavefunctionhandleProductDetailSaveCallback on save; triggers cache invalidation

Data Flow Diagrams

Catalogue Loading & Product Display

User opens MyCatalogues

useEffect: load catalogues list

loadCatalogues() → fetchUserCatalogues()

Try IndexedDB cache (TTL check)
├─ Hit → Return cached catalogues
└─ Miss → Fetch from Firebase

setCatalogues(result)

Render catalogue selector list

User clicks catalogue

setSelectedCatalogueId(id)

useEffect: load products for catalogue

loadProductsForCatalogue() → fetchProducts()

Build Firebase query (brand, availability filters)

Try IndexedDB cache
├─ Hit → Return cached products
└─ Miss → Fetch from Firebase

setProducts(result) + setTotalProducts(count)

Render products table with rows

Product Expansion & Variant Loading

User clicks expand icon on product row

onRowExpand() triggered

Check: variants[productId] already loaded?
├─ YES → Show cached variants, return
└─ NO → Continue

Check: variant fetch in progress?
├─ YES → Return (skip duplicate request)
└─ NO → Continue

setLoadingVariantsByProduct[productId] = true

loadProductVariants(productId)

Try useRef cache first
├─ Hit & fresh → Use cached data
└─ Miss/stale → Fetch from Firebase

setVariants(prev => { ...prev, [productId]: loaded })

Cache to useRef for future expansions

setLoadingVariantsByProduct[productId] = false

Nested variant rows render

Bulk Import Workflow

User clicks "Import CSV"

BulkAddModal opens

User selects file

parseCsvFile(file)

Validate CSV columns
├─ Missing columns → Error message
└─ OK → Continue

detectConflicts(rows, catalogueId)
├─ No conflicts → Proceed to confirm
└─ Conflicts found → Show resolution UI

User confirms

chunkArray(rows, 15)

For each batch:
├─ apiClient.bulkAddProductsToCatalogue()
├─ Update progress
└─ On error: retry or skip

invalidateProductsCache(catalogueId)

loadProductsForCatalogue()

Show success message with stats

Publish Workflow

User clicks "Publish Catalogue"

apiClient.getPublishPreview(catalogueId)

PublishPreviewModal opens
├─ Show changes summary
├─ List affected products
└─ Allow selection (all/specific)

User clicks "Confirm Publish"

apiClient.publishCatalogue(catalogueId, selectedProductIds)

Server updates status & timestamps

Audit trail recorded

invalidatePublishCache(catalogueId)

loadCatalogues()

Show success message

Catalogue now visible in ARShades Library

Performance Optimisations

1. Memoised Selectors

const visibleProducts = useMemo(() => {
// Apply role-based filtering
return isSuperAdmin(userRole)
? products
: products.filter((p) => p.isVisibleToRole?.[userRole] !== false);
}, [products, userRole]);

const filteredProducts = useMemo(() => {
// Apply product-level filters
return visibleProducts.filter((p) => {
if (
productFilters.brand.length &&
!productFilters.brand.includes(p.brand)
)
return false;
if (
productFilters.availability.length &&
!productFilters.availability.includes(p.status)
)
return false;
if (
productFilters.sku &&
!p.sku.toLowerCase().includes(productFilters.sku.toLowerCase())
)
return false;
return true;
});
}, [visibleProducts, productFilters]);

2. Lazy Variant Loading

  • Variants loaded only on row expansion (not on initial page load)
  • Reduces initial bundle by ~70%
  • Improves time-to-interactive

3. Pagination Over Full Load

  • 20 products per page (configurable)
  • Infinite scroll or "Load More" button
  • Avoids rendering hundreds of rows at once

4. Throttled Bulk Operations

const handleBulkOp = useCallback(async () => {
const now = Date.now();
if (now - lastBulkOpTimeRef.current < MIN_BULK_OP_INTERVAL) {
return; // Throttle rapid bulk ops
}
lastBulkOpTimeRef.current = now;
// ... perform bulk operation ...
}, []);

5. Session Cache for Navigation

Saves and restores entire page state to sessionStorage, avoiding full re-loads when returning from product detail.


Components

  • /src/pages/MyCatalogues.js – Main page
  • /src/components/MyCatalogues/CatalogueSelector.js – Left panel
  • /src/components/MyCatalogues/CatalogueDetail.js – Right panel
  • /src/components/MyCatalogues/ProductsTable.js – Main table
  • /src/components/MyCatalogues/BulkAddModal.js – CSV import
  • /src/components/MyCatalogues/PublishPreviewModal.js – Publish preview
  • /src/components/ProductDetail/UnifiedProductDetail.jsShared editor (#file:UnifiedProductDetail.js)

Services & Utilities

  • /src/services/api/MyCataloguesApi.js – API client for catalogue operations
  • /src/services/cache/CacheService.js – IndexedDB helpers
  • /src/utils/csv-parser.js – CSV parsing and validation
  • /src/utils/date-utils.js – Date formatting and comparisons

Configuration Constants

const PAGE_CACHE_KEY = "myCatalogues_pageState";
const PAGE_SIZE = 20;
const MIN_BULK_OP_INTERVAL = 500; // ms
const ERROR_DEBOUNCE_TIME = 2000; // ms

const CACHE_TTL = {
CATALOGUES: 60 * 60 * 1000, // 1 hour
PRODUCTS: 30 * 60 * 1000, // 30 minutes (shorter for consistency)
VARIANTS: 2 * 60 * 60 * 1000, // 2 hours
};

const BATCH_SIZE = {
BULK_ADD: 15,
BULK_REMOVE: 20,
PUBLISH: 25,
};

Testing Considerations

Test Scenarios

  1. Catalogue CRUD

    • Create new catalogue
    • Edit catalogue metadata
    • Publish catalogue
    • Verify draft/published states
  2. Product Management

    • Add product to catalogue
    • Remove product
    • Edit product via UnifiedProductDetail
    • Verify cache invalidation
  3. Bulk Operations

    • Import CSV with valid data
    • Import with conflicting SKUs
    • Bulk remove selected products
    • Verify progress indication
  4. Pagination

    • Load page 1 (20 items)
    • Scroll/click "Load More"
    • Verify page 2 loads correctly
    • Verify cursor progression
  5. Variant Loading

    • Expand product row
    • Verify variants load (and not duplicated on re-expand)
    • Verify loading spinner shown
    • Verify cache persists across expand/collapse
  6. RBAC

    • Non-Admin user hides Publish/Import buttons
    • Only Admin can publish
    • Field editing gated by role
    • Verify read-only mode in UnifiedProductDetail for restricted users
  7. Session Persistence

    • Navigate to product detail
    • Return to MyCatalogues
    • Verify filters, pagination, expansions restored
    • Verify state cleared on fresh load
  8. Error Handling

    • Network timeout during bulk import
    • Firebase write failure
    • CSV parsing error
    • Concurrent edit conflict
    • Verify error messages and retry options

Future Enhancements

  • Saved Filter Presets – Allow users to save and recall filter combinations
  • Audit Trail UI – Display edit history and version control
  • Inline Editing – Edit variant specs directly in table rows
  • Real-Time Sync – Firestore listeners for live catalogue updates
  • Undo/Redo – Session-based operation history
  • Batch Export – Export selected products/variants to CSV
  • Template Catalogues – Clone catalogues as starting templates
  • Collaborative Editing – Conflict resolution for concurrent edits
  • Performance Dashboard – Monitor cache hits, API calls, load times

Useful References

Overview

My Catalogues enables administrators to:

  • Create and manage catalogues
  • Add and edit products and variants
  • Upload and manage 3D models and images
  • Draft and publish catalogues
  • Manage product metadata
  • Generate USDZ files from GLB models
  • Collaborate with team members

Architecture

Main Components

MyCatalogues.js (Page Container)
├── Catalogue List View
├── Catalogue Details View
│ ├── Products Grid
│ ├── Add/Edit Product Modal
│ └── Product Detail Editor
│ └── UnifiedProductDetail (Shared Component)
├── Draft/Publish Workflow
└── Modal System
├── Create Catalogue Modal
├── Product Editor Modal
├── Image Upload Modal
├── 3D Model Upload Modal
└── Confirmation Modals

Key Features

1. Catalogue Management

Create Catalogue:

  • Catalogue name and description (multi-language support)
  • Metadata (logo, domain, app configuration)
  • Visibility settings (public/private)

Edit Catalogue:

  • Update all catalogue properties
  • Publish/draft status control
  • Version management

Delete Catalogue:

  • Archive or permanently delete
  • Confirmation workflow

2. Product Administration

Create Product:

  • Product name and model name
  • Specifications and metadata
  • Multiple variant support

Edit Product:

  • Update product information
  • Manage variants
  • Handle images and 3D models

Delete Product:

  • Remove product from catalogue
  • Confirmation workflow

3. Variant Management

Create Variant:

  • Colour and style specification
  • Metadata (size, material, etc.)
  • Image uploads (multiple)
  • 3D model upload (GLB format)

Edit Variant:

  • Update variant specifications
  • Replace/add images
  • Update 3D models
  • Generate USDZ files

Delete Variant:

  • Remove variant with confirmation
  • Cleanup associated files

4. Product Detail Editor

Component: UnifiedProductDetail.js (Shared with ARShades Library)

Location: /src/components/ProductDetail/UnifiedProductDetail.js

In My Catalogues, UnifiedProductDetail operates in edit mode:

  • Full editing capabilities
  • Image management (upload, delete, reorder)
  • 3D model management
  • Variant selection and editing
  • Role-based editing restrictions
  • USDZ file generation
  • Metadata editing

UnifiedProductDetail in My Catalogues

Props Configuration:

<UnifiedProductDetail
apiConfig={{
fetchProduct: getProductFromMyFirebase,
fetchVariants: getVariantsFromMyFirebase,
buildModelUrl: buildModelUrlForEdit,
buildVtoUrl: buildVtoUrlForEdit,
}}
routeConfig={{
backRoute: "/my-catalog",
catalogueRoute: "/my-catalog/:catalogueId",
}}
componentName="Product"
productId={selectedProductId}
catalogueId={selectedCatalogueId}
show3DAssets={true}
/>

Key Characteristics:

  • Full editing capabilities enabled
  • Image upload/delete functionality
  • 3D model management
  • Role-based field editing
  • Save and validation
  • Metadata editing (descriptions, specifications)

Editable Fields (by role):

  • Admin: All fields editable
  • Manager: Most fields editable
  • Modellista: Limited to specifications
  • ModellerSupervisor: Limited to model verification

5. Image Management

Upload Images:

  • Multiple format support (JPG, PNG, WebP)
  • Automatic validation and compression
  • Progress indication
  • Error handling and retry

Manage Images:

  • Reorder images via drag-and-drop
  • Delete individual images
  • View full-resolution
  • Set primary/thumbnail image

Image Types in UnifiedProductDetail:

  • Product poster images (poster, poster2, poster3, etc.)
  • User-generated thumbnails
  • ARShades-generated thumbnails
  • Preview images for variants

6. 3D Model Management

Upload 3D Models:

  • GLB format support
  • File size validation
  • Preview before upload
  • Progress indication

3D Model Operations:

  • Update existing model
  • Delete model
  • Generate USDZ file (for iOS compatibility)
  • Preview 3D viewer

USDZ Generation:

  • Convert GLB to USDZ format
  • Automatic processing
  • Storage and retrieval
  • Version management

7. Draft and Publish Workflow

Draft State:

  • Catalogue remains unpublished
  • Visible only to administrators
  • Full editing capabilities
  • Can test before release

Publish State:

  • Catalogue becomes available in ARShades Library
  • Visible to all users
  • Restricted editing (some fields locked)
  • Version tracking

Workflow:

Create Catalogue (Draft)

Add Products & Variants (Draft)

Upload Images & 3D Models (Draft)

Test & Validate (Draft)

Publish to Library

Visible in ARShades Library

State Management

Local State

const [catalogues, setCatalogues] = useState([]);
const [selectedCatalogue, setSelectedCatalogue] = useState(null);
const [products, setProducts] = useState([]);
const [selectedProduct, setSelectedProduct] = useState(null);
const [showProductEditor, setShowProductEditor] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [uploadingFiles, setUploadingFiles] = useState({});
const [pendingUploads, setPendingUploads] = useState({});

Redux State

const userRole = useSelector((state) => state.user?.role);
const catalogues = useSelector(
(state) => state.catalogues?.myListaCatalogues || []
);

Data Flow

User opens My Catalogues

Load user's catalogues

Display catalogue list

User selects catalogue

Load catalogue's products

Display products grid

User clicks product

Open UnifiedProductDetail editor

User edits product

Save to Firebase

Catalogue updated
ModalPurposeTriggered By
Create CatalogueNew catalogue creation"New Catalogue" button
Edit CatalogueModify catalogue detailsCatalogue context menu
Product EditorAdd/edit product"New Product" or product click
Image UploadUpload product imagesImage section in editor
3D Model UploadUpload GLB model3D section in editor
Publish ConfirmationConfirm catalogue publishing"Publish" button
Delete ConfirmationConfirm deletion actionDelete buttons
Page Content
├── Catalogue List Modal
├── Product Editor Modal
│ ├── Image Upload Modal
│ ├── 3D Model Upload Modal
│ └── Confirmation Modals

Role-Based Access Control

Field Editability by Role

FieldAdminManagerModellistaModellerSupervisor
Product Name
Variant Specs
Images
3D Models
Descriptions
Publish

Performance Optimisations

File Upload Optimisations

  • Compress images before upload
  • Chunk large files
  • Parallel upload with retry logic
  • Progress indication
  • Cancel support

Image Caching

  • Cache uploaded images locally
  • Lazy load previews
  • Progressive rendering

3D Model Handling

  • Stream GLB uploads
  • Validate format before upload
  • Cache USDZ conversions
  • Background processing for USDZ generation

UnifiedProductDetail Reference

Important: The UnifiedProductDetail component is shared between ARShades Library and My Catalogues pages.

Differences in Usage

AspectARShades LibraryMy Catalogues
ModeRead-only viewingFull editing
Image Upload❌ Disabled✅ Enabled
Image Delete❌ Disabled✅ Enabled
3D Model Upload❌ Disabled✅ Enabled
Model Download
Field Editing✅ (role-based)
Variant Switch
VTO Preview✅ Read-only
Metadata Edit✅ (role-based)

For detailed UnifiedProductDetail documentation, see: ../shared-components/unified-product-detail.md

Components

  • /src/pages/MyCatalogues.js – Main page component
  • /src/components/ProductDetail/UnifiedProductDetail.jsShared product detail component (also used in ARShades Library)
  • Catalogue list component
  • Product grid component
  • Modal system components

Services

  • /src/services/my-catalogues.js – Catalogue management API
  • /src/services/firebase.js – Firebase operations
  • /src/services/image-upload.js – Image handling
  • /src/services/model-conversion.js – 3D model operations (GLB → USDZ)

Data

  • Firebase collection: Catalogues – Catalogue data
  • Firebase collection: CataloguesVariants – Product variants
  • Firebase collection: CatalogueImages – Image storage
  • Firebase collection: CatalogueModels – 3D model storage

Error Handling

Common Issues

File upload timeout: Show retry option with exponential backoff

Invalid file format: Display error with accepted formats list

Firebase write failure: Queue update and retry when online

Concurrent edit conflict: Show conflict resolution dialog

USDZ generation failed: Show error and fallback to GLB