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):
BrandIdentitySection.js— Main container, team gallery cardsBrandIdentityGallery.js— Modal with tabbed interface (Gallery/Details/Colors)BrandIdentity/GalleryView.js— Image grid with drag-drop uploadBrandIdentity/DetailsView.js— Image metadata editor (name, description, alt text)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
- Team card gallery — Responsive grid (1-4 columns based on screen size)
- Logo preview — First uploaded logo displayed as card background
- Brand colors fallback — If no logo, shows gradient using primary/secondary colors
- Team metadata — Team name overlaid on card with dark gradient for readability
- 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
Workflow: Open Brand Identity Gallery
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
-
Three-tab interface:
- Logos — Team logos (max 1 per team concept)
- Images — Brand asset images
- Colors — Brand color palette
-
Image upload — Drag-drop with validation:
- Supported:
image/*(PNG, JPG, GIF, SVG, etc.) - Max size: 3MB per image
- Firebase Storage integration with automatic retry
- Supported:
-
Image metadata editing:
- Display Name
- Description (rich text)
- Alternative Text (for accessibility)
-
Color management:
- Primary color (brand main)
- Secondary color (accent)
- Alternative 1 (usually white)
- Alternative 2 (usually black)
-
Image deletion with validation:
- Check if image is used by catalogues
- Show warning if dependencies exist
- Confirmation modal before deletion
Tab 1: Gallery View
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
originalColorsstate
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 Role | Can View | Can Upload | Can Edit Metadata | Can Delete | Can 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)