Spaces:
Sleeping
Sleeping
| import React, { useState } from 'react'; | |
| import { useAuth } from '../context/AuthContext'; | |
| function CategoryGroup({ category, activeChannel, onSelectChannel }) { | |
| const [collapsed, setCollapsed] = useState(false); | |
| return ( | |
| <div className="mb-1"> | |
| <button | |
| onClick={() => setCollapsed(!collapsed)} | |
| className="flex items-center gap-1 w-full px-2 py-1.5 text-xs font-semibold uppercase tracking-wide text-gray-400 hover:text-gray-200 transition-colors duration-200" | |
| > | |
| <svg | |
| className={`w-3 h-3 transition-transform duration-200 ${collapsed ? '-rotate-90' : ''}`} | |
| fill="currentColor" | |
| viewBox="0 0 20 20" | |
| > | |
| <path | |
| fillRule="evenodd" | |
| d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" | |
| clipRule="evenodd" | |
| /> | |
| </svg> | |
| {category.name} | |
| </button> | |
| {!collapsed && ( | |
| <div className="space-y-0.5"> | |
| {(category.channels || []).map((channel) => ( | |
| <ChannelItem | |
| key={channel.id} | |
| channel={channel} | |
| isActive={activeChannel?.id === channel.id} | |
| onClick={() => onSelectChannel(channel)} | |
| /> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| function ChannelItem({ channel, isActive, onClick }) { | |
| return ( | |
| <button | |
| onClick={onClick} | |
| className={`flex items-center gap-2 w-full px-2 py-1.5 mx-1 rounded-md text-sm transition-colors duration-200 ${ | |
| isActive | |
| ? 'bg-[#FFD700]/10 text-[#FFD700]' | |
| : 'text-gray-400 hover:text-gray-200 hover:bg-[#25252a]' | |
| }`} | |
| > | |
| {channel.type === 'voice' ? ( | |
| <svg className="w-4 h-4 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24"> | |
| <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5z" /> | |
| <path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" /> | |
| </svg> | |
| ) : ( | |
| <span className="text-base font-medium flex-shrink-0 leading-none">#</span> | |
| )} | |
| <span className="truncate">{channel.name}</span> | |
| </button> | |
| ); | |
| } | |
| function UserBar({ user, onOpenSettings }) { | |
| return ( | |
| <div className="p-2 bg-[#111114] flex items-center gap-2 mt-auto"> | |
| <div className="w-8 h-8 rounded-full overflow-hidden bg-[#2f2f35] flex-shrink-0 flex items-center justify-center"> | |
| {user?.avatar ? ( | |
| <img src={user.avatar} alt={user.username} className="w-full h-full object-cover" /> | |
| ) : ( | |
| <span className="text-xs font-bold text-gray-300"> | |
| {user?.username?.charAt(0)?.toUpperCase() || '?'} | |
| </span> | |
| )} | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-sm font-medium text-gray-100 truncate">{user?.username}</p> | |
| <p className="text-xs text-gray-500 truncate">Online</p> | |
| </div> | |
| <button | |
| onClick={onOpenSettings} | |
| className="p-1.5 rounded-md hover:bg-[#25252a] text-gray-400 hover:text-gray-200 transition-colors duration-200" | |
| title="User Settings" | |
| > | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}> | |
| <path | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" | |
| /> | |
| <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> | |
| </svg> | |
| </button> | |
| </div> | |
| ); | |
| } | |
| export default function ChannelSidebar({ | |
| server, | |
| activeChannel, | |
| onSelectChannel, | |
| onOpenServerSettings, | |
| onOpenInvite, | |
| dmMode, | |
| user, | |
| token, | |
| }) { | |
| const { logout } = useAuth(); | |
| const [showServerMenu, setShowServerMenu] = useState(false); | |
| if (dmMode) { | |
| return ( | |
| <div className="w-60 bg-[#18181b] flex flex-col border-r border-[#25252a]"> | |
| {/* DM Header */} | |
| <div className="h-12 px-4 flex items-center border-b border-[#25252a] shadow-sm"> | |
| <div className="relative flex-1"> | |
| <input | |
| type="text" | |
| placeholder="Find or start a conversation" | |
| className="w-full bg-[#0e0e10] text-sm text-gray-300 placeholder-gray-500 rounded-md px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-[#FFD700]/50 transition-colors duration-200" | |
| /> | |
| </div> | |
| </div> | |
| {/* DM List placeholder */} | |
| <div className="flex-1 overflow-y-auto p-2"> | |
| <h3 className="text-xs font-semibold uppercase tracking-wide text-gray-400 px-2 mb-2"> | |
| Direct Messages | |
| </h3> | |
| <div className="text-center text-gray-500 text-sm mt-8 px-4"> | |
| <p>No conversations yet</p> | |
| <p className="text-xs mt-1">Start a new DM to begin chatting</p> | |
| </div> | |
| </div> | |
| <UserBar user={user} onOpenSettings={() => {}} /> | |
| </div> | |
| ); | |
| } | |
| if (!server) { | |
| return ( | |
| <div className="w-60 bg-[#18181b] flex flex-col border-r border-[#25252a]"> | |
| <div className="flex-1 flex items-center justify-center"> | |
| <div className="w-6 h-6 border-2 border-[#FFD700] border-t-transparent rounded-full animate-spin" /> | |
| </div> | |
| <UserBar user={user} onOpenSettings={() => {}} /> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="w-60 bg-[#18181b] flex flex-col border-r border-[#25252a]"> | |
| {/* Server Header */} | |
| <div className="relative"> | |
| <button | |
| onClick={() => setShowServerMenu(!showServerMenu)} | |
| className="w-full h-12 px-4 flex items-center justify-between border-b border-[#25252a] hover:bg-[#25252a] transition-colors duration-200" | |
| > | |
| <span className="font-semibold text-gray-100 truncate">{server.name}</span> | |
| <svg | |
| className={`w-4 h-4 text-gray-400 transition-transform duration-200 ${showServerMenu ? 'rotate-180' : ''}`} | |
| fill="none" | |
| stroke="currentColor" | |
| viewBox="0 0 24 24" | |
| strokeWidth={2} | |
| > | |
| <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /> | |
| </svg> | |
| </button> | |
| {/* Server dropdown menu */} | |
| {showServerMenu && ( | |
| <> | |
| <div | |
| className="fixed inset-0 z-10" | |
| onClick={() => setShowServerMenu(false)} | |
| /> | |
| <div className="absolute top-12 left-2 right-2 z-20 bg-[#111114] rounded-lg shadow-xl border border-[#25252a] py-1.5"> | |
| <button | |
| onClick={() => { | |
| setShowServerMenu(false); | |
| onOpenInvite(); | |
| }} | |
| className="w-full px-3 py-2 text-sm text-[#FFD700] hover:bg-[#FFD700]/10 flex items-center gap-2 transition-colors duration-200" | |
| > | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}> | |
| <path strokeLinecap="round" strokeLinejoin="round" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" /> | |
| </svg> | |
| Invite People | |
| </button> | |
| <button | |
| onClick={() => { | |
| setShowServerMenu(false); | |
| onOpenServerSettings(); | |
| }} | |
| className="w-full px-3 py-2 text-sm text-gray-300 hover:bg-[#25252a] flex items-center gap-2 transition-colors duration-200" | |
| > | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}> | |
| <path | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" | |
| /> | |
| <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> | |
| </svg> | |
| Server Settings | |
| </button> | |
| <div className="border-t border-[#25252a] my-1" /> | |
| <button | |
| onClick={() => { | |
| setShowServerMenu(false); | |
| logout(); | |
| }} | |
| className="w-full px-3 py-2 text-sm text-red-400 hover:bg-red-500/10 flex items-center gap-2 transition-colors duration-200" | |
| > | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}> | |
| <path strokeLinecap="round" strokeLinejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /> | |
| </svg> | |
| Log Out | |
| </button> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| {/* Channels */} | |
| <div className="flex-1 overflow-y-auto py-2 px-1"> | |
| {server.categories && server.categories.length > 0 ? ( | |
| server.categories.map((cat) => ( | |
| <CategoryGroup | |
| key={cat.id || cat.name} | |
| category={cat} | |
| activeChannel={activeChannel} | |
| onSelectChannel={onSelectChannel} | |
| /> | |
| )) | |
| ) : server.channels && server.channels.length > 0 ? ( | |
| <div className="space-y-0.5"> | |
| {server.channels.map((channel) => ( | |
| <ChannelItem | |
| key={channel.id} | |
| channel={channel} | |
| isActive={activeChannel?.id === channel.id} | |
| onClick={() => onSelectChannel(channel)} | |
| /> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div className="text-center text-gray-500 text-sm mt-8 px-4"> | |
| <p>No channels yet</p> | |
| </div> | |
| )} | |
| </div> | |
| {/* User Bar */} | |
| <UserBar user={user} onOpenSettings={() => {}} /> | |
| </div> | |
| ); | |
| } | |