Skip to main content
Version: 1.0.0

Routing System

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

Overview

The Dashboard Layout is the main container for the entire application after authentication. It provides:

  • Fixed Header with logo, title, user greeting, help button, and notifications
  • Animated Sidebar with role-based menu items and collapse/expand functionality
  • Main Content Area with <Outlet /> for page rendering
  • Menu System with dynamic visibility based on user role
  • URL-Based Navigation synchronized with sidebar state
  • Responsive Design with automatic collapse on mobile devices
  • Redux Integration for notification counts and sidebar state
  • Framer Motion Animations for smooth transitions

Key characteristics:

  • Role-Based Menu Generation (Super Admin, Admin, Member, Modeller, ModellerSupervisor)
  • Dynamic Notification Badges (Frame Validation, Orders, Teams)
  • Smooth Animations (sidebar collapse, menu expansion, label fade)
  • Mobile-First Responsive (auto-collapse sidebar on small screens)
  • SVG Icon Filtering (custom hexToFilter system for color transformations)
  • Session Persistence (last visited page stored in localStorage)
  • Fixed Layout with full viewport coverage

Location:

  • File: /src/components/DashboardLayout.js
  • Styles: /src/styles/Dashboard/Dashboard.module.css

Architecture Overview

Dashboard Layout
├─ Fixed Header (100% width, top: 0)
│ ├─ Logo (left)
│ ├─ Title "ARShades Studio" (left-center)
│ ├─ User Info (right, hidden on home page)
│ │ ├─ Greeting (Good morning/afternoon/evening/night)
│ │ ├─ Name (firstName lastName)
│ │ ├─ Role (mapped role)
│ │ └─ Company (companyName)
│ ├─ Help Button (right)
│ └─ Notifications Dropdown (right)

├─ Animated Sidebar (left, collapsible)
│ ├─ Main Menu Items (flex: 1)
│ │ ├─ Dashboard (always visible)
│ │ ├─ Role-Specific Items
│ │ │ ├─ Modeller: ARShades Library, My catalogues, Frame Validation
│ │ │ ├─ ModellerSupervisor: ARShades Library, My catalogues, Frame Validation
│ │ │ └─ Super Admin/Admin/Member: Full menu
│ │ └─ Conditional Submenu Items
│ │ ├─ Analytics (with VTO services + Gateways submenu)
│ │ ├─ Subscriptions
│ │ └─ Orders (with notification badge)
│ │
│ └─ Bottom Menu Items (fixed)
│ ├─ Profile (with Teams notification badge)
│ └─ Logout (shows confirmation modal)

├─ Main Content Area
│ └─ <Outlet /> (renders matched route page)

└─ Modals
├─ Logout Confirmation Modal
└─ Support Modal (SupportModal component)

Core Components & Utilities

1. SVG Icon Color System

Function: hexToFilter(hex)

const hexToFilter = (hex) => {
// Se il colore non è una stringa, ritorna none
if (typeof hex !== "string") {
return "none";
}

// Rimuovi il # se presente
hex = hex.replace("#", "");

// Per primaryShades.primary400 (#6774b6) che è il colore delle icone inattive
if (hex.toLowerCase() === "6774b6") {
return "brightness(0) saturate(100%) invert(44%) sepia(18%) saturate(1142%) hue-rotate(201deg) brightness(95%) contrast(86%)";
}

// Per il colore bianco delle icone attive (#ffffff)
if (hex.toLowerCase() === "ffffff") {
return "brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(100%) contrast(100%)";
}

// Per altri colori, prova un filtro generico
return "brightness(0) saturate(100%) invert(44%) sepia(18%) saturate(1142%) hue-rotate(201deg) brightness(95%) contrast(86%)";
};

Purpose:

  • Converts hex colors to CSS filter values for SVG colorization
  • Handles two main states:
    • Inactive (#6774b6): Blue tint for menu items not currently active
    • Active (#ffffff): White for selected/hovered menu items
  • Applies filter via filter CSS property on <img> tags

Supported Colors:

HexUsagePurpose
#6774b6Inactive iconsPrimary color for menu items not selected
#ffffffActive iconsWhite for selected/hovered menu items
OtherFallbackUses inactive color filter

2. Custom SVG Icon Component

Component: CustomSVGIcon

const CustomSVGIcon = ({ src, alt, style, ...props }) => {
// Applica il filtro solo se abbiamo un colore valido
const filter = style?.color ? hexToFilter(style.color) : "none";

return (
<img
src={src}
alt={alt}
style={{
width: style?.fontSize || "1.75rem",
height: style?.fontSize || "1.75rem",
filter: filter,
transition: "filter 0.3s ease",
// Rimuovi il color dal style per evitare conflitti
...Object.fromEntries(
Object.entries(style || {}).filter(
([key]) => key !== "color"
)
),
}}
{...props}
/>
);
};

Props:

  • src (string) — SVG image path
  • alt (string) — Alt text for accessibility
  • style (object) — Style object with fontSize and color
  • ...props — Any additional HTML attributes

Features:

  • Wraps SVG files in <img> tags
  • Applies hexToFilter based on style.color
  • Smooth filter transition (0.3s ease)
  • Strips color from final style to avoid conflicts

State Management

Local Component State

const [messageApi, contextHolder] = message.useMessage(); // Ant message API
const [isLogoutModalVisible, setIsLogoutModalVisible] = useState(false);
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
const [openKeys, setOpenKeys] = useState([]); // Submenu open/close state
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [companyName, setCompanyName] = useState("");
const [mappedRole, setMappedRole] = useState("");
const [analyticsGatewaysVisible, setAnalyticsGatewaysVisible] = useState(false);
const [activeMenuKey, setActiveMenuKey] = useState("home");

Redux Integration

// Notification counts from Redux
const frameValidationNotifCount = useSelector(
(state) => state.notifications.frameValidationCount
);
const ordersNotifCount = useSelector(
(state) => state.notifications.ordersCount
);
const teamsNotifCount = useSelector((state) => state.notifications.teamsCount);

// Sidebar state from Redux
const collapsed = useSelector((state) => state.sidebar?.collapsed);

// Auth permissions from Redux
const userPermissions = useSelector((state) => state.auth?.permissions);
const role = userPermissions?.role || secureGetItem("role");
const email = userPermissions?.email || secureGetItem("email");

// Dispatch actions
const dispatch = useDispatch();
dispatch(setSidebarState(true)); // Collapse sidebar
dispatch(setSidebarBroken(true)); // Set mobile flag
dispatch(fetchNotifications(email)); // Load notifications

Theme & Color Configuration

Color Tokens

const { token } = theme.useToken(); // Ant Design theme tokens

const iconSize = "clamp(1.75rem, 2vw, 2rem)";
const textActiveColor = token.colorTextWhite; // #ffffff
const textColor = primaryShades.primary400; // #6774b6
const backgroundColor = token.colorBgBase; // Base bg
const backgroundColorActive = variantTokens.accent; // Accent color
const headerTextColor = primaryShades.primary700; // Darker blue
const badgeBackgroundColor = textColor; // #6774b6
const badgeIconColor = token.colorTextWhite; // #ffffff
const badgeBgColor = statusTokens.error; // Red for notifications

Computed Colors

Hover Color Calculation:

const backgroundColorActiveHover = useMemo(() => {
// Controlliamo se backgroundColorActive è un valore esadecimale
if (backgroundColorActive.startsWith("#")) {
// Convertiamo da HEX a RGB
const r = parseInt(backgroundColorActive.slice(1, 3), 16);
const g = parseInt(backgroundColorActive.slice(3, 5), 16);
const b = parseInt(backgroundColorActive.slice(5, 7), 16);

// Schiarisci leggermente i valori RGB (max 255)
const lightenFactor = 20;
const newR = Math.min(r + lightenFactor, 255);
const newG = Math.min(g + lightenFactor, 255);
const newB = Math.min(b + lightenFactor, 255);

// Riconverti in HEX
return `#${newR.toString(16).padStart(2, "0")}${newG
.toString(16)
.padStart(2, "0")}${newB.toString(16).padStart(2, "0")}`;
}

// Se non è un valore esadecimale, restituisci un valore di fallback
return "rgba(75, 85, 130, 0.9)";
}, [backgroundColorActive]);

Process:

  1. Convert active background color from HEX to RGB
  2. Add lightenFactor (20) to each component
  3. Clamp to max 255
  4. Convert back to HEX
  5. Result: Slightly lighter version for hover state

User Profile Loading

Effect: Fetch User Profile Data

useEffect(() => {
const fetchUserProfile = async () => {
if (!email) return;
try {
// 1. Get profile document
const profileRef = doc(db, "Profiles", email);
const profileSnap = await getDoc(profileRef);

if (!profileSnap.exists()) return;

const profileData = profileSnap.data();
const serverRole = profileData.role || secureGetItem("role") || "";

// 2. Extract name
setFirstName(profileData.firstName || "");
setLastName(profileData.lastName || "");

// 3. Get company name and check if main profile
let companyName = "";
let isMainProfile = false;

if (profileData.clientRef) {
const clientDoc = await getDoc(
doc(db, "Client", profileData.clientRef)
);
if (clientDoc.exists()) {
const clientData = clientDoc.data();
companyName =
clientData.companyName || profileData.clientRef;

// Check if current email is in mainProfileList
const mainProfileList = clientData.mainProfileList || [];
if (
serverRole === "Cliente" &&
mainProfileList.includes(email.toLowerCase())
) {
isMainProfile = true;
}
}
}

// 4. Map role
const mappedRoleValue = mapRole(serverRole, isMainProfile);

setCompanyName(companyName);
setMappedRole(mappedRoleValue);
} catch (error) {
console.error("Error fetching user profile:", error);
}
};

fetchUserProfile();
}, [email]);

Role Mapping:

const mapRole = (serverRole, isMainProfile = false) => {
switch (serverRole) {
case "Admin":
return "Super Admin";
case "Cliente":
// Cliente è sempre Member, Client Admin è determinato dalla mainProfileList
return isMainProfile ? "Admin" : "Member";
case "Modellista":
return "Modeller";
case "ModellerSupervisor":
return "Modeller Supervisor";
case "Guest":
return "Guest";
default:
return "Member";
}
};

Analytics Gateway Access Check

Effect: Determine Analytics Menu Visibility

useEffect(() => {
const checkAnalyticsAccess = async () => {
if (!email) return;
try {
// 1. Fetch user profile
const profileRef = doc(db, "Profiles", email.toLowerCase());
const profileSnap = await getDoc(profileRef);

if (!profileSnap.exists()) return;

const profileData = profileSnap.data();
const catalogsList = profileData.list_catalogues;

if (!Array.isArray(catalogsList) || catalogsList.length === 0)
return;

// 2. Query catalogues to find Gateway services
const brandQuery = query(
collection(db, "Catalogues"),
where("__name__", "in", catalogsList)
);

const brandSnapshot = await getDocs(brandQuery);

// 3. Check if any catalogue has isGatewayCatalog: true
let found = false;
brandSnapshot.forEach((doc) => {
const brandData = doc.data();
if (brandData.isGatewayCatalog === true) {
found = true;
}
});

setAnalyticsGatewaysVisible(found);
} catch (error) {
console.error(
"[DEBUG] Errore nel controllo di Analytics Gateway:",
error
);
}
};

checkAnalyticsAccess();
}, [email]);

Logic:

  1. Fetch user's list_catalogues from Profiles
  2. Query Catalogues collection for those IDs
  3. Check if any has isGatewayCatalog: true
  4. Set analyticsGatewaysVisible accordingly
  5. If true, Analytics → Gateways menu item is enabled

Main Menu Structure (useMemo):

const menuItems = useMemo(() => {
const items = [];

// 1) Dashboard - sempre visibile per tutti
items.push({
key: "home",
icon: <DashboardSharpIcon style={{ fontSize: iconSize, color: textColor }} />,
label: <span className="menu-label-text" style={{ color: textColor }}>Dashboard</span>,
});

// Logica per ruoli specifici
if (role?.toLowerCase() === "modellista") {
// For Modeller: Dashboard, ARShades Library, My catalogues, Frame Validation
items.push({
key: "ars-catalog",
icon: <CustomSVGIcon src={ARShadesLibraryIcon} alt="ARShades Library" style={{ fontSize: iconSize, color: textColor }} />,
label: <span className="menu-label-text" style={{ color: textColor }}>ARShades Library</span>,
});
items.push({
key: "my-catalogues",
icon: <CustomSVGIcon src={ARShadesCataloguesIcon} alt="My catalogues" style={{ fontSize: iconSize, color: textColor }} />,
label: <span className="menu-label-text" style={{ color: textColor }}>My catalogues</span>,
});
items.push({
key: "framevalidation",
icon: (
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
<CheckCircleIcon style={{ fontSize: iconSize, color: textColor }} />
{collapsed && <CustomBadge count={frameValidationNotifCount} type="frame" />}
</div>
),
label: (
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', width: '100%' }}>
<span className="menu-label-text" style={{ color: textColor }}>Frame Validation</span>
{!collapsed && <CustomBadge count={frameValidationNotifCount} type="frame" />}
</div>
),
});
return items;
}

if (role?.toLowerCase() === "modellersupervisor") {
// Same as Modeller
items.push({...});
return items;
}

// Menu completo per altri ruoli (Super Admin, Admin, Member, etc.)
// 2) ARShades Library
items.push({
key: "ars-catalog",
icon: <CustomSVGIcon src={ARShadesLibraryIcon} alt="ARShades Library" style={{ fontSize: iconSize, color: textColor }} />,
label: <span className="menu-label-text" style={{ color: textColor }}>ARShades Library</span>,
});

// 3) My catalogues
items.push({
key: "my-catalogues",
icon: <CustomSVGIcon src={ARShadesCataloguesIcon} alt="My catalogues" style={{ fontSize: iconSize, color: textColor }} />,
label: <span className="menu-label-text" style={{ color: textColor }}>My catalogues</span>,
});

// 5) Analytics - disponibile per Super Admin, Admin, Member
if (role?.toLowerCase() === "admin" || role?.toLowerCase() === "cliente" || (role?.toLowerCase() !== "modellista" && role?.toLowerCase() !== "modellersupervisor")) {
if (analyticsGatewaysVisible) {
// Both VTO and Gateways available
items.push({
key: "analytics",
icon: <BarChartIcon style={{ fontSize: iconSize, color: textColor }} />,
label: <span className="menu-label-text" style={{ color: textColor }}>Analytics</span>,
children: [
{ key: "analytics-vtos", label: <span className="menu-label-text" style={{ color: textColor }}>VTO services</span> },
{ key: "analytics-gateways", label: <span className="menu-label-text" style={{ color: textColor }}>Gateways</span> },
],
});
} else {
// Only VTO available, Gateways disabled
items.push({
key: "analytics",
icon: <BarChartIcon style={{ fontSize: iconSize, color: textColor }} />,
label: <span className="menu-label-text" style={{ color: textColor }}>Analytics</span>,
children: [
{ key: "analytics-vtos", label: <span className="menu-label-text" style={{ color: textColor }}>VTO services</span> },
{
key: "analytics-gateways",
label: <span className="menu-label-text" style={{ color: textColor, opacity: 0.5 }}>Gateways</span>,
disabled: true
},
],
});
}

// 6) Subscriptions
items.push({
key: "subscriptions",
icon: <CustomSVGIcon src={ARShadesSubscriptionsIcon} alt="Subscriptions" style={{ fontSize: iconSize, color: textColor }} />,
label: <span className="menu-label-text" style={{ color: textColor }}>Subscriptions</span>,
});

// 7) Orders - con badge
items.push({
key: "orderpage",
icon: (
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
<CustomSVGIcon src={ARShadesOrdersIcon} alt="Orders" style={{ fontSize: iconSize, color: textColor }} />
{collapsed && <CustomBadge count={ordersNotifCount} type="order" />}
</div>
),
label: (
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', width: '100%' }}>
<span className="menu-label-text" style={{ color: textColor }}>Orders</span>
{!collapsed && <CustomBadge count={ordersNotifCount} type="order" />}
</div>
),
});
}

// 8) Frame Validation - sempre visibile con badge
items.push({
key: "framevalidation",
icon: (
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
<CheckCircleIcon style={{ fontSize: iconSize, color: textColor }} />
{collapsed && <CustomBadge count={frameValidationNotifCount} type="frame" />}
</div>
),
label: (
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', width: '100%' }}>
<span className="menu-label-text" style={{ color: textColor }}>Frame Validation</span>
{!collapsed && <CustomBadge count={frameValidationNotifCount} type="frame" />}
</div>
),
});

// 9) Components (Admin only)
if (role?.toLowerCase() === "admin") {
items.push({
key: "components",
icon: <CodeIcon style={{ fontSize: iconSize, color: textColor }} />,
label: <span className="menu-label-text" style={{ color: textColor }}>Components</span>,
});
}

// 10) Admin (Admin only)
if (role?.toLowerCase() === "admin") {
items.push({
key: "admin",
icon: <StorageIcon style={{ fontSize: iconSize, color: textColor }} />,
label: <span className="menu-label-text" style={{ color: textColor }}>Admin</span>,
});
}

return items;
}, [frameValidationNotifCount, ordersNotifCount, collapsed, role, analyticsGatewaysVisible, textColor, iconSize]);
RoleMenu Items
ModellerDashboard, ARShades Library, My catalogues, Frame Validation
ModellerSupervisorDashboard, ARShades Library, My catalogues, Frame Validation
Super AdminDashboard, ARShades Library, My catalogues, Analytics (VTO + Gateways), Subscriptions, Orders, Frame Validation, Components, Admin
AdminDashboard, ARShades Library, My catalogues, Analytics (VTO + Gateways), Subscriptions, Orders, Frame Validation
MemberDashboard, ARShades Library, My catalogues, Analytics (VTO + Gateways), Subscriptions, Orders, Frame Validation

Special Cases:

  • Analytics Gateways — Only visible if user has isGatewayCatalog: true in catalogues
  • Notification Badges — Frame Validation, Orders, Teams show counts when > 0
  • Disabled Items — Gateways submenu disabled if not available (grayed out)

Bottom Menu Items

const bottomMenuItems = useMemo(
() => [
{
key: "profile",
icon: (
<div
style={{
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
}}
>
<SettingsIcon
style={{ fontSize: iconSize, color: textColor }}
/>
{collapsed && (
<CustomBadge count={teamsNotifCount} type="teams" />
)}
</div>
),
label: (
<div
style={{
position: "relative",
display: "flex",
alignItems: "center",
width: "100%",
}}
>
<span
className="menu-label-text"
style={{ color: textColor }}
>
Profile
</span>
{!collapsed && (
<CustomBadge count={teamsNotifCount} type="teams" />
)}
</div>
),
},
{
key: "logout",
icon: (
<LogoutIcon style={{ fontSize: iconSize, color: textColor }} />
),
label: (
<span className="menu-label-text" style={{ color: textColor }}>
Logout
</span>
),
onClick: () => setIsLogoutModalVisible(true),
},
],
[iconSize, textColor, collapsed, teamsNotifCount]
);

URL-Based Menu Synchronization

Effect: Update menu state based on current URL

useEffect(() => {
const path = location.pathname === "/" ? "/home" : location.pathname;

// Definisci le relazioni parent-child per i menu
const getMenuKeyFromPath = (pathname) => {
// ARSCatalog parent-child relationships
if (pathname.startsWith("/ars-catalog")) {
return "ars-catalog";
}

// My Catalogues parent-child relationships
if (pathname.startsWith("/my-catalogues")) {
return "my-catalogues";
}

// Analytics parent-child relationships
if (pathname === "/analytics" || pathname === "/analytics-vtos") {
return "analytics-vtos";
}
if (pathname === "/analytics-gateways") {
return "analytics-gateways";
}

// Frame Validation parent-child relationships
if (pathname.startsWith("/framevalidation")) {
return "framevalidation";
}

// Per tutti gli altri percorsi, usa il primo segmento dopo lo slash
const segments = pathname.split("/").filter(Boolean);
return segments[0] || "home";
};

const menuKey = getMenuKeyFromPath(path);
setActiveMenuKey(menuKey);

const findParentAndChild = (items, path) => {
for (const item of items) {
if (`/${item.key}` === path)
return { parentKey: item, childKey: null };
if (item.children) {
const childItem = item.children.find(
(child) => `/${child.key}` === path
);
if (childItem) return { parentKey: item, childKey: childItem };
}
}
return null;
};

const selectedItem = findParentAndChild(menuItems, path);

if (selectedItem) {
setOpenKeys([selectedItem.parentKey.key]);
}

if (location.pathname === "/") {
navigate("/home");
}

localStorage.setItem("lastVisitedPage", location.pathname);
}, [location, menuItems, bottomMenuItems, navigate]);

Path Mapping:

'/home''home'
'/ars-catalog/*''ars-catalog'
'/my-catalogues/*''my-catalogues'
'/analytics' or
'/analytics-vtos''analytics-vtos'
'/analytics-gateways''analytics-gateways'
'/framevalidation/*''framevalidation'
'/profile''profile'
'/subscriptions''subscriptions'
'/orderpage''orderpage'
'/' → redirects to '/home'

const handleMenuClick = ({ key }) => {
// Mobile: collapse sidebar after selection
if (isMobile) {
dispatch(setSidebarState(true));
}

// Find parent and child relationship
const findParentAndChild = (items, key) => {
for (const item of items) {
if (item.key === key)
return { parentKey: item.key, childKey: null };
if (item.children) {
const childItem = item.children.find(
(child) => child.key === key
);
if (childItem) {
// Check if child is disabled
if (childItem.disabled) {
return null; // Don't navigate if disabled
}
return { parentKey: item.key, childKey: childItem.key };
}
}
}
return null;
};

const selectedItem =
findParentAndChild(menuItems, key) ||
bottomMenuItems.find((item) => item.key === key);

if (selectedItem) {
// Handle custom onClick (e.g., logout)
if (selectedItem.onClick) {
selectedItem.onClick();
return;
}

// Set active menu key
setActiveMenuKey(key);
setOpenKeys([selectedItem.parentKey || key]);

// Navigate
const path = selectedItem.path || `/${key}`;
navigate(path);
}
};

Responsive Design & Mobile Handling

Mobile Detection & Auto-Collapse

// Determina se è un dispositivo mobile
const isMobile = useMediaQuery({ query: "(max-width: 768px)" });

// Gestisce il collasso automatico in base alla dimensione dello schermo
useEffect(() => {
if (isMobile) {
dispatch(setSidebarState(true)); // Collapse sidebar
dispatch(setSidebarBroken(true)); // Set mobile flag
} else {
dispatch(setSidebarBroken(false)); // Set desktop flag
}
}, [isMobile, dispatch]);

Behavior:

  • Desktop (> 768px): Sidebar expanded by default, can collapse on hover
  • Mobile (≤ 768px): Sidebar collapsed by default, expands to full width when opened

const handleMouseEnter = () => {
if (!isMobile) {
dispatch(setSidebarState(false)); // Expand on hover
}
};

const handleMouseLeave = () => {
if (!isMobile) {
dispatch(setSidebarState(true)); // Collapse on leave
}
};

Header Component

Header Structure

<Header
style={{
width: "100%",
background: backgroundColor,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0 clamp(0.5rem, 2vw, 1rem)",
position: "fixed",
top: 0,
left: 0,
right: 0,
zIndex: 1001,
height: "clamp(4rem, 6vw, 5rem)",
transition: "all 0.4s ease",
}}
>

Properties:

  • Position: Fixed at top with z-index 1001
  • Size: Responsive height using clamp (4rem to 5rem)
  • Spacing: Responsive padding using clamp (0.5rem to 1rem)
  • Layout: Flex with space-between for left/right distribution

Logo & Title

<div style={{
display: "flex",
alignItems: "center",
height: "clamp(4rem, 6vw, 5rem)"
}}>
<img
src={logo}
alt="Logo"
style={{
width: "clamp(3rem, 4vw, 3.75rem)",
height: "clamp(3rem, 4vw, 3.75rem)",
objectFit: "contain"
}}
/>
</div>

<div style={{
pointerEvents: "none",
marginLeft: "clamp(0.5rem, 1vw, 1rem)"
}}>
<h1 style={{
margin: 0,
fontSize: "clamp(1rem, 3vw, 1.5rem)",
fontWeight: "700",
letterSpacing: "0.063rem",
color: textColor,
textTransform: "uppercase"
}}>
ARShades Studio
</h1>
</div>

User Information Display

{
/* Nascoste quando si è nella Home */
}
{
activeMenuKey !== "home" && (
<div style={{ textAlign: "right", lineHeight: "1.2" }}>
<p
style={{
margin: 0,
fontSize: "clamp(0.75rem, 1vw, 1rem)",
color: headerTextColor,
}}
>
{greeting},{" "}
<span style={{ fontWeight: "bold", color: headerTextColor }}>
{firstName} {lastName}
</span>
</p>
<p
style={{
fontSize: "clamp(0.75rem, 1vw, 1rem)",
color: headerTextColor,
marginTop: "0.188rem",
marginBottom: 0,
}}
>
You are{" "}
<span style={{ fontWeight: "bold", color: headerTextColor }}>
{mappedRole === "SuperAdmin" ? "Super Admin" : mappedRole}
</span>{" "}
for{" "}
<span style={{ fontWeight: "bold", color: headerTextColor }}>
{mappedRole === "SuperAdmin" ||
mappedRole === "Modeller" ||
mappedRole === "Modeller Supervisor"
? "Spaarkly"
: companyName}
</span>
</p>
</div>
);
}

Visibility:

  • Hidden when activeMenuKey === "home"
  • Shows: Greeting + Name + Role + Company

Time-Based Greeting:

  • "Good morning" (0:00 - 11:59)
  • "Good afternoon" (12:00 - 17:59)
  • "Good evening" (18:00 - 23:59)
  • "Good night" (0:00 - 5:59)

Help Button

<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
width: "clamp(1.5rem, 2vw, 1.875rem)",
height: "clamp(1.5rem, 2vw, 1.875rem)",
backgroundColor: badgeBackgroundColor,
borderRadius: "0.25rem",
transition: "background-color 0.3s ease",
}}
onClick={() => setIsSupportModalVisible(true)}
>
<HelpOutlineIcon
style={{
fontSize: "clamp(1.125rem, 1.5vw, 1.375rem)",
color: badgeIconColor,
}}
/>
</div>

Notifications Dropdown

<NotificationsDropdown
badgeBackgroundColor={badgeBackgroundColor}
badgeIconColor={badgeIconColor}
onNotificationCountChange={(count) => {
// Ricarica i conteggi categorizzati quando le notifiche cambiano
if (email) {
dispatch(fetchNotifications(email));
}
}}
/>

<motion.div
className={styles["sidebar-animated"]}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
animate={{
width: collapsed ? "clamp(4rem, 6vw, 5rem)" : "clamp(20rem, 25vw, 23rem)"
}}
transition={{
duration: 0.3,
ease: [0.4, 0, 0.2, 1]
}}
style={{
"--background-color-active": backgroundColorActive,
"--background-color-active-hover": backgroundColorActiveHover,
"--text-active-color": textActiveColor,
top: "clamp(4rem, 6vw, 5rem)",
height: `calc(100% - clamp(4rem, 6vw, 5rem))`
}}
>

CSS Variables:

  • --background-color-active — Active menu item background
  • --background-color-active-hover — Hover state background (lightened)
  • --text-active-color — Active menu item text color

Animation:

  • Duration: 0.3s
  • Easing: [0.4, 0, 0.2, 1] (cubic-bezier)
  • Width states:
    • Collapsed: "clamp(4rem, 6vw, 5rem)" (icon-only)
    • Expanded: "clamp(20rem, 25vw, 23rem)" (full sidebar)

Regular Menu Items Rendering

<div
key={item.key}
className={`${styles["menu-item"]} ${
isItemActive ? styles["menu-item-active"] : ""
}`}
onClick={() => handleMenuClick({ key: item.key })}
>
<div className={styles["icon-container"]}>
{/* Icon rendering with color update */}
{React.cloneElement(item.icon, {
style: {
...item.icon.props.style,
color: isItemActive ? textActiveColor : textColor,
fontSize: iconSize,
},
})}
</div>

{/* Label with fade animation */}
<AnimatePresence mode="wait">
{!collapsed && (
<motion.div
className={styles["menu-item-label"]}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.2,
delay: 0.1,
ease: [0.4, 0, 0.2, 1],
}}
style={{
color: isItemActive ? textActiveColor : textColor,
}}
>
{/* Label content */}
</motion.div>
)}
</AnimatePresence>
</div>

Animations:

  • Label: Fade + slide in (opacity 0→1, x -20→0)
  • Timing: 0.2s duration, 0.1s delay
  • Only shows when sidebar is expanded

<div key={item.key}>
{/* Submenu Title */}
<div
className={`${styles["menu-item"]} ${styles["submenu-title"]} ...`}
onClick={() => {
const newOpenKeys = isSubMenuOpen
? openKeys.filter((key) => key !== item.key)
: [...openKeys, item.key];
setOpenKeys(newOpenKeys);
}}
>
{/* Icon and Label */}
</div>

{/* Submenu Items Container */}
<AnimatePresence mode="wait">
{!collapsed && isSubMenuOpen && (
<motion.div
className={styles["submenu-items"]}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{
duration: 0.2,
delay: 0.1,
ease: [0.4, 0, 0.2, 1],
}}
>
{item.children.map((child) => (
<div
key={child.key}
className={`${styles["submenu-item"]} ${
isChildActive ? styles["menu-item-active"] : ""
} ${isDisabled ? styles["menu-item-disabled"] : ""}`}
onClick={() =>
!isDisabled && handleMenuClick({ key: child.key })
}
style={{
cursor: isDisabled ? "not-allowed" : "pointer",
opacity: isDisabled ? 0.5 : 1,
}}
>
<div className={styles["submenu-item-content"]}>
{/* Child label */}
</div>
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>

Submenu Features:

  • Expansion: Clicks toggle openKeys state
  • Animation: Fade + height collapse/expand
  • Disabled state: Shows 50% opacity, prevents click
  • Arrow indicator: Rotates when expanded/collapsed

Custom Badge Component

const CustomBadge = ({ count, type = "frame" }) => {
if (!count || count <= 0) return null;

// Determina la classe CSS in base allo stato del menu e al tipo
const badgeClass = collapsed
? styles["badge-collapsed"]
: type === "frame"
? styles["badge-expanded-frame"]
: type === "order"
? styles["badge-expanded-order"]
: styles["badge-expanded-order"];

return (
<div
className={badgeClass}
style={{
backgroundColor: badgeBgColor,
color: badgeIconColor,
}}
>
{count > 10 ? "10+" : count}
</div>
);
};

Features:

  • Shows only if count > 0
  • Different positioning when collapsed vs expanded
  • Different CSS classes for different badge types (frame, order, teams)
  • Displays "10+" for counts above 10
  • Red background (statusTokens.error) with white text

Main Content Area

<Content
id="page-content-wrapper"
style={{
marginLeft: "5rem",
marginRight: "0",
marginTop: "clamp(4rem, 6vw, 5rem)",
marginBottom: "0",
padding: "clamp(1rem, 2vw, 1.5rem)",
minHeight: "17.5rem",
height: `calc(100vh - clamp(4rem, 6vw, 5rem))`,
overflowY: "auto",
}}
>
<Outlet />
</Content>

Layout:

  • marginLeft: 5rem (always space for collapsed sidebar minimum)
  • marginTop: Matches header height (clamp value)
  • Padding: Responsive spacing (clamp)
  • Height: Fills remaining viewport minus header
  • Overflow: Auto for scrollable content
  • Outlet: React Router outlet for page rendering

Logout Flow

Logout Modal

<Modal
title="Confirm Logout"
open={isLogoutModalVisible}
onOk={() => {
setIsLogoutModalVisible(false);
handleLogout();
}}
onCancel={() => setIsLogoutModalVisible(false)}
okText="Logout"
cancelText="Cancel"
centered
>
<p>Are you sure you want to logout?</p>
</Modal>

Logout Handler

const handleLogout = async () => {
try {
await signOut(auth);
messageApi.success("Logout successful");
navigate("/login");
} catch (error) {
messageApi.error("Error logging out");
console.error(error);
}
};

Flow:

  1. User clicks "Logout" in bottom menu
  2. Confirmation modal appears
  3. On confirm: Firebase signOut + navigate to /login
  4. On cancel: Modal closes, user stays on current page

Redux Slices Used

SliceActionsPurpose
sidebarsetSidebarState, setSidebarBrokenManage sidebar collapsed state and mobile flag
notificationsfetchNotificationsLoad notification counts (frameValidationCount, ordersCount, teamsCount)
authRedux auth stateStore user permissions (role, email)

Components:

  • /src/components/DashboardLayout.js (main layout)
  • /src/components/SupportModal.js (support help modal)
  • /src/frameValidation/modal/NotificationsDropdown.js (notifications dropdown)

Styles:

  • /src/styles/Dashboard/Dashboard.module.css (CSS modules for layout)

Icons (SVG):

  • /src/assets/icon/ARShades Library icon.svg
  • /src/assets/icon/ARShades Catalogues icon.svg
  • /src/assets/icon/ARShades Subscriptions icon.svg
  • /src/assets/icon/ARShades Orders icon2.png

Redux:

  • /src/redux/sidebar.js (sidebar state)
  • /src/redux/notifications/actions.js (notification actions)
  • /src/redux/authentication/authSlice.js (auth state)

Theme:

  • /src/theme/theme.js (primaryShades, statusTokens, variantTokens)

Firebase:

  • /src/data/base.js (Firebase config: db, auth)

Utilities:

  • /src/data/utils.js (secureGetItem, secureSetItem)

Router:

  • Main route config must include <DashboardLayout> with <Outlet />
  • Child routes render inside the Content area

Responsive Breakpoints

BreakpointDeviceSidebar BehaviorHeader Behavior
> 768pxDesktopExpanded by default, collapsible on hoverFull width
≤ 768pxMobile/TabletCollapsed by default, expands on clickFull width, compact

Performance Optimization

useMemo Dependencies

  • menuItems — Recalculates when: frameValidationNotifCount, ordersNotifCount, collapsed, role, analyticsGatewaysVisible, textColor, iconSize
  • bottomMenuItems — Recalculates when: iconSize, textColor, collapsed, teamsNotifCount
  • backgroundColorActiveHover — Recalculates when: backgroundColorActive

Key Optimizations

  • Memoized menu structures prevent unnecessary re-renders
  • Lazy profile data loading (only fetches on component mount and email change)
  • Non-blocking analytics gateway check (separate effect)
  • Efficient notification badge rendering (only shows if count > 0)