瀏覽代碼

shadcn like sidebar

Matthew Trejo 2 月之前
父節點
當前提交
4c87d57b16

+ 10 - 3
src/components/AuthenticatedLayout.tsx

@@ -2,7 +2,7 @@
 
 import { useSession } from "next-auth/react"
 import { useRouter } from "next/navigation"
-import { useEffect } from "react"
+import { useEffect, useState } from "react"
 import Sidebar from "@/components/Sidebar"
 import Footer from "@/components/Footer"
 import { COLOR_PALETTE } from "@/utils/palette"
@@ -14,6 +14,7 @@ interface AuthenticatedLayoutProps {
 export default function AuthenticatedLayout({ children }: AuthenticatedLayoutProps) {
   const { data: session, status } = useSession()
   const router = useRouter()
+  const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false)
 
   useEffect(() => {
     if (status === "unauthenticated") {
@@ -38,8 +39,14 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
 
   return (
     <div className="min-h-screen" style={{ backgroundColor: COLOR_PALETTE.gray[50] }}>
-      <Sidebar />
-      <div className="lg:ml-64 min-h-screen flex flex-col">
+      <Sidebar 
+        isCollapsed={isSidebarCollapsed} 
+        onToggleCollapse={() => setIsSidebarCollapsed(!isSidebarCollapsed)} 
+      />
+      <div 
+        className="min-h-screen flex flex-col transition-all duration-300"
+        style={{ marginLeft: isSidebarCollapsed ? '4rem' : '16rem' }}
+      >
         <main className="flex-1 lg:pt-0 pt-16">
           {children}
         </main>

+ 41 - 19
src/components/Sidebar.tsx

@@ -1,16 +1,24 @@
 "use client"
 
 import { useState } from "react"
-import { Menu } from "lucide-react"
+import { Menu, ChevronLeft, ChevronRight } from "lucide-react"
 import SidebarHeader from "@/components/sidebar/SidebarHeader"
 import SidebarUserInfo from "@/components/sidebar/SidebarUserInfo"
 import SidebarNavigation from "@/components/sidebar/SidebarNavigation"
 import SidebarFooter from "@/components/sidebar/SidebarFooter"
 import MobileSidebar from "@/components/sidebar/MobileSidebar"
-import { COLOR_PALETTE } from "@/utils/palette"
 
-export default function Sidebar() {
+interface SidebarProps {
+  isCollapsed?: boolean
+  onToggleCollapse?: () => void
+}
+
+export default function Sidebar({ isCollapsed: externalIsCollapsed, onToggleCollapse }: SidebarProps) {
   const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
+  const [internalIsCollapsed, setInternalIsCollapsed] = useState(false)
+  
+  const isCollapsed = externalIsCollapsed !== undefined ? externalIsCollapsed : internalIsCollapsed
+  const toggleCollapse = onToggleCollapse || (() => setInternalIsCollapsed(!internalIsCollapsed))
 
 
 
@@ -19,14 +27,10 @@ export default function Sidebar() {
       {/* Botón del menú móvil */}
       <button
         type="button"
-        className="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-md shadow-lg border"
-        style={{
-          backgroundColor: 'white',
-          borderColor: COLOR_PALETTE.gray[200]
-        }}
+        className="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-md shadow-lg border border-gray-200 bg-white"
         onClick={() => setIsMobileMenuOpen(true)}
       >
-        <Menu className="h-6 w-6" style={{ color: COLOR_PALETTE.gray[600] }} />
+        <Menu className="h-6 w-6 text-gray-600" />
       </button>
 
       {/* Sidebar móvil */}
@@ -36,18 +40,36 @@ export default function Sidebar() {
       />
 
       {/* Sidebar de escritorio */}
-      <div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
+      <div 
+        className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:flex-col transition-all duration-300"
+        style={{ width: isCollapsed ? '4rem' : '18rem' }}
+      >
         <div 
-          className="flex grow flex-col gap-y-5 overflow-y-auto border-r shadow-lg"
-          style={{
-            backgroundColor: 'white',
-            borderRightColor: COLOR_PALETTE.gray[200]
-          }}
+          className="flex grow flex-col overflow-y-auto border-r border-gray-200 bg-white shadow-sm"
         >
-          <SidebarHeader />
-          <SidebarUserInfo />
-          <SidebarNavigation />
-          <SidebarFooter />
+          {/* Botón de toggle */}
+          <div className={`flex ${isCollapsed ? 'justify-center' : 'justify-end'} p-4 border-b border-gray-200`}>
+            <button
+              onClick={toggleCollapse}
+              className="p-1.5 rounded-md hover:bg-gray-100 transition-colors"
+              title={isCollapsed ? 'Expandir sidebar' : 'Colapsar sidebar'}
+            >
+              {isCollapsed ? (
+                <ChevronRight className="h-4 w-4 text-gray-600" />
+              ) : (
+                <ChevronLeft className="h-4 w-4 text-gray-600" />
+              )}
+            </button>
+          </div>
+
+          <SidebarHeader isCollapsed={isCollapsed} />
+          <SidebarNavigation isCollapsed={isCollapsed} />
+          
+          {/* User info y footer al final */}
+          <div className="mt-auto">
+            <SidebarUserInfo isCollapsed={isCollapsed} />
+            <SidebarFooter isCollapsed={isCollapsed} />
+          </div>
         </div>
       </div>
     </>

+ 4 - 4
src/components/sidebar/MobileSidebar.tsx

@@ -68,10 +68,10 @@ export default function MobileSidebar({ isOpen, onClose }: MobileSidebarProps) {
                 className="flex grow flex-col overflow-y-auto ring-1 ring-white/10"
                 style={{ backgroundColor: 'white' }}
               >
-                <SidebarHeader />
-                <SidebarUserInfo />
-                <SidebarNavigation onItemClick={onClose} />
-                <SidebarFooter onItemClick={onClose} />
+                <SidebarHeader isCollapsed={false} />
+                <SidebarUserInfo isCollapsed={false} />
+                <SidebarNavigation onItemClick={onClose} isCollapsed={false} />
+                <SidebarFooter onItemClick={onClose} isCollapsed={false} />
               </div>
             </Dialog.Panel>
           </Transition.Child>

+ 26 - 36
src/components/sidebar/SidebarFooter.tsx

@@ -7,9 +7,10 @@ import { COLOR_PALETTE } from "@/utils/palette"
 
 interface SidebarFooterProps {
   onItemClick?: () => void
+  isCollapsed?: boolean
 }
 
-export default function SidebarFooter({ onItemClick }: SidebarFooterProps) {
+export default function SidebarFooter({ onItemClick, isCollapsed = false }: SidebarFooterProps) {
   const handleLogout = async () => {
     await signOut({ callbackUrl: "/" })
     onItemClick?.()
@@ -17,50 +18,39 @@ export default function SidebarFooter({ onItemClick }: SidebarFooterProps) {
 
   return (
     <div 
-      className="p-4 border-t"
+      className={`${isCollapsed ? 'p-3' : 'p-4'} border-t ${isCollapsed ? 'flex flex-col items-center space-y-2' : ''}`}
       style={{ 
-        borderTopColor: COLOR_PALETTE.gray[100],
-        backgroundColor: COLOR_PALETTE.gray[50]
+        borderTopColor: COLOR_PALETTE.gray[200],
+        backgroundColor: 'white'
       }}
     >
-      <div className="space-y-2">
-        <Link
-          href="/account"
-          className="flex items-center space-x-3 px-3 py-2 rounded-lg text-sm transition-colors"
-          style={{ color: COLOR_PALETTE.gray[600] }}
-          onMouseEnter={(e) => {
-            e.currentTarget.style.backgroundColor = COLOR_PALETTE.gray[100]
-            e.currentTarget.style.color = COLOR_PALETTE.gray[900]
-          }}
-          onMouseLeave={(e) => {
-            e.currentTarget.style.backgroundColor = 'transparent'
-            e.currentTarget.style.color = COLOR_PALETTE.gray[600]
-          }}
-          onClick={onItemClick}
-        >
-          <Settings className="w-4 h-4" style={{ color: COLOR_PALETTE.gray[500] } as React.CSSProperties} />
-          <span>Configuración de Cuenta</span>
-        </Link>
+      <div className={`space-y-2 ${isCollapsed ? 'w-full flex flex-col items-center' : ''}`}>
+        {!isCollapsed && (
+          <Link
+            href="/account"
+            className="flex items-center space-x-3 px-3 py-2 rounded-lg text-sm transition-colors hover:bg-gray-50"
+            style={{ color: COLOR_PALETTE.gray[600] }}
+            onClick={onItemClick}
+          >
+            <Settings className="w-4 h-4" style={{ color: COLOR_PALETTE.gray[500] } as React.CSSProperties} />
+            <span>Configuración de Cuenta</span>
+          </Link>
+        )}
         
         <button
-          onClick={handleLogout}
-          className="flex items-center space-x-3 w-full px-3 py-2 rounded-lg text-sm transition-all duration-200 border"
+          onClick={() => {
+            handleLogout();
+            onItemClick?.();
+          }}
+          className={`flex items-center ${isCollapsed ? 'justify-center px-2' : 'space-x-3 px-3'} w-full py-2 rounded-lg text-sm transition-colors hover:bg-gray-50 border border-gray-200`}
           style={{
-            color: COLOR_PALETTE.error[600],
-            borderColor: COLOR_PALETTE.error[200],
+            color: COLOR_PALETTE.gray[700],
             backgroundColor: 'white'
           }}
-          onMouseEnter={(e) => {
-            e.currentTarget.style.backgroundColor = COLOR_PALETTE.error[50]
-            e.currentTarget.style.borderColor = COLOR_PALETTE.error[300]
-          }}
-          onMouseLeave={(e) => {
-            e.currentTarget.style.backgroundColor = 'white'
-            e.currentTarget.style.borderColor = COLOR_PALETTE.error[200]
-          }}
+          title={isCollapsed ? 'Cerrar Sesión' : undefined}
         >
-          <LogOut className="w-4 h-4" style={{ color: COLOR_PALETTE.error[600] } as React.CSSProperties} />
-          <span>Cerrar Sesión</span>
+          <LogOut className="w-4 h-4 flex-shrink-0" style={{ color: COLOR_PALETTE.gray[500] } as React.CSSProperties} />
+          {!isCollapsed && <span>Cerrar Sesión</span>}
         </button>
       </div>
     </div>

+ 11 - 15
src/components/sidebar/SidebarHeader.tsx

@@ -1,29 +1,25 @@
 "use client"
 
 import Link from "next/link"
-import { User } from "lucide-react"
-import { COLOR_PALETTE, COMPONENT_COLORS } from "@/utils/palette"
+import { Stethoscope } from "lucide-react"
 
-export default function SidebarHeader() {
+export default function SidebarHeader({ isCollapsed }: { isCollapsed: boolean }) {
   return (
     <div 
-      className="p-6 border-b"
-      style={{ 
-        borderBottomColor: COLOR_PALETTE.gray[100],
-        background: COMPONENT_COLORS.gradients.primary
-      }}
+      className={`${isCollapsed ? 'p-4 flex justify-center' : 'p-6'} border-b border-gray-200 bg-white`}
     >
       <Link href="/dashboard" className="flex items-center space-x-3">
         <div 
-          className="w-10 h-10 backdrop-blur-sm rounded-xl flex items-center justify-center"
-          style={{ backgroundColor: 'rgba(255, 255, 255, 0.2)' }}
+          className="w-10 h-10 rounded-xl flex items-center justify-center bg-primary"
         >
-          <User className="w-6 h-6" style={{ color: 'white' }} />
-        </div>
-        <div>
-          <span className="text-xl font-bold" style={{ color: 'white' }}>Ani Assistant</span>
-          <p className="text-xs" style={{ color: COLOR_PALETTE.primary[100] }}>Medical Assistant</p>
+          <Stethoscope className="h-6 w-6 text-primary-foreground" />
         </div>
+        {!isCollapsed && (
+          <div>
+            <span className="text-xl font-bold text-gray-900">Ani Assistant</span>
+            <p className="text-xs text-gray-500">Medical Assistant</p>
+          </div>
+        )}
       </Link>
     </div>
   )

+ 40 - 53
src/components/sidebar/SidebarNavigation.tsx

@@ -12,7 +12,7 @@ import {
   ChevronRight,
   Home
 } from "lucide-react"
-import { COLOR_PALETTE, COMPONENT_COLORS } from "@/utils/palette"
+import { COLOR_PALETTE } from "@/utils/palette"
 
 interface SidebarItem {
   title: string
@@ -28,9 +28,10 @@ interface SidebarSection {
 
 interface SidebarNavigationProps {
   onItemClick?: () => void
+  isCollapsed?: boolean
 }
 
-export default function SidebarNavigation({ onItemClick }: SidebarNavigationProps) {
+export default function SidebarNavigation({ onItemClick, isCollapsed = false }: SidebarNavigationProps) {
   const { data: session } = useSession()
   const pathname = usePathname()
   const [expandedSections, setExpandedSections] = useState<string[]>([])
@@ -167,77 +168,63 @@ export default function SidebarNavigation({ onItemClick }: SidebarNavigationProp
   }
 
   return (
-    <nav className="flex-1 overflow-y-auto p-4">
+    <nav className={`flex-1 ${isCollapsed ? 'px-2' : 'px-4'} py-4`}>
       {sidebarSections.map((section) => (
         <div key={section.title} className="mb-6">
-          <button
-            onClick={() => toggleSection(section.title)}
-            className="flex items-center justify-between w-full text-left text-sm font-semibold mb-3 px-2 py-1 rounded-md transition-colors"
-            style={{ 
-              color: COLOR_PALETTE.gray[700]
-            }}
-            onMouseEnter={(e) => {
-              e.currentTarget.style.color = COLOR_PALETTE.gray[900]
-              e.currentTarget.style.backgroundColor = COLOR_PALETTE.gray[100]
-            }}
-            onMouseLeave={(e) => {
-              e.currentTarget.style.color = COLOR_PALETTE.gray[700]
-              e.currentTarget.style.backgroundColor = 'transparent'
-            }}
-          >
-            {section.title}
-            {expandedSections.includes(section.title) ? (
-              <ChevronDown className="w-4 h-4" style={{ color: COLOR_PALETTE.gray[500] } as React.CSSProperties} />
-            ) : (
-              <ChevronRight className="w-4 h-4" style={{ color: COLOR_PALETTE.gray[500] } as React.CSSProperties} />
-            )}
-          </button>
+          {!isCollapsed && (
+            <button
+              onClick={() => toggleSection(section.title)}
+              className="flex items-center justify-between w-full text-left text-sm font-semibold mb-3 px-2 py-1 rounded-md transition-colors hover:bg-gray-50"
+              style={{ 
+                color: COLOR_PALETTE.gray[700]
+              }}
+            >
+              {section.title}
+              {expandedSections.includes(section.title) ? (
+                <ChevronDown className="w-4 h-4" style={{ color: COLOR_PALETTE.gray[500] } as React.CSSProperties} />
+              ) : (
+                <ChevronRight className="w-4 h-4" style={{ color: COLOR_PALETTE.gray[500] } as React.CSSProperties} />
+              )}
+            </button>
+          )}
           
-          {expandedSections.includes(section.title) && (
-            <div className="space-y-1 ml-2">
+          {(isCollapsed || expandedSections.includes(section.title)) && (
+            <div className={`space-y-1 ${isCollapsed ? 'ml-0' : 'ml-2'}`}>
               {section.items.map((item) => (
                 <Link
                   key={`${section.title}-${item.href}`}
                   href={item.href}
-                  className="flex items-center space-x-3 px-3 py-2.5 rounded-lg text-sm transition-all duration-200"
+                  className={`flex items-center ${isCollapsed ? 'justify-center px-2' : 'space-x-3 px-3'} py-2.5 rounded-lg text-sm transition-all duration-200 hover:bg-gray-50`}
                   style={{
-                    background: isActive(item.href)
-                      ? COMPONENT_COLORS.gradients.primary
+                    backgroundColor: isActive(item.href)
+                      ? COLOR_PALETTE.gray[100]
                       : 'transparent',
-                    color: isActive(item.href) ? 'white' : COLOR_PALETTE.gray[600],
-                    boxShadow: isActive(item.href) ? '0 4px 6px -1px rgba(0, 0, 0, 0.1)' : 'none'
-                  }}
-                  onMouseEnter={(e) => {
-                    if (!isActive(item.href)) {
-                      e.currentTarget.style.backgroundColor = COLOR_PALETTE.gray[100]
-                      e.currentTarget.style.color = COLOR_PALETTE.gray[900]
-                    }
-                  }}
-                  onMouseLeave={(e) => {
-                    if (!isActive(item.href)) {
-                      e.currentTarget.style.backgroundColor = 'transparent'
-                      e.currentTarget.style.color = COLOR_PALETTE.gray[600]
-                    }
+                    color: isActive(item.href) ? COLOR_PALETTE.gray[900] : COLOR_PALETTE.gray[600],
+                    border: isActive(item.href) ? `1px solid ${COLOR_PALETTE.gray[200]}` : 'none'
                   }}
                   onClick={onItemClick}
+                  title={isCollapsed ? item.title : undefined}
                 >
                   <item.icon 
-                    className="w-4 h-4"
+                    className="w-4 h-4 flex-shrink-0"
                     style={{ 
-                      color: isActive(item.href) ? 'white' : COLOR_PALETTE.gray[500] 
+                      color: isActive(item.href) ? COLOR_PALETTE.gray[900] : COLOR_PALETTE.gray[500] 
                     } as React.CSSProperties}
                   />
-                  <span className="flex-1 font-medium">{item.title}</span>
-                  {item.badge && (
+                  {!isCollapsed && (
+                    <span className="flex-1 font-medium truncate">{item.title}</span>
+                  )}
+                  {!isCollapsed && item.badge && (
                     <span 
-                      className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
+                      className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium border"
                       style={{
                         backgroundColor: isActive(item.href)
-                          ? 'rgba(255, 255, 255, 0.2)'
-                          : COLOR_PALETTE.primary[100],
+                          ? COLOR_PALETTE.gray[200]
+                          : COLOR_PALETTE.gray[100],
                         color: isActive(item.href)
-                          ? 'white'
-                          : COLOR_PALETTE.primary[800]
+                          ? COLOR_PALETTE.gray[700]
+                          : COLOR_PALETTE.gray[600],
+                        borderColor: COLOR_PALETTE.gray[200]
                       }}
                     >
                       {item.badge}

+ 34 - 39
src/components/sidebar/SidebarUserInfo.tsx

@@ -6,7 +6,7 @@ import { ProfileImage } from "@/components/ui/profile-image"
 import { useProfileImageContext } from "@/contexts/ProfileImageContext"
 import { COLOR_PALETTE } from "@/utils/palette"
 
-export default function SidebarUserInfo() {
+export default function SidebarUserInfo({ isCollapsed }: { isCollapsed: boolean }) {
   const { data: session } = useSession()
   const { profileImage } = useProfileImageContext()
 
@@ -14,52 +14,47 @@ export default function SidebarUserInfo() {
 
   return (
     <div 
-      className="p-4 border-b"
+      className={`${isCollapsed ? 'p-3' : 'p-4'} border-b ${isCollapsed ? 'flex justify-center' : ''}`}
       style={{ 
-        borderBottomColor: COLOR_PALETTE.gray[100],
-        backgroundColor: COLOR_PALETTE.gray[50]
+        borderBottomColor: COLOR_PALETTE.gray[200],
+        backgroundColor: 'white'
       }}
     >
-      <div className="flex items-center space-x-3">
+      <div className={`flex items-center ${isCollapsed ? 'flex-col space-y-2' : 'space-x-3'}`}>
         <ProfileImage
           src={profileImage}
           alt={`${session.user.name} ${session.user.lastname}`}
           fallback={session.user.name}
-          size="md"
+          size={isCollapsed ? "sm" : "md"}
         />
-        <div className="flex-1 min-w-0">
-          <p 
-            className="text-sm font-semibold truncate"
-            style={{ color: COLOR_PALETTE.gray[900] }}
-          >
-            {session.user.name} {session.user.lastname}
-          </p>
-          <p 
-            className="text-xs truncate"
-            style={{ color: COLOR_PALETTE.gray[500] }}
-          >
-            {session.user.email}
-          </p>
-          <span 
-            className={cn(
-              "inline-flex items-center px-2 py-1 mt-1 rounded-full text-xs font-medium"
-            )}
-            style={{
-              backgroundColor: session.user.role === "ADMIN" 
-                ? "#f3e8ff"
-                : session.user.role === "DOCTOR" 
-                ? COLOR_PALETTE.primary[100] 
-                : COLOR_PALETTE.success[100],
-              color: session.user.role === "ADMIN" 
-                ? "#6b21a8"
-                : session.user.role === "DOCTOR" 
-                ? COLOR_PALETTE.primary[800] 
-                : COLOR_PALETTE.success[800]
-            }}
-          >
-            {session.user.role === "ADMIN" ? "Administrador" : session.user.role === "DOCTOR" ? "Doctor" : "Paciente"}
-          </span>
-        </div>
+        {!isCollapsed && (
+          <div className="flex-1 min-w-0">
+            <p 
+              className="text-sm font-semibold truncate"
+              style={{ color: COLOR_PALETTE.gray[900] }}
+            >
+              {session.user.name} {session.user.lastname}
+            </p>
+            <p 
+              className="text-xs truncate"
+              style={{ color: COLOR_PALETTE.gray[500] }}
+            >
+              {session.user.email}
+            </p>
+            <span 
+              className={cn(
+                "inline-flex items-center px-2 py-1 mt-1 rounded-full text-xs font-medium border"
+              )}
+              style={{
+                backgroundColor: COLOR_PALETTE.gray[50],
+                color: COLOR_PALETTE.gray[700],
+                borderColor: COLOR_PALETTE.gray[200]
+              }}
+            >
+              {session.user.role === "ADMIN" ? "Administrador" : session.user.role === "DOCTOR" ? "Doctor" : "Paciente"}
+            </span>
+          </div>
+        )}
       </div>
     </div>
   )

+ 2 - 2
src/components/ui/profile-image.tsx

@@ -49,14 +49,14 @@ export function ProfileImage({
           />
           {/* Fallback que se muestra mientras carga o si falla */}
           <div className={cn(
-            "absolute inset-0 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold",
+            "absolute inset-0 bg-primary rounded-full flex items-center justify-center text-primary-foreground font-semibold",
             imageLoaded && !imageError ? "hidden" : ""
           )}>
             {fallback.charAt(0).toUpperCase()}
           </div>
         </>
       ) : (
-        <div className="w-full h-full bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold">
+        <div className="w-full h-full bg-primary rounded-full flex items-center justify-center text-primary-foreground font-semibold">
           {fallback.charAt(0).toUpperCase()}
         </div>
       )}