mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 23:00:57 +00:00
703 lines
28 KiB
TypeScript
703 lines
28 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { motion, AnimatePresence, Variants } from "framer-motion";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useAuthUser } from "@/shared/hooks/useAuthUser";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/shared/common/ui/select";
|
|
import { toast } from "sonner";
|
|
import {
|
|
ChevronDown,
|
|
FileText,
|
|
Menu,
|
|
X,
|
|
User,
|
|
UserPlus as UserPen,
|
|
Key,
|
|
LogOut,
|
|
Cog,
|
|
Bolt,
|
|
Globe,
|
|
Moon,
|
|
Sun,
|
|
} from "lucide-react";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/shared/common/ui/dropdown-menu";
|
|
import { Button } from "@/shared/common/ui/button";
|
|
import { useTheme } from "@/user-management/web-Management/hooks/useTheme";
|
|
import { useLocalizedName } from "@/shared/common/localizedName";
|
|
import { useModules } from "@/user-management/web-Management/hooks/useModules";
|
|
import { useDarkMode } from "@/shared/hooks/useDarkMode";
|
|
import {
|
|
useTenantConfig,
|
|
resolveModuleConfig,
|
|
} from "@/layout/components/TenantConfig";
|
|
import {
|
|
UI_LANGUAGE_OPTIONS,
|
|
resolveUiLanguage,
|
|
} from "@/shared/i18n/uiLanguages";
|
|
|
|
interface NavItem {
|
|
id: string;
|
|
label?: string;
|
|
path?: string;
|
|
link?: string;
|
|
requiresCompleteRegistration?: true;
|
|
children?: NavItem[];
|
|
name?: {
|
|
am: string;
|
|
en: string;
|
|
};
|
|
description?: string;
|
|
}
|
|
|
|
const languageOptions = UI_LANGUAGE_OPTIONS;
|
|
|
|
const Header = () => {
|
|
const [activeTab, setActiveTab] = useState("overview");
|
|
const [scrolled, setScrolled] = useState(false);
|
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
|
const navigate = useNavigate();
|
|
const { items: item } = useTheme();
|
|
const { userDetails, logout } = useAuthUser();
|
|
const { t, i18n } = useTranslation();
|
|
const currentLanguage = resolveUiLanguage(i18n.language);
|
|
const { config: tenantConfig } = useTenantConfig();
|
|
const moduleConfig = resolveModuleConfig(tenantConfig);
|
|
const localizedName = useLocalizedName();
|
|
const hasCompletedRegistration = userDetails?.hasFinishedRegistration;
|
|
const baseParams = {
|
|
isActive: true,
|
|
};
|
|
const { items: modules } = useModules(baseParams);
|
|
const fullName = localizedName(userDetails?.name) || t("header.user");
|
|
const splittedName = fullName.trim().split(" ");
|
|
const initials =
|
|
splittedName.length === 1
|
|
? splittedName[0][0]
|
|
: `${splittedName[0][0]}${splittedName[1][0]}`;
|
|
|
|
const changeLanguage = (lng: string) => {
|
|
i18n.changeLanguage(lng);
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
logout();
|
|
};
|
|
|
|
const { isDarkMode, toggleDarkMode } = useDarkMode();
|
|
|
|
useEffect(() => {
|
|
if (!localStorage.getItem("i18nextLng")) {
|
|
i18n.changeLanguage("am");
|
|
localStorage.setItem("i18nextLng", "am");
|
|
}
|
|
}, [i18n]);
|
|
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
setScrolled(window.scrollY > 10);
|
|
};
|
|
window.addEventListener("scroll", handleScroll);
|
|
return () => window.removeEventListener("scroll", handleScroll);
|
|
}, []);
|
|
const isSuperAdmin = userDetails?.roles?.some(
|
|
(role) => role.key === "super_admin",
|
|
);
|
|
const isOrgAdmin = userDetails?.roles?.some(
|
|
(role) => role.key === "unit_admin" || role.key === "organization_admin",
|
|
);
|
|
const navItems: NavItem[] = [
|
|
{
|
|
id: "Modules",
|
|
label: t("nav.modules"),
|
|
children: [
|
|
...(moduleConfig.recordManagement
|
|
? [
|
|
{
|
|
id: "recordManagement",
|
|
label: t("nav.Record Management"),
|
|
path: "/record-management/dashboard",
|
|
description: t("msg.recordDes"),
|
|
},
|
|
]
|
|
: []),
|
|
...(moduleConfig.performance
|
|
? [
|
|
{
|
|
id: "performanceManagement",
|
|
label: t("nav.PerformanceManagement"),
|
|
path: "/performance-management/plan-years",
|
|
description: t("nav.PerformanceManagement"),
|
|
},
|
|
]
|
|
: []),
|
|
...(moduleConfig.objective
|
|
? [
|
|
{
|
|
id: "objectiveManagement",
|
|
label: t("nav.objectiveManagement"),
|
|
path: "/objective-management/plan-years",
|
|
description: t("nav.objectiveManagement"),
|
|
},
|
|
]
|
|
: []),
|
|
...(moduleConfig.dms
|
|
? [
|
|
{
|
|
id: "documentManagement",
|
|
label: t("nav.DocumentManagement"),
|
|
path: "/dms/dashboard",
|
|
description: t("nav.DocumentManagement"),
|
|
},
|
|
]
|
|
: []),
|
|
...(isOrgAdmin && moduleConfig.siteManagement
|
|
? [
|
|
{
|
|
id: "orgAdmin",
|
|
label: t("nav.admin"),
|
|
path: "/user-management/user_management-dashboard",
|
|
description: t("msg.orgAdminDes"),
|
|
},
|
|
]
|
|
: []),
|
|
...(isSuperAdmin
|
|
? [
|
|
{
|
|
id: "superAdmin",
|
|
label: t("OrganizationAdmin"),
|
|
path: "/user-management/dashboard",
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
},
|
|
{ id: "OfficeHeadMessage", label: t("nav.officeHeadMessage") },
|
|
{ id: "DeputyMessage", label: t("nav.deputyMessage") },
|
|
{
|
|
id: "OrganizationGoal",
|
|
label: t("nav.orgGoal"),
|
|
},
|
|
{ id: "NewsSection", label: t("nav.news") },
|
|
...(modules && modules.length > 0
|
|
? [
|
|
{
|
|
id: "Other",
|
|
label: t("nav.other"),
|
|
children:
|
|
modules?.map((i) => ({
|
|
id: i.id,
|
|
name: i.label,
|
|
link: i.link,
|
|
description: localizedName(i.description),
|
|
})) || [],
|
|
},
|
|
]
|
|
: []),
|
|
];
|
|
const handleNavClick = (item: NavItem) => {
|
|
if (item.requiresCompleteRegistration && !hasCompletedRegistration) {
|
|
toast.error(t("registration.registrationRequired"));
|
|
return;
|
|
}
|
|
|
|
if (item.path) {
|
|
navigate(item.path);
|
|
setMobileMenuOpen(false);
|
|
return;
|
|
}
|
|
|
|
if (item.link) {
|
|
let url = item.link.trim();
|
|
|
|
// 🧹 Fix common typos like "https//:" or "http//:"
|
|
url = url.replace(/^https?\/\/:/i, "https://");
|
|
|
|
// 🌐 If it doesn't start with http/https, add https://
|
|
if (!/^https?:\/\//i.test(url)) {
|
|
url = `https://${url.replace(/^\/+/, "")}`; // remove leading slashes
|
|
}
|
|
|
|
// ✅ Finally open the link safely in a new tab
|
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
setMobileMenuOpen(false);
|
|
}
|
|
};
|
|
|
|
const mobileMenuVariants = {
|
|
hidden: {
|
|
opacity: 0,
|
|
height: 0,
|
|
transition: { duration: 0.3, when: "afterChildren" },
|
|
},
|
|
visible: {
|
|
opacity: 1,
|
|
height: "auto",
|
|
transition: {
|
|
duration: 0.3,
|
|
when: "beforeChildren",
|
|
staggerChildren: 0.08,
|
|
},
|
|
},
|
|
};
|
|
|
|
const mobileItemVariants: Variants = {
|
|
hidden: { x: -20, opacity: 0 },
|
|
visible: {
|
|
x: 0,
|
|
opacity: 1,
|
|
transition: { type: "spring" as const, stiffness: 300, damping: 30 },
|
|
},
|
|
};
|
|
|
|
const servicesVariants = {
|
|
hidden: { opacity: 0, y: -15, scale: 0.92 },
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
scale: 1,
|
|
transition: { duration: 0.25, staggerChildren: 0.06 },
|
|
},
|
|
};
|
|
|
|
const serviceItemVariants = {
|
|
hidden: { opacity: 0, x: -10 },
|
|
visible: { opacity: 1, x: 0 },
|
|
};
|
|
|
|
const getServiceIcon = (id: string, isOrgAdmin: any, isSuperAdmin: any) => {
|
|
switch (id) {
|
|
case "recordManagement":
|
|
return <FileText className="w-4 h-4" />;
|
|
|
|
case "orgAdmin":
|
|
return isOrgAdmin ? <Cog className="w-4 h-4" /> : null;
|
|
case "superAdmin":
|
|
return isSuperAdmin ? <Bolt className="w-4 h-4" /> : null;
|
|
|
|
default:
|
|
return <Globe className="w-4 h-4" />;
|
|
}
|
|
};
|
|
|
|
const getCurrentLanguageDisplay = () => {
|
|
const currentLang = languageOptions.find(
|
|
(lang) => lang.value === currentLanguage,
|
|
);
|
|
return currentLang ? currentLang.label : "English";
|
|
};
|
|
return (
|
|
<>
|
|
<nav
|
|
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
|
scrolled
|
|
? "bg-white/95 dark:bg-gray-900/95 backdrop-blur-md shadow-lg border-b border-gray-100 dark:border-gray-800"
|
|
: "bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm shadow-sm border-b border-gray-50 dark:border-gray-800"
|
|
}`}
|
|
>
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between items-center h-16 md:h-20">
|
|
<div className="flex items-center gap-3 md:gap-4">
|
|
<motion.img
|
|
src={tenantConfig.logo}
|
|
alt={tenantConfig.appName}
|
|
className="h-10 w-15 md:h-17 md:w-25 rounded-lg object-contain cursor-pointer shadow-sm hover:shadow-md transition-shadow"
|
|
whileHover={{ scale: 1.08 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
onClick={() => navigate("/")}
|
|
/>
|
|
</div>
|
|
<div className="hidden md:flex md:gap-2 lg:gap-4 items-center flex-1 ml-8">
|
|
{navItems.map((item) => (
|
|
<div key={item?.id} className="relative group">
|
|
{item?.children ? (
|
|
<div
|
|
className="relative"
|
|
onMouseEnter={() => setOpenDropdown(item.id)}
|
|
onMouseLeave={() => setOpenDropdown(null)}
|
|
>
|
|
<motion.button
|
|
onClick={() =>
|
|
setOpenDropdown(
|
|
openDropdown === item.id ? null : item.id,
|
|
)
|
|
}
|
|
className={`relative inline-flex items-center gap-1.5 px-3 py-2 text-sm lg:text-base font-medium transition-all duration-200 rounded-lg ${
|
|
activeTab === item?.id
|
|
? "text-primary bg-primary/10 dark:bg-primary/20"
|
|
: "text-gray-700 dark:text-gray-300 hover:text-primary hover:bg-gray-50 dark:hover:bg-gray-800"
|
|
}`}
|
|
whileHover={{ y: -2 }}
|
|
>
|
|
{item?.label || localizedName(item?.name)}
|
|
<ChevronDown
|
|
className={`w-4 h-4 transition-transform duration-300 ${
|
|
openDropdown === item.id ? "rotate-180" : ""
|
|
}`}
|
|
/>
|
|
</motion.button>
|
|
|
|
<AnimatePresence>
|
|
{openDropdown === item.id && (
|
|
<motion.div
|
|
initial="hidden"
|
|
animate="visible"
|
|
exit="hidden"
|
|
variants={servicesVariants}
|
|
className="absolute top-full left-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-100 dark:border-gray-800 z-50 overflow-hidden"
|
|
>
|
|
<div className="py-2">
|
|
{item?.children.map((child) => (
|
|
<div
|
|
key={child.id}
|
|
className="group/child relative"
|
|
>
|
|
<motion.button
|
|
variants={serviceItemVariants}
|
|
onClick={() => handleNavClick(child)}
|
|
className="w-full text-left hover:bg-primary/20 dark:hover:bg-primary/30 hover:text-primary transition-all duration-200"
|
|
>
|
|
<div className="flex items-start gap-3 px-4 py-3">
|
|
<span className="text-primary-600 dark:text-primary-400 flex-shrink-0 mt-0.5">
|
|
{getServiceIcon(
|
|
child.id,
|
|
isOrgAdmin,
|
|
isSuperAdmin,
|
|
)}
|
|
</span>
|
|
<div className="flex-1">
|
|
<div className="font-medium text-sm text-gray-900 dark:text-gray-100">
|
|
{child.label ||
|
|
localizedName(child.name)}
|
|
</div>
|
|
{child.description && (
|
|
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 max-h-0 overflow-hidden group-hover/child:max-h-20 transition-all duration-300">
|
|
{child.description}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</motion.button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
) : (
|
|
<motion.button
|
|
onClick={() => {
|
|
setActiveTab(item?.id);
|
|
if (item?.path) {
|
|
handleNavClick(item);
|
|
} else {
|
|
const el = document.getElementById(item?.id);
|
|
if (el) {
|
|
el.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "start",
|
|
});
|
|
}
|
|
}
|
|
}}
|
|
className={`relative inline-flex items-center px-3 py-2 text-sm lg:text-base font-medium transition-all duration-200 rounded-lg ${
|
|
activeTab === item?.id
|
|
? "text-primary bg-primary/10 dark:bg-primary/20"
|
|
: "text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary-400 hover:bg-gray-50 dark:hover:bg-gray-800"
|
|
}`}
|
|
whileHover={{ y: -2 }}
|
|
>
|
|
{item?.label || localizedName(item?.name)}
|
|
{activeTab === item?.id && (
|
|
<motion.div
|
|
layoutId="activeTabIndicator"
|
|
className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"
|
|
transition={{
|
|
type: "spring",
|
|
bounce: 0.2,
|
|
duration: 0.6,
|
|
}}
|
|
/>
|
|
)}
|
|
</motion.button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="hidden md:flex items-center gap-3 lg:gap-4">
|
|
<motion.button
|
|
onClick={toggleDarkMode}
|
|
className="p-2 rounded-lg text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800 transition-colors"
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
aria-label={
|
|
isDarkMode ? t("header.lightMode") : t("header.darkMode")
|
|
}
|
|
>
|
|
{isDarkMode ? (
|
|
<Sun className="w-5 h-5" />
|
|
) : (
|
|
<Moon className="w-5 h-5" />
|
|
)}
|
|
</motion.button>
|
|
|
|
<div className="w-32 lg:w-36">
|
|
<Select
|
|
value={currentLanguage}
|
|
onValueChange={(lng) => changeLanguage(lng)}
|
|
>
|
|
<SelectTrigger className="w-full text-sm rounded-lg border border-gray-200 px-3 py-2 shadow-sm hover:border-primary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all">
|
|
<SelectValue>{getCurrentLanguageDisplay()}</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{languageOptions.map((lang) => (
|
|
<SelectItem key={lang.value} value={lang.value}>
|
|
{lang.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className="flex items-center gap-2 h-10 px-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
aria-label={t("header.userMenu")}
|
|
>
|
|
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-primary-600 text-primary-foreground font-semibold text-sm shadow-md">
|
|
{initials.toUpperCase()}
|
|
</div>
|
|
<div className="hidden sm:flex flex-col items-start">
|
|
<span className="text-xs font-semibold leading-none">
|
|
{localizedName(userDetails?.name) || ""}
|
|
</span>
|
|
<span className="text-[10px] text-gray-500 dark:text-gray-400 leading-none mt-0.5">
|
|
{userDetails?.roles && userDetails.roles.length > 0
|
|
? "User Role"
|
|
: "User"}
|
|
</span>
|
|
</div>
|
|
<ChevronDown className="h-4 w-4 text-gray-500" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
className="w-56 p-2 bg-white dark:bg-gray-900 shadow-xl rounded-xl border border-gray-100 dark:border-gray-800"
|
|
align="end"
|
|
>
|
|
<DropdownMenuItem
|
|
className="flex items-center gap-3 px-3 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-primary-50 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-400 rounded-lg cursor-pointer transition-colors"
|
|
onClick={() => navigate("/profile")}
|
|
>
|
|
<User className="w-4 h-4 text-primary-600 dark:text-primary-400" />
|
|
<span className="font-medium">
|
|
{t("header.viewProfile")}
|
|
</span>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
className="flex items-center gap-3 px-3 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-primary-50 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-400 rounded-lg cursor-pointer transition-colors"
|
|
onClick={() => navigate("/update-profile")}
|
|
>
|
|
<UserPen className="w-4 h-4 text-primary-600 dark:text-primary-400" />
|
|
<span className="font-medium">
|
|
{t("header.editProfile")}
|
|
</span>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem
|
|
className="flex items-center gap-3 px-3 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-primary-50 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-400 rounded-lg cursor-pointer transition-colors"
|
|
onClick={() => navigate("/change-password")}
|
|
>
|
|
<Key className="w-4 h-4 text-primary-600 dark:text-primary-400" />
|
|
<span className="font-medium">
|
|
{t("header.changePassword")}
|
|
</span>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuSeparator className="my-2 h-px bg-gray-100 dark:bg-gray-800" />
|
|
|
|
<DropdownMenuItem
|
|
className="flex items-center gap-3 px-3 py-2.5 text-sm text-red-600 hover:bg-red-50 rounded-lg cursor-pointer transition-colors"
|
|
onClick={handleLogout}
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
<span className="font-medium">{t("header.signOut")}</span>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
<div className="flex items-center md:hidden gap-2">
|
|
<motion.button
|
|
onClick={toggleDarkMode}
|
|
className="p-2 rounded-lg text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800 transition-colors"
|
|
whileTap={{ scale: 0.9 }}
|
|
aria-label={
|
|
isDarkMode ? t("header.lightMode") : t("header.darkMode")
|
|
}
|
|
>
|
|
{isDarkMode ? (
|
|
<Sun className="h-5 w-5" />
|
|
) : (
|
|
<Moon className="h-5 w-5" />
|
|
)}
|
|
</motion.button>
|
|
|
|
<motion.button
|
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
className="inline-flex items-center justify-center p-2 rounded-lg text-gray-600 hover:text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all"
|
|
aria-expanded="false"
|
|
whileTap={{ scale: 0.9 }}
|
|
>
|
|
<span className="sr-only">Open main menu</span>
|
|
{mobileMenuOpen ? (
|
|
<X className="h-6 w-6" />
|
|
) : (
|
|
<Menu className="h-6 w-6" />
|
|
)}
|
|
</motion.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<AnimatePresence>
|
|
{mobileMenuOpen && (
|
|
<motion.div
|
|
initial="hidden"
|
|
animate="visible"
|
|
exit="hidden"
|
|
variants={mobileMenuVariants}
|
|
className="md:hidden overflow-hidden bg-white dark:bg-gray-900 border-t border-gray-100 dark:border-gray-800 shadow-xl"
|
|
>
|
|
<motion.div className="pt-2 pb-4 space-y-1 px-4">
|
|
{navItems.map((item) => (
|
|
<div key={item?.id}>
|
|
{item?.children ? (
|
|
<div>
|
|
<motion.button
|
|
onClick={() =>
|
|
setOpenDropdown(
|
|
openDropdown === item.id ? null : item.id,
|
|
)
|
|
}
|
|
className={`block w-full text-left px-3 py-3 rounded-lg text-base font-medium transition-all duration-200 ${
|
|
activeTab === item?.id
|
|
? "bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400"
|
|
: "text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-primary-600 dark:hover:text-primary-400"
|
|
}`}
|
|
whileHover={{ x: 4 }}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
{item?.label || localizedName(item?.name)}
|
|
<ChevronDown
|
|
className={`w-4 h-4 transition-transform ${
|
|
openDropdown === item.id ? "rotate-180" : ""
|
|
}`}
|
|
/>
|
|
</div>
|
|
</motion.button>
|
|
|
|
<AnimatePresence>
|
|
{openDropdown === item.id && (
|
|
<motion.div
|
|
initial={{ opacity: 0, height: 0 }}
|
|
animate={{ opacity: 1, height: "auto" }}
|
|
exit={{ opacity: 0, height: 0 }}
|
|
className="pl-4 overflow-hidden"
|
|
>
|
|
{item.children.map((child) => (
|
|
<motion.button
|
|
key={child.id}
|
|
onClick={() => handleNavClick(child)}
|
|
className="block w-full text-left px-3 py-2.5 text-sm text-gray-600 dark:text-gray-400 hover:bg-primary-50 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-400 rounded-lg transition-colors"
|
|
whileHover={{ x: 4 }}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-primary-600 dark:text-primary-400">
|
|
{getServiceIcon(
|
|
child.id,
|
|
isOrgAdmin,
|
|
isSuperAdmin,
|
|
)}
|
|
</span>
|
|
{child.label || localizedName(child.name)}
|
|
</div>
|
|
</motion.button>
|
|
))}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
) : (
|
|
<motion.button
|
|
variants={mobileItemVariants}
|
|
onClick={() => {
|
|
setActiveTab(item?.id);
|
|
setMobileMenuOpen(false);
|
|
if (item.path) {
|
|
handleNavClick(item);
|
|
}
|
|
}}
|
|
whileHover={{ x: 4 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
className={`block w-full text-left px-3 py-3 rounded-lg text-base font-medium transition-all duration-200 ${
|
|
activeTab === item?.id
|
|
? "bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400"
|
|
: "text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-primary-600 dark:hover:text-primary-400"
|
|
}`}
|
|
>
|
|
{item?.label || localizedName(item?.name)}
|
|
</motion.button>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
<motion.div
|
|
variants={mobileItemVariants}
|
|
className="pt-4 pb-2 border-t border-gray-100 space-y-3"
|
|
>
|
|
<div className="px-2">
|
|
<Select
|
|
value={currentLanguage}
|
|
onValueChange={(lng) => changeLanguage(lng)}
|
|
>
|
|
<SelectTrigger className="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-3 py-2.5 shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all">
|
|
<SelectValue>{getCurrentLanguageDisplay()}</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{languageOptions.map((lang) => (
|
|
<SelectItem key={lang.value} value={lang.value}>
|
|
{lang.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</nav>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Header;
|