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
| Aspect | ARSCatalog | MyCatalogues |
|---|---|---|
| Purpose | Browse & filter all products | Create & manage own catalogues |
| Write Operations | Limited (bulk SKU adds) | Extensive (CRUD + publish) |
| State Scope | All products | Scoped to selected catalogue |
| Publishing | N/A | Core workflow |
| UnifiedProductDetail | Read-only view | Edit mode (role-gated) |
| Cache Strategy | Long TTL (safety-focused) | Short TTL (consistency-focused) |
Quick Navigation
- For Quick Start: Jump to Component Hierarchy
- For State Deep Dive: See State Management
- For Filtering: See Filter Architecture
- For Caching: Review Caching Strategy
- For Integration: Consult UnifiedProductDetail Integration
- For Bulk Ops: See Bulk Operations
- For Testing: See Testing Considerations
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
UnifiedProductDetailcomponent - 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
| Variable | Type | Purpose | Initial Value |
|---|---|---|---|
catalogues | Array | List of user's catalogues | [] |
selectedCatalogueId | string | Currently selected catalogue ID | null |
products | Array | Products in selected catalogue | [] |
variants | Object | Variants indexed by productId | |
expandedRowKeys | Array | Product IDs with expanded variant rows | [] |
currentPage | Number | Pagination cursor | 1 |
selectedProducts | Array | Product IDs selected for bulk ops | [] |
selectedVariants | Object | Variants per product selected | |
bulkOperationInProgress | Boolean | Indicates ongoing bulk operation | false |
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
| Operation | Admin | Super Admin | Manager | Modellista | Modeller | User |
|---|---|---|---|---|---|---|
| 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
-
File Upload & Parsing
- User selects CSV file
- Client-side parsing (Papa Parse library)
- Validation: required columns (SKU, Variant Name, etc.)
-
Conflict Detection
- Check for duplicate SKUs within import
- Check for existing SKUs in catalogue
- Resolve conflicts: skip, replace, or error
-
Preflight
- Show summary: X products to add, Y conflicts
- Show detailed conflict list
- User confirms or cancels
-
Batch Import
- Break into batches (10-20 items per batch)
- Submit batches with retry logic
- Show progress bar
-
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
- User selects products/variants
- Clicks "Remove Selected"
- Confirmation dialog
- Batch delete with progress
- 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
-
Draft Creation
- Catalogue created with status "Draft"
- Products can be added/modified freely
- No public visibility
-
Publish Preview
- User clicks "Publish"
PublishPreviewModalshows:- Summary of changes since last publish
- List of new/updated products
- Option to select specific products to publish
-
Publish Execution
- Server updates catalogue status to "Published"
- Products marked as published versions
- Audit trail recorded
- Cache invalidated
-
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
| Prop | Type | Value (MyCatalogues) | Notes |
|---|---|---|---|
apiConfig | Object | { fetchProduct, fetchVariants, buildModelUrl, buildVtoUrl } | Connects to MyCataloguesApi |
routeConfig | Object | { backRoute: "/my-catalogues", catalogueRoute: "..." } | Navigation context |
productId | string | Selected product ID | From row click |
catalogueId | string | Selected catalogue ID | For scoped operations |
componentName | string | "MyCataloguesProduct" | For logging/debugging |
show3DAssets | boolean | true | Enable 3D model viewing |
mode | string | "edit" (if canEditProduct(userRole)) | Or "view" |
onClose | function | handleProductDetailClose | Callback on close |
onSave | function | handleProductDetailSave | Callback 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.
Related Components & Services
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.js– Shared 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
-
Catalogue CRUD
- Create new catalogue
- Edit catalogue metadata
- Publish catalogue
- Verify draft/published states
-
Product Management
- Add product to catalogue
- Remove product
- Edit product via UnifiedProductDetail
- Verify cache invalidation
-
Bulk Operations
- Import CSV with valid data
- Import with conflicting SKUs
- Bulk remove selected products
- Verify progress indication
-
Pagination
- Load page 1 (20 items)
- Scroll/click "Load More"
- Verify page 2 loads correctly
- Verify cursor progression
-
Variant Loading
- Expand product row
- Verify variants load (and not duplicated on re-expand)
- Verify loading spinner shown
- Verify cache persists across expand/collapse
-
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
-
Session Persistence
- Navigate to product detail
- Return to MyCatalogues
- Verify filters, pagination, expansions restored
- Verify state cleared on fresh load
-
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
- Firestore Queries: https://firebase.google.com/docs/firestore/query-data/queries
- React Hooks: 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
- Papa Parse (CSV): https://www.papaparse.com/
- React Performance: https://react.dev/reference/react/useMemo
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
Modal System
Modal Types
| Modal | Purpose | Triggered By |
|---|---|---|
| Create Catalogue | New catalogue creation | "New Catalogue" button |
| Edit Catalogue | Modify catalogue details | Catalogue context menu |
| Product Editor | Add/edit product | "New Product" or product click |
| Image Upload | Upload product images | Image section in editor |
| 3D Model Upload | Upload GLB model | 3D section in editor |
| Publish Confirmation | Confirm catalogue publishing | "Publish" button |
| Delete Confirmation | Confirm deletion action | Delete buttons |
Modal Nesting
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
| Field | Admin | Manager | Modellista | ModellerSupervisor |
|---|---|---|---|---|
| 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
| Aspect | ARShades Library | My Catalogues |
|---|---|---|
| Mode | Read-only viewing | Full 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
Related Files & Services
Components
/src/pages/MyCatalogues.js– Main page component/src/components/ProductDetail/UnifiedProductDetail.js– Shared 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