UNPKG

aurcare-ui

Version:

Aurcare UI component library for healthcare applications with dynamic sidebar, navbar, and stats cards

603 lines (600 loc) 27.8 kB
import { useState, useEffect, useCallback, Fragment as Fragment$1 } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { useTheme } from 'next-themes'; import { Button } from '@heroui/button'; import { Badge } from '@heroui/badge'; import { cn as cn$1 } from '@heroui/theme'; import { LayoutDashboard, ChevronLeft, ChevronRight, Bell, Clock, ChevronDown, User, Settings, Check, Sun, Moon, Monitor, Languages, LogOut, Info, AlertTriangle, CheckCircle, ChevronUp } from 'lucide-react'; import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@heroui/dropdown'; import { Avatar } from '@heroui/avatar'; import { Divider } from '@heroui/divider'; import { Card, CardBody } from '@heroui/card'; // src/components/Sidebar/Sidebar.tsx // src/utils/index.ts function cn(...classes) { return classes.filter(Boolean).join(" "); } function isPathActive(itemPath, currentPath) { if (itemPath === currentPath) return true; if (itemPath === "/" || itemPath === "") return currentPath === "/" || currentPath === ""; return currentPath.startsWith(itemPath + "/"); } function isRTL(locale) { const rtlLocales = ["ar", "he", "fa", "ur"]; return locale ? rtlLocales.includes(locale) : false; } function getDirection(locale) { return isRTL(locale) ? "rtl" : "ltr"; } function defaultTranslate(key) { return key; } function truncate(text, maxLength) { if (text.length <= maxLength) return text; return text.slice(0, maxLength) + "..."; } function timeAgo(date) { const now = /* @__PURE__ */ new Date(); const past = new Date(date); const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1e3); if (diffInSeconds < 60) return "just now"; if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`; if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`; if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`; return past.toLocaleDateString(); } function Sidebar({ sections, isCollapsed: controlledCollapsed, onCollapse, currentPath, locale = "en", appName = "Aurcare", appSubtitle = "Healthcare System", appLogo: AppLogo = LayoutDashboard, className, expandedWidth = "w-72", collapsedWidth = "w-20", hideCollapseButton = false, customHeader, customFooter, onNavigate, logoClassName, logoContainerClassName, logoIconClassName, navItemClassName, navSectionClassName, backgroundClassName }) { const { theme } = useTheme(); const [mounted, setMounted] = useState(false); const [internalCollapsed, setInternalCollapsed] = useState(false); const [expandedItems, setExpandedItems] = useState([]); const pathname = usePathname?.() || currentPath || "/"; const isRTLMode = isRTL(locale); useEffect(() => { setMounted(true); }, []); const isCollapsed = controlledCollapsed !== void 0 ? controlledCollapsed : internalCollapsed; const toggleCollapse = useCallback(() => { const newValue = !isCollapsed; setInternalCollapsed(newValue); onCollapse?.(newValue); }, [isCollapsed, onCollapse]); const toggleExpanded = useCallback((itemId) => { setExpandedItems( (prev) => prev.includes(itemId) ? prev.filter((id) => id !== itemId) : [...prev, itemId] ); }, []); const handleNavigate = useCallback((item) => { onNavigate?.(item); }, [onNavigate]); const renderNavItem = (item, depth = 0) => { const Icon = item.icon; const isActive = item.href ? isPathActive(item.href, pathname) : false; const hasSubItems = item.subItems && item.subItems.length > 0; const isExpanded = expandedItems.includes(item.id); if (item.render) { return /* @__PURE__ */ jsx("li", { children: item.render(item, isActive) }, item.id); } return /* @__PURE__ */ jsx("li", { children: hasSubItems ? /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs( "div", { onClick: () => toggleExpanded(item.id), className: cn$1( "flex items-center justify-between gap-3 px-3 py-3 rounded-lg transition-all duration-200 cursor-pointer", "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-800", depth > 0 && "py-2.5", navItemClassName, item.className ), children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [ /* @__PURE__ */ jsx(Icon, { className: cn$1("flex-shrink-0", depth === 0 ? "w-5 h-5" : "w-4 h-4") }), !isCollapsed && /* @__PURE__ */ jsx("span", { className: "font-medium text-sm truncate", children: item.label }), !isCollapsed && item.badge && /* @__PURE__ */ jsx( Badge, { color: item.badgeColor || "primary", size: "sm", children: item.badge } ) ] }), !isCollapsed && (isExpanded ? /* @__PURE__ */ jsx(ChevronUp, { className: "w-4 h-4 flex-shrink-0" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "w-4 h-4 flex-shrink-0" })) ] } ), isExpanded && !isCollapsed && item.subItems && /* @__PURE__ */ jsx("ul", { className: cn$1("mt-1.5 space-y-1.5", "ms-6"), children: item.subItems.map((subItem) => renderNavItem(subItem, depth + 1)) }) ] }) : /* @__PURE__ */ jsx( Link, { href: item.href || "#", onClick: () => handleNavigate(item), children: /* @__PURE__ */ jsxs( "div", { className: cn$1( "flex items-center gap-3 px-3 py-3 rounded-lg transition-all duration-200", isActive ? "bg-primary text-white shadow-md" : "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-800", depth > 0 && "py-2.5", navItemClassName, item.className ), children: [ /* @__PURE__ */ jsx(Icon, { className: cn$1("flex-shrink-0", depth === 0 ? "w-5 h-5" : "w-4 h-4") }), !isCollapsed && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("span", { className: "font-medium text-sm truncate flex-1", children: item.label }), item.badge && /* @__PURE__ */ jsx( Badge, { color: item.badgeColor || "primary", size: "sm", children: item.badge } ) ] }) ] } ) } ) }, item.id); }; return /* @__PURE__ */ jsxs( "aside", { suppressHydrationWarning: true, className: cn( "flex flex-col h-screen bg-white dark:bg-slate-900 transition-all duration-300 flex-shrink-0 border-gray-200 dark:border-slate-700", "border-s", isCollapsed ? collapsedWidth : expandedWidth, "inset-x-0", isRTLMode ? "rtl:right-auto" : "ltr:left-auto", backgroundClassName, className ), dir: isRTLMode ? "rtl" : "ltr", children: [ customHeader || /* @__PURE__ */ jsxs("div", { className: cn( "flex items-center justify-between px-6 py-3 border-b border-gray-200 dark:border-slate-700 h-[63px] mb-[15px]", logoClassName ), children: [ !isCollapsed && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [ /* @__PURE__ */ jsx("div", { className: cn( "w-10 h-10 rounded-xl bg-primary flex items-center justify-center shadow-lg", logoContainerClassName ), children: /* @__PURE__ */ jsx(AppLogo, { className: cn("w-6 h-6 text-white", logoIconClassName) }) }), /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [ /* @__PURE__ */ jsx("span", { className: "font-bold text-xl text-primary block", children: appName }), /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-0.5", children: appSubtitle }) ] }) ] }), !hideCollapseButton && /* @__PURE__ */ jsx( Button, { isIconOnly: true, size: "sm", variant: "light", onPress: toggleCollapse, children: isCollapsed ? isRTLMode ? /* @__PURE__ */ jsx(ChevronLeft, { className: "w-4 h-4" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "w-4 h-4" }) : isRTLMode ? /* @__PURE__ */ jsx(ChevronRight, { className: "w-4 h-4" }) : /* @__PURE__ */ jsx(ChevronLeft, { className: "w-4 h-4" }) } ) ] }), /* @__PURE__ */ jsx("nav", { className: "flex-1 overflow-y-auto px-4 py-5", children: /* @__PURE__ */ jsx("div", { className: "space-y-6", children: sections.map((section) => /* @__PURE__ */ jsxs("div", { children: [ section.title && !isCollapsed && /* @__PURE__ */ jsx("div", { className: cn$1("px-3 mb-3", navSectionClassName), children: /* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400", children: section.title }) }), /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: section.items.map((item) => renderNavItem(item)) }) ] }, section.id)) }) }), customFooter ] } ); } function Navbar({ user = { name: "Admin", role: "Administrator" }, notifications = [], onNotificationClick, onMarkAllAsRead, onLogout, onProfileClick, onSettingsClick, languages = [ { code: "en", label: "English" }, { code: "ar", label: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629" } ], currentLanguage = "en", onLanguageChange, showThemeToggle = true, showNotifications = true, showLanguageSwitcher = true, leftContent, rightContent, className, t = defaultTranslate, containerClassName, notificationsClassName, userMenuClassName, backgroundClassName }) { const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false); const [showNotificationsDropdown, setShowNotificationsDropdown] = useState(false); const unreadCount = notifications.filter((n) => !n.read).length; useEffect(() => { setMounted(true); }, []); const getNotificationIcon = (type) => { switch (type) { case "success": return /* @__PURE__ */ jsx(CheckCircle, { className: "w-5 h-5 text-success" }); case "warning": return /* @__PURE__ */ jsx(AlertTriangle, { className: "w-5 h-5 text-warning" }); case "error": return /* @__PURE__ */ jsx(AlertTriangle, { className: "w-5 h-5 text-danger" }); default: return /* @__PURE__ */ jsx(Info, { className: "w-5 h-5 text-primary" }); } }; const handleNotificationClick = (notification) => { onNotificationClick?.(notification); }; return /* @__PURE__ */ jsxs( "nav", { suppressHydrationWarning: true, className: cn( "h-16 bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-700 px-6 flex items-center justify-between sticky top-0 z-40", backgroundClassName, containerClassName, className ), children: [ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-4", children: leftContent }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [ rightContent, showNotifications && /* @__PURE__ */ jsxs( Dropdown, { isOpen: showNotificationsDropdown, onOpenChange: setShowNotificationsDropdown, placement: "bottom-end", children: [ /* @__PURE__ */ jsx(DropdownTrigger, { children: /* @__PURE__ */ jsx( Button, { isIconOnly: true, variant: "light", radius: "full", className: "relative", children: /* @__PURE__ */ jsx( Badge, { content: unreadCount, color: "danger", isInvisible: unreadCount === 0, size: "sm", placement: "top-right", children: /* @__PURE__ */ jsx(Bell, { className: "w-5 h-5" }) } ) } ) }), /* @__PURE__ */ jsxs( DropdownMenu, { "aria-label": t("notifications"), className: cn("w-[480px] max-h-[500px] overflow-y-auto", notificationsClassName), closeOnSelect: false, children: [ /* @__PURE__ */ jsx( DropdownItem, { isReadOnly: true, className: "h-14 gap-2", textValue: t("notifications"), children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-2", children: [ /* @__PURE__ */ jsx("p", { className: "font-bold text-lg text-gray-900 dark:text-white", children: t("notifications") }), unreadCount > 0 && /* @__PURE__ */ jsx( Button, { size: "sm", variant: "flat", color: "primary", onPress: onMarkAllAsRead, children: t("markAllRead") } ) ] }) }, "header" ), /* @__PURE__ */ jsx(DropdownItem, { isReadOnly: true, textValue: "divider", children: /* @__PURE__ */ jsx(Divider, {}) }, "divider1"), notifications.length === 0 ? /* @__PURE__ */ jsx(DropdownItem, { isReadOnly: true, textValue: t("noNotifications"), children: /* @__PURE__ */ jsxs("div", { className: "text-center py-8 text-gray-500", children: [ /* @__PURE__ */ jsx(Bell, { className: "w-12 h-12 mx-auto mb-2 opacity-50" }), /* @__PURE__ */ jsx("p", { className: "text-sm", children: t("noNotifications") }) ] }) }, "empty") : /* @__PURE__ */ jsx(Fragment, { children: notifications.map((notification, index) => /* @__PURE__ */ jsxs(Fragment$1, { children: [ /* @__PURE__ */ jsx( DropdownItem, { textValue: notification.title, className: `py-3 px-3 ${!notification.read ? "bg-primary/5" : ""}`, onPress: () => handleNotificationClick(notification), children: /* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [ /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 mt-1", children: getNotificationIcon(notification.type) }), /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [ /* @__PURE__ */ jsx("p", { className: "font-semibold text-sm text-gray-900 dark:text-white", children: notification.title }), !notification.read && /* @__PURE__ */ jsx("div", { className: "w-2 h-2 bg-primary rounded-full flex-shrink-0 mt-1" }) ] }), /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-600 dark:text-gray-400 mt-1 line-clamp-2", children: notification.message }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 mt-2 text-xs text-gray-500", children: [ /* @__PURE__ */ jsx(Clock, { className: "w-3 h-3" }), /* @__PURE__ */ jsx("span", { children: notification.time }) ] }) ] }) ] }) }, notification.id ), index < notifications.length - 1 && /* @__PURE__ */ jsx( DropdownItem, { isReadOnly: true, textValue: "divider", children: /* @__PURE__ */ jsx(Divider, { className: "my-0" }) }, `divider-${notification.id}` ) ] }, notification.id)) }) ] } ) ] } ), /* @__PURE__ */ jsxs(Dropdown, { placement: "bottom-end", children: [ /* @__PURE__ */ jsx(DropdownTrigger, { children: /* @__PURE__ */ jsxs(Button, { variant: "light", className: "gap-2 px-2 h-auto py-2", children: [ /* @__PURE__ */ jsx( Avatar, { size: "sm", name: user.name, src: user.avatarUrl, className: "w-9 h-9 bg-gradient-to-br from-primary to-secondary text-white font-semibold" } ), /* @__PURE__ */ jsxs("div", { className: "hidden md:flex flex-col items-start", children: [ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: user.name }), /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500", children: user.role || user.email || t("user") }) ] }), /* @__PURE__ */ jsx(ChevronDown, { className: "w-4 h-4 text-gray-500 hidden md:block" }) ] }) }), /* @__PURE__ */ jsxs(DropdownMenu, { "aria-label": "User menu", className: cn("w-80", userMenuClassName), children: [ /* @__PURE__ */ jsx( DropdownItem, { isReadOnly: true, textValue: "Profile info", className: "h-14 gap-2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [ /* @__PURE__ */ jsx( Avatar, { size: "md", name: user.name, src: user.avatarUrl, className: "w-10 h-10 bg-gradient-to-br from-primary to-secondary text-white font-semibold" } ), /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: user.name }), /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: user.role || user.email || t("user") }) ] }) ] }) }, "profile-info" ), /* @__PURE__ */ jsx(DropdownItem, { isReadOnly: true, textValue: "divider", children: /* @__PURE__ */ jsx(Divider, {}) }, "divider1"), onProfileClick ? /* @__PURE__ */ jsx( DropdownItem, { startContent: /* @__PURE__ */ jsx(User, { className: "w-4 h-4" }), textValue: t("profile"), onPress: onProfileClick, className: "text-gray-900 dark:text-white", children: t("profile") }, "profile" ) : null, onSettingsClick ? /* @__PURE__ */ jsx( DropdownItem, { startContent: /* @__PURE__ */ jsx(Settings, { className: "w-4 h-4" }), textValue: t("settings"), onPress: onSettingsClick, className: "text-gray-900 dark:text-white", children: t("settings") }, "settings" ) : null, onProfileClick || onSettingsClick ? /* @__PURE__ */ jsx(DropdownItem, { isReadOnly: true, textValue: "divider", children: /* @__PURE__ */ jsx(Divider, {}) }, "divider2") : null, showThemeToggle ? /* @__PURE__ */ jsxs(Fragment$1, { children: [ /* @__PURE__ */ jsx( DropdownItem, { isReadOnly: true, className: "opacity-60 text-xs font-semibold text-gray-700 dark:text-gray-300", textValue: "Theme", children: t("theme") }, "theme-header" ), /* @__PURE__ */ jsx( DropdownItem, { startContent: /* @__PURE__ */ jsx(Sun, { className: "w-4 h-4" }), endContent: theme === "light" ? /* @__PURE__ */ jsx(Check, { className: "w-4 h-4 text-primary" }) : null, textValue: "Light", onPress: () => setTheme("light"), className: "text-gray-900 dark:text-white", children: t("light") }, "theme-light" ), /* @__PURE__ */ jsx( DropdownItem, { startContent: /* @__PURE__ */ jsx(Moon, { className: "w-4 h-4" }), endContent: theme === "dark" ? /* @__PURE__ */ jsx(Check, { className: "w-4 h-4 text-primary" }) : null, textValue: "Dark", onPress: () => setTheme("dark"), className: "text-gray-900 dark:text-white", children: t("dark") }, "theme-dark" ), /* @__PURE__ */ jsx( DropdownItem, { startContent: /* @__PURE__ */ jsx(Monitor, { className: "w-4 h-4" }), endContent: theme === "system" ? /* @__PURE__ */ jsx(Check, { className: "w-4 h-4 text-primary" }) : null, textValue: "System", onPress: () => setTheme("system"), className: "text-gray-900 dark:text-white", children: t("system") }, "theme-system" ), /* @__PURE__ */ jsx(DropdownItem, { isReadOnly: true, textValue: "divider", children: /* @__PURE__ */ jsx(Divider, {}) }, "divider3") ] }) : null, showLanguageSwitcher && languages.length > 0 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [ /* @__PURE__ */ jsx( DropdownItem, { isReadOnly: true, className: "opacity-60 text-xs font-semibold text-gray-700 dark:text-gray-300", textValue: "Language", children: t("language") }, "language-header" ), languages.map((lang) => { const LangIcon = lang.icon || Languages; return /* @__PURE__ */ jsx( DropdownItem, { startContent: /* @__PURE__ */ jsx(LangIcon, { className: "w-4 h-4" }), endContent: currentLanguage === lang.code ? /* @__PURE__ */ jsx(Check, { className: "w-4 h-4 text-primary" }) : null, textValue: lang.label, onPress: () => onLanguageChange?.(lang.code), className: "text-gray-900 dark:text-white", children: lang.label }, `language-${lang.code}` ); }), /* @__PURE__ */ jsx(DropdownItem, { isReadOnly: true, textValue: "divider", children: /* @__PURE__ */ jsx(Divider, {}) }, "divider4") ] }) : null, onLogout ? /* @__PURE__ */ jsx( DropdownItem, { color: "danger", startContent: /* @__PURE__ */ jsx(LogOut, { className: "w-4 h-4" }), textValue: t("logout"), onPress: onLogout, className: "text-danger", children: t("logout") }, "logout" ) : null ] }) ] }) ] }) ] } ); } var colorClasses = { primary: { border: "border-l-primary", bg: "bg-primary/10", text: "text-primary" }, success: { border: "border-l-success", bg: "bg-success/10", text: "text-success" }, warning: { border: "border-l-warning", bg: "bg-warning/10", text: "text-warning" }, danger: { border: "border-l-danger", bg: "bg-danger/10", text: "text-danger" }, secondary: { border: "border-l-secondary", bg: "bg-secondary/10", text: "text-secondary" }, default: { border: "border-l-default", bg: "bg-default/10", text: "text-default" } }; function StatsCard({ label, value, icon: Icon, color = "primary", subtitle, trend }) { const colors = colorClasses[color]; return /* @__PURE__ */ jsx(Card, { className: `border-l-4 ${colors.border} shadow-md hover:shadow-lg transition-shadow`, children: /* @__PURE__ */ jsx(CardBody, { className: "p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-gray-600 dark:text-gray-400 uppercase tracking-wide", children: label }), /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2 mt-1", children: [ /* @__PURE__ */ jsx("p", { className: "text-2xl font-bold", children: value }), trend && /* @__PURE__ */ jsxs("span", { className: `text-xs font-medium ${trend.isPositive ? "text-success" : "text-danger"}`, children: [ trend.isPositive ? "+" : "", trend.value, "%" ] }) ] }), subtitle && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: subtitle }) ] }), /* @__PURE__ */ jsx("div", { className: `p-2 ${colors.bg} rounded-lg`, children: /* @__PURE__ */ jsx(Icon, { className: `w-6 h-6 ${colors.text}` }) }) ] }) }) }); } function StatsGrid({ children, columns = 4 }) { const gridCols = { 2: "lg:grid-cols-2", 3: "lg:grid-cols-3", 4: "lg:grid-cols-4", 5: "lg:grid-cols-5" }; return /* @__PURE__ */ jsx("div", { className: `grid grid-cols-1 md:grid-cols-2 ${gridCols[columns]} gap-4`, children }); } export { Navbar, Sidebar, StatsCard, StatsGrid, cn, defaultTranslate, getDirection, isPathActive, isRTL, timeAgo, truncate }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map