gilded / client /src /components /ChannelSidebar.jsx
OmegaOne
Upload 41 files
0a8fe79 verified
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>
);
}