Skip to main content
Version: 1.0.0

Brand Identity

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

Overview

The Brand Identity section provides comprehensive brand management capabilities for teams. It allows users to:

  • View and manage brand assets (logos, images, graphics) organized by team
  • Customize brand colors (primary, secondary, alternative colors)
  • Edit image metadata (display name, description, alt text)
  • Upload and organize brand identity resources
  • Preview brand assets in a gallery-style interface

This system is built on Firebase Storage for image hosting and Firestore for metadata storage, providing a scalable and secure solution for brand asset management.

Folder location: /src/components/Profile

Components (5 files):

  1. BrandIdentitySection.js — Main container, team gallery cards
  2. BrandIdentityGallery.js — Modal with tabbed interface (Gallery/Details/Colors)
  3. BrandIdentity/GalleryView.js — Image grid with drag-drop upload
  4. BrandIdentity/DetailsView.js — Image metadata editor (name, description, alt text)
  5. BrandIdentity/ColorsView.js — Brand color picker and editor

BrandIdentitySection Component

Purpose

Entry point for brand identity management. Displays a grid of team cards with brand logos and preview information. Each card links to the detailed gallery interface.

Props

{
teams: Array<{
id: string,
name: string,
logo?: { url: string, name: string },
hasLogo: boolean,
primaryColor: string,
secondaryColor: string
}>,
loading: boolean
}

Features

  1. Team card gallery — Responsive grid (1-4 columns based on screen size)
  2. Logo preview — First uploaded logo displayed as card background
  3. Brand colors fallback — If no logo, shows gradient using primary/secondary colors
  4. Team metadata — Team name overlaid on card with dark gradient for readability
  5. Lazy loading — Fetches logos on component mount via getAllBrandIdentityImages()

Data Flow

On component mount:

1. For each team:
├─ Call getAllBrandIdentityImages(team.id)
│ └─ Fetch logos/images from Firebase Storage
├─ Fetch team colors from Firestore (Teams/{teamId}/brand_identity)
└─ Set defaults if not found
├─ primaryColor: '#016845'
└─ secondaryColor: '#dec690'

2. Populate teamsWithLogos with enriched data
3. Render card grid

Card Design

┌─────────────────────────────┐
│ │
│ Logo Image/Gradient │
│ (200px height) │
│ │
│ ┌───────────────────────┐ │
│ │ Team Name │◄─ Dark overlay gradient
│ │ (white text, bold) │ │
│ └───────────────────────┘ │
└─────────────────────────────┘

Responsive grid:

  • Mobile (xs): 1 column
  • Tablet (sm): 2 columns
  • Medium (md): 3 columns
  • Large (lg): 4 columns
const handleOpenBrandIdentityGallery = (team) => {
setSelectedTeamForGallery(team);
setIsBrandIdentityGalleryOpen(true);
// Passes to BrandIdentityGallery modal
};

BrandIdentityGallery Component

Purpose

Main modal interface for managing a team's brand identity. Provides three tabbed views: Gallery, Details, and Colors.

Features

  1. Three-tab interface:

    • Logos — Team logos (max 1 per team concept)
    • Images — Brand asset images
    • Colors — Brand color palette
  2. Image upload — Drag-drop with validation:

    • Supported: image/* (PNG, JPG, GIF, SVG, etc.)
    • Max size: 3MB per image
    • Firebase Storage integration with automatic retry
  3. Image metadata editing:

    • Display Name
    • Description (rich text)
    • Alternative Text (for accessibility)
  4. Color management:

    • Primary color (brand main)
    • Secondary color (accent)
    • Alternative 1 (usually white)
    • Alternative 2 (usually black)
  5. Image deletion with validation:

    • Check if image is used by catalogues
    • Show warning if dependencies exist
    • Confirmation modal before deletion

Layout: 70% gallery grid + 30% upload panel

Gallery Grid:

  • Responsive columns: 8 on xs, 6 on sm, 4 on md, 3 on lg
  • Aspect ratio: 1:1 (square)
  • Image name overlay at bottom
  • Click to select and switch to Details view

Upload Panel (Right side):

┌─────────────────────┐
│ │
│ Upload Dragger: │
│ [Click or drag] │
│ │
│ Supported: image/* │
│ Max: 3MB each │
│ │
└─────────────────────┘

Tab 2: Details View

Layout: 70% image preview + 30% metadata panel

Left Column (Image Preview):

┌──────────────────────┐
│ │
│ Selected Image │
│ (fit, no crop) │
│ │
└──────────────────────┘

Right Column (Metadata):

┌──────────────────────┐
│ Image Details │
├──────────────────────┤
│ Display Name │
│ [text input] │
│ │
│ Description │
│ [textarea, 4 rows] │
│ │
│ Alt Text │
│ [textarea, 3 rows] │
│ │
│ File Info: │
│ Size: XX KB │
│ Type: image/png │
│ Uploaded: MM/DD/YY │
│ │
│ [Save] [Delete] │
└──────────────────────┘

Workflow: Save Image Metadata

const handleSaveMetadata = async () => {
// 1. Validate required fields
if (!editingMetadata.displayName) {
message.error('Display name is required');
return;
}

try {
setIsSaving(true);

// 2. Call API to update image metadata in Firebase
await updateBrandIdentityMetadata(teamId, selectedImage.fullPath, {
displayName: editingMetadata.displayName,
description: editingMetadata.description,
alternativeText: editingMetadata.alternativeText
});

// 3. Update local state
setSelectedImage(prev => ({
...prev,
customMetadata: editingMetadata
}));

message.success('Image metadata saved successfully');
} catch (error) {
console.error('Error saving metadata:', error);
message.error('Failed to save image metadata');
} finally {
setIsSaving(false);
}
};

Workflow: Delete Image with Dependency Check

const handleDeleteClick = async (image) => {
try {
setCheckingCatalogues(true);

// 1. Check if image used by catalogues
const catalogues = await checkImageDependencies(teamId, image.fullPath);
setCataloguesUsingImage(catalogues);

// 2. Show confirmation modal with warning if dependencies
setImageToDelete(image);
setDeleteConfirmVisible(true);
} catch (error) {
console.error('Error checking catalogues:', error);
message.error('Failed to check image usage');
} finally {
setCheckingCatalogues(false);
}
};

const handleConfirmDelete = async () => {
try {
setIsDeleting(true);

// 1. Delete from Firebase Storage
await deleteBrandIdentityImage(teamId, imageToDelete.fullPath);

// 2. Remove from local state
setImages(prev => ({
...prev,
[activeTab]: prev[activeTab].filter(
img => img.fullPath !== imageToDelete.fullPath
)
}));

// 3. Clear selection
setSelectedImage(null);

message.success('Image deleted successfully');
} catch (error) {
console.error('Error deleting image:', error);
message.error('Failed to delete image');
} finally {
setIsDeleting(false);
setDeleteConfirmVisible(false);
}
};

Tab 3: Colors View

Read Mode:

┌──────────────────────────────┐
│ Brand Colors [Edit] │
├──────────────────────────────┤
│ ┌─────────┐ │
│ │ Primary │ Primary │
│ │ #016845 │ #016845 │
│ └─────────┘ │
│ │
│ ┌─────────┐ │
│ │Secondary│ Secondary │
│ │ #dec690 │ #dec690 │
│ └─────────┘ │
│ │
│ ┌─────────┐ │
│ │ Alt 1 │ Alternative 1 │
│ │ #FFFFFF │ #FFFFFF │
│ └─────────┘ │
│ │
│ ┌─────────┐ │
│ │ Alt 2 │ Alternative 2 │
│ │ #000000 │ #000000 │
│ └─────────┘ │
└──────────────────────────────┘

Edit Mode:

┌──────────────────────────────────────┐
│ Brand Colors [Cancel] [Save] │
├──────────────────────────────────────┤
│ ┌─────────┐ │
│ │ Primary │ [Color Picker Input] │
│ │ │ Shows hex + live update │
│ └─────────┘ │
│ │
│ ┌─────────┐ │
│ │Secondary│ [Color Picker Input] │
│ │ │ │
│ └─────────┘ │
│ │
│ ┌─────────┐ │
│ │ Alt 1 │ [Color Picker Input] │
│ │ │ │
│ └─────────┘ │
│ │
│ ┌─────────┐ │
│ │ Alt 2 │ [Color Picker Input] │
│ │ │ │
│ └─────────┘ │
└──────────────────────────────────────┘

Features:

  • Ant Design ColorPicker: Native hex color input
  • Live preview: Color swatches update instantly
  • Change detection: Save button disabled if no changes
  • Cancel restores original: Reverts to originalColors state

Workflow: Edit and Save Colors

// Load colors on modal open
useEffect(() => {
if (visible && teamId) {
const loadBrandColors = async () => {
const teamDoc = await getDoc(doc(db, 'Teams', teamId));
if (teamDoc.exists()) {
const brandIdentity = teamDoc.data().brand_identity;
const colors = {
primaryColor: brandIdentity?.primaryColor || '#016845',
secondaryColor: brandIdentity?.secondaryColor || '#dec690',
altColorOne: brandIdentity?.altColorOne || '#FFFFFF',
altColorTwo: brandIdentity?.altColorTwo || '#000000'
};
setColors(colors);
setOriginalColors(colors);
}
};
loadBrandColors();
}
}, [visible, teamId]);

// Save colors to Firestore
const handleSaveColors = async () => {
try {
setSavingColors(true);
const teamDocRef = doc(db, 'Teams', teamId);

await updateDoc(teamDocRef, {
'brand_identity.primaryColor': primaryColor,
'brand_identity.secondaryColor': secondaryColor,
'brand_identity.altColorOne': altColorOne,
'brand_identity.altColorTwo': altColorTwo
});

// Update original colors (no longer "dirty")
setOriginalColors({
primaryColor,
secondaryColor,
altColorOne,
altColorTwo
});

setIsEditingColors(false);
message.success('Brand colors saved successfully');
} catch (error) {
message.error('Failed to save brand colors');
} finally {
setSavingColors(false);
}
};

File Upload & Storage Architecture

Upload Flow

User selects files in GalleryView

onBeforeUpload validation:
- Check file type (image/*)
- Check file size (max 3MB)
- Reject invalid files

uploadBrandIdentityImage() API call:
- Determine folder: 'teams/{teamId}/logos' or 'teams/{teamId}/images'
- Generate unique filename with timestamp
- Upload to Firebase Storage
- Store custom metadata (displayName, description, alternativeText)
- Add to images state

Upload complete → Refresh gallery

Firebase Storage Structure

root/
├─ teams/
│ ├─ {teamId1}/
│ │ ├─ logos/
│ │ │ ├─ logo_1_20251107_123456.png
│ │ │ ├─ logo_2_20251107_123457.png
│ │ │ └─ ...
│ │ └─ images/
│ │ ├─ brand_guide_20251107_123458.pdf
│ │ ├─ asset_sheet_20251107_123459.xlsx
│ │ └─ ...
│ ├─ {teamId2}/
│ └─ ...

Firestore Metadata Storage

Document structure: Teams/{teamId}/brand_identity

{
primaryColor: '#016845',
secondaryColor: '#dec690',
altColorOne: '#FFFFFF',
altColorTwo: '#000000',
// Individual images are stored in Firebase Storage custom metadata
// which maps: {displayName, description, alternativeText}
}

Role-Based Access Control (RBAC)

The Brand Identity section respects team role hierarchy:

User RoleCan ViewCan UploadCan Edit MetadataCan DeleteCan Edit Colors
Member
Team Admin
Admin
Super Admin

Error Handling & Edge Cases

Edge Case 1: Image Upload Fails

Scenario: Network error during upload Handling:

  • Show error toast
  • Retry upload (automatic retry with exponential backoff)
  • Display error message after 3 failed attempts

Edge Case 2: Image Used by Catalogues

Scenario: User tries to delete logo used in catalogue Handling:

  • Check dependencies before allowing deletion
  • Show warning modal: "This image is used by X catalogues"
  • Allow user to proceed anyway or cancel

Edge Case 3: Color Save Fails

Scenario: Firestore update fails while saving colors Handling:

  • Error toast displayed
  • Colors remain in edit mode
  • User can retry or cancel

Edge Case 4: No Logos Uploaded

Scenario: Team has no brand identity images Handling:

  • Show empty state in gallery
  • Gradient background used in team cards (from brand colors)
  • Placeholder message: "No images uploaded yet"

Edge Case 5: Concurrent Uploads

Scenario: User uploads multiple large files simultaneously Handling:

  • Queue uploads (managed by uploadedCountRef)
  • Show upload progress
  • Non-blocking UI (uploads in background)

Performance Optimization

Lazy Loading

Images are loaded only when modal is opened:

useEffect(() => {
if (visible && teamId) {
loadAllImages(); // On demand
}
}, [visible, teamId]);

Image Compression

  • Server-side compression via Firebase Storage rules
  • Recommended: Always upload web-optimized images (max 2MB)
  • Users should pre-compress large PNG/JPG files

Caching Strategy

  • Images cached by browser (via HTTP cache headers)
  • Firestore data cached locally
  • Modal state resets on close (fresh load on reopen)