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
filterCSS property on<img>tags
Supported Colors:
| Hex | Usage | Purpose |
|---|---|---|
#6774b6 | Inactive icons | Primary color for menu items not selected |
#ffffff | Active icons | White for selected/hovered menu items |
| Other | Fallback | Uses 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 pathalt(string) — Alt text for accessibilitystyle(object) — Style object withfontSizeandcolor...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
colorfrom 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:
- Convert active background color from HEX to RGB
- Add lightenFactor (20) to each component
- Clamp to max 255
- Convert back to HEX
- 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:
- Fetch user's
list_cataloguesfrom Profiles - Query Catalogues collection for those IDs
- Check if any has
isGatewayCatalog: true - Set
analyticsGatewaysVisibleaccordingly - If true, Analytics → Gateways menu item is enabled
Menu System & Role-Based Access
Menu Items Generation
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]);
Menu Items by Role
| Role | Menu Items |
|---|---|
| Modeller | Dashboard, ARShades Library, My catalogues, Frame Validation |
| ModellerSupervisor | Dashboard, ARShades Library, My catalogues, Frame Validation |
| Super Admin | Dashboard, ARShades Library, My catalogues, Analytics (VTO + Gateways), Subscriptions, Orders, Frame Validation, Components, Admin |
| Admin | Dashboard, ARShades Library, My catalogues, Analytics (VTO + Gateways), Subscriptions, Orders, Frame Validation |
| Member | Dashboard, ARShades Library, My catalogues, Analytics (VTO + Gateways), Subscriptions, Orders, Frame Validation |
Special Cases:
- Analytics Gateways — Only visible if user has
isGatewayCatalog: truein 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]
);
Navigation & Routing
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'
Menu Click Handler
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
Sidebar Hover Handlers
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));
}
}}
/>
Sidebar Component
Sidebar Animation & Structure
<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
Submenu Items Rendering
<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
openKeysstate - 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:
- User clicks "Logout" in bottom menu
- Confirmation modal appears
- On confirm: Firebase signOut + navigate to /login
- On cancel: Modal closes, user stays on current page
Redux Slices Used
| Slice | Actions | Purpose |
|---|---|---|
sidebar | setSidebarState, setSidebarBroken | Manage sidebar collapsed state and mobile flag |
notifications | fetchNotifications | Load notification counts (frameValidationCount, ordersCount, teamsCount) |
auth | Redux auth state | Store user permissions (role, email) |
Related Files & Architecture
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
| Breakpoint | Device | Sidebar Behavior | Header Behavior |
|---|---|---|---|
| > 768px | Desktop | Expanded by default, collapsible on hover | Full width |
| ≤ 768px | Mobile/Tablet | Collapsed by default, expands on click | Full width, compact |
Performance Optimization
useMemo Dependencies
menuItems— Recalculates when:frameValidationNotifCount,ordersNotifCount,collapsed,role,analyticsGatewaysVisible,textColor,iconSizebottomMenuItems— Recalculates when:iconSize,textColor,collapsed,teamsNotifCountbackgroundColorActiveHover— 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)