Spaces:
Sleeping
Sleeping
import React from 'react'; | |
import { motion, AnimatePresence } from 'framer-motion'; | |
import { | |
PlusIcon, | |
TrashIcon, | |
XMarkIcon, | |
ChatBubbleLeftIcon, | |
HomeIcon, | |
Bars3Icon | |
} from '@heroicons/react/24/outline'; | |
const Sidebar = ({ | |
open, | |
onClose, | |
conversations, | |
activeConversationId, | |
onConversationSelect, | |
onNewChat, | |
onDeleteConversation, | |
onBackToHome, | |
darkMode | |
}) => { | |
const formatDate = (date) => { | |
const now = new Date(); | |
const messageDate = new Date(date); | |
const diffTime = Math.abs(now - messageDate); | |
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); | |
if (diffDays === 1) return 'Today'; | |
if (diffDays === 2) return 'Yesterday'; | |
if (diffDays <= 7) return `${diffDays - 1} days ago`; | |
return messageDate.toLocaleDateString(); | |
}; | |
const truncateTitle = (title, maxLength = 25) => { | |
if (title.length <= maxLength) return title; | |
return title.substring(0, maxLength) + '...'; | |
}; | |
return ( | |
<AnimatePresence> | |
{open && ( | |
<> | |
{/* Mobile: Full-screen overlay background */} | |
<motion.div | |
initial={{ opacity: 0 }} | |
animate={{ opacity: 1 }} | |
exit={{ opacity: 0 }} | |
className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden" | |
onClick={onClose} | |
/> | |
{/* Sidebar Content */} | |
<motion.div | |
initial={{ | |
x: '-100%', | |
opacity: 0 | |
}} | |
animate={{ | |
x: 0, | |
opacity: 1 | |
}} | |
exit={{ | |
x: '-100%', | |
opacity: 0 | |
}} | |
transition={{ | |
type: "spring", | |
damping: 25, | |
stiffness: 200 | |
}} | |
className={`fixed top-0 left-0 h-full w-full md:w-80 z-50 md:z-30 ${ | |
darkMode | |
? 'bg-gray-900 border-gray-700' | |
: 'bg-white border-gray-200' | |
} border-r shadow-2xl md:shadow-xl flex flex-col`} | |
> | |
{/* Header - Mobile optimized */} | |
<div className={`p-4 md:p-6 border-b ${ | |
darkMode ? 'border-gray-700/50' : 'border-gray-200/50' | |
} flex-shrink-0`}> | |
{/* Top row: Close button and title */} | |
<div className="flex items-center justify-between mb-4 md:mb-3"> | |
<h2 className={`text-lg md:text-xl font-bold ${ | |
darkMode ? 'text-white' : 'text-gray-900' | |
}`}> | |
Conversations | |
</h2> | |
<button | |
onClick={onClose} | |
className={`p-2 md:p-1.5 rounded-lg transition-colors touch-manipulation ${ | |
darkMode | |
? 'hover:bg-gray-800 active:bg-gray-700 text-gray-400' | |
: 'hover:bg-gray-100 active:bg-gray-200 text-gray-500' | |
}`} | |
title="Close sidebar" | |
> | |
<XMarkIcon className="w-6 h-6 md:w-5 md:h-5" /> | |
</button> | |
</div> | |
{/* Action buttons - Mobile optimized */} | |
<div className="flex flex-col sm:flex-row gap-2 md:gap-3"> | |
<motion.button | |
whileHover={{ scale: 1.02 }} | |
whileTap={{ scale: 0.98 }} | |
onClick={() => { | |
onNewChat(); | |
onClose(); | |
}} | |
className={`flex items-center justify-center gap-2 md:gap-3 px-4 py-3 md:py-2.5 rounded-xl md:rounded-lg font-medium transition-all touch-manipulation ${ | |
darkMode | |
? 'bg-primary-600 hover:bg-primary-700 active:bg-primary-800 text-white shadow-lg' | |
: 'bg-primary-500 hover:bg-primary-600 active:bg-primary-700 text-white shadow-lg' | |
} hover:shadow-xl active:shadow-2xl flex-1 sm:flex-none`} | |
> | |
<PlusIcon className="w-5 h-5 md:w-4 md:h-4" /> | |
<span className="text-base md:text-sm">New Chat</span> | |
</motion.button> | |
<motion.button | |
whileHover={{ scale: 1.02 }} | |
whileTap={{ scale: 0.98 }} | |
onClick={() => { | |
onBackToHome(); | |
onClose(); | |
}} | |
className={`flex items-center justify-center gap-2 md:gap-3 px-4 py-3 md:py-2.5 rounded-xl md:rounded-lg font-medium transition-all touch-manipulation ${ | |
darkMode | |
? 'bg-gray-700 hover:bg-gray-600 active:bg-gray-500 text-gray-200 shadow-lg' | |
: 'bg-gray-200 hover:bg-gray-300 active:bg-gray-400 text-gray-700 shadow-lg' | |
} hover:shadow-xl active:shadow-2xl flex-1 sm:flex-none`} | |
> | |
<HomeIcon className="w-5 h-5 md:w-4 md:h-4" /> | |
<span className="text-base md:text-sm">Home</span> | |
</motion.button> | |
</div> | |
</div> | |
{/* Conversations List - Mobile optimized */} | |
<div className="flex-1 overflow-y-auto p-3 md:p-4"> | |
{conversations.length === 0 ? ( | |
<div className="flex flex-col items-center justify-center h-full text-center px-4"> | |
<ChatBubbleLeftIcon className={`w-12 h-12 md:w-16 md:h-16 mb-4 ${ | |
darkMode ? 'text-gray-600' : 'text-gray-400' | |
}`} /> | |
<p className={`text-base md:text-lg font-medium mb-2 ${ | |
darkMode ? 'text-gray-400' : 'text-gray-500' | |
}`}> | |
No conversations yet | |
</p> | |
<p className={`text-sm md:text-base ${ | |
darkMode ? 'text-gray-500' : 'text-gray-400' | |
}`}> | |
Start a new chat to begin your CA study session | |
</p> | |
</div> | |
) : ( | |
<div className="space-y-2 md:space-y-1"> | |
{conversations.map((conv) => ( | |
<motion.div | |
key={conv.id} | |
initial={{ opacity: 0, x: -20 }} | |
animate={{ opacity: 1, x: 0 }} | |
whileHover={{ x: 4 }} | |
className={`group relative p-3 md:p-3 rounded-xl md:rounded-lg cursor-pointer transition-all touch-manipulation ${ | |
activeConversationId === conv.id | |
? darkMode | |
? 'bg-primary-600/20 border-primary-500/30 shadow-lg' | |
: 'bg-primary-50 border-primary-200 shadow-lg' | |
: darkMode | |
? 'hover:bg-gray-800 active:bg-gray-700' | |
: 'hover:bg-gray-50 active:bg-gray-100' | |
} border ${ | |
activeConversationId === conv.id | |
? '' | |
: darkMode | |
? 'border-transparent' | |
: 'border-transparent' | |
}`} | |
onClick={() => { | |
onConversationSelect(conv.id); | |
onClose(); | |
}} | |
> | |
{/* Conversation Content */} | |
<div className="flex items-start justify-between"> | |
<div className="flex-1 min-w-0 mr-2"> | |
{/* Title */} | |
<h3 className={`font-medium text-sm md:text-sm mb-1 truncate ${ | |
activeConversationId === conv.id | |
? darkMode | |
? 'text-primary-300' | |
: 'text-primary-700' | |
: darkMode | |
? 'text-gray-200' | |
: 'text-gray-900' | |
}`}> | |
{truncateTitle(conv.title || 'New Conversation')} | |
</h3> | |
{/* Message count and date */} | |
<div className="flex items-center space-x-2"> | |
<span className={`text-xs ${ | |
activeConversationId === conv.id | |
? darkMode | |
? 'text-primary-400/80' | |
: 'text-primary-600/80' | |
: darkMode | |
? 'text-gray-500' | |
: 'text-gray-500' | |
}`}> | |
{conv.messages?.length || 0} messages | |
</span> | |
<span className={`text-xs ${ | |
darkMode ? 'text-gray-600' : 'text-gray-400' | |
}`}> | |
• | |
</span> | |
<span className={`text-xs ${ | |
darkMode ? 'text-gray-500' : 'text-gray-400' | |
}`}> | |
{formatDate(conv.createdAt)} | |
</span> | |
</div> | |
{/* Last message preview - Mobile optimized */} | |
{conv.messages && conv.messages.length > 0 && ( | |
<p className={`text-xs mt-1 truncate ${ | |
darkMode ? 'text-gray-500' : 'text-gray-500' | |
}`}> | |
{conv.messages[conv.messages.length - 1].content.substring(0, 40)}... | |
</p> | |
)} | |
</div> | |
{/* Delete Button - Larger touch target for mobile */} | |
<motion.button | |
whileHover={{ scale: 1.1 }} | |
whileTap={{ scale: 0.9 }} | |
onClick={(e) => { | |
e.stopPropagation(); | |
onDeleteConversation(conv.id); | |
}} | |
className={`p-2 md:p-1.5 rounded-lg opacity-0 md:group-hover:opacity-100 transition-all touch-manipulation ${ | |
darkMode | |
? 'hover:bg-red-600/20 active:bg-red-600/30 text-red-400' | |
: 'hover:bg-red-50 active:bg-red-100 text-red-500' | |
} flex-shrink-0 md:opacity-100 sm:opacity-100`} | |
title="Delete conversation" | |
> | |
<TrashIcon className="w-4 h-4 md:w-4 md:h-4" /> | |
</motion.button> | |
</div> | |
{/* Active indicator */} | |
{activeConversationId === conv.id && ( | |
<motion.div | |
layoutId="activeConversation" | |
className={`absolute left-0 top-0 bottom-0 w-1 rounded-r ${ | |
darkMode ? 'bg-primary-500' : 'bg-primary-500' | |
}`} | |
/> | |
)} | |
</motion.div> | |
))} | |
</div> | |
)} | |
</div> | |
{/* Footer - Mobile optimized */} | |
<div className={`p-4 md:p-6 border-t ${ | |
darkMode ? 'border-gray-700/50' : 'border-gray-200/50' | |
} flex-shrink-0`}> | |
<div className={`text-center text-xs ${ | |
darkMode ? 'text-gray-500' : 'text-gray-400' | |
}`}> | |
<p className="mb-1">📚 CA Study Assistant</p> | |
<p>{conversations.length} conversation{conversations.length !== 1 ? 's' : ''}</p> | |
</div> | |
</div> | |
</motion.div> | |
</> | |
)} | |
</AnimatePresence> | |
); | |
}; | |
export default Sidebar; |