Move the serach box to teh sidebar
This commit is contained in:
@@ -33,7 +33,8 @@ import {
|
||||
Menu,
|
||||
LogOut,
|
||||
MoreHorizontal,
|
||||
X
|
||||
X,
|
||||
Search
|
||||
} from 'lucide-react';
|
||||
|
||||
export interface SidebarProps {
|
||||
@@ -143,6 +144,9 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
|
||||
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
|
||||
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const sidebarRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Get subscription-aware navigation routes
|
||||
@@ -260,6 +264,29 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
}
|
||||
}, [navigate, onClose]);
|
||||
|
||||
// Handle search
|
||||
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(e.target.value);
|
||||
}, []);
|
||||
|
||||
const handleSearchSubmit = useCallback((e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (searchValue.trim()) {
|
||||
// TODO: Implement search functionality
|
||||
console.log('Search:', searchValue);
|
||||
}
|
||||
}, [searchValue]);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setSearchValue('');
|
||||
searchInputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Focus search input
|
||||
const focusSearch = useCallback(() => {
|
||||
searchInputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Handle logout
|
||||
const handleLogout = useCallback(async () => {
|
||||
await logout();
|
||||
@@ -389,6 +416,42 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
}
|
||||
}, [isProfileMenuOpen]);
|
||||
|
||||
// Keyboard shortcuts for search and other functionality
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Cmd/Ctrl + K for search
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
focusSearch();
|
||||
}
|
||||
|
||||
// Escape to close menus
|
||||
if (e.key === 'Escape') {
|
||||
setIsProfileMenuOpen(false);
|
||||
if (onClose) onClose(); // Close mobile sidebar
|
||||
if (isSearchFocused) {
|
||||
searchInputRef.current?.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [focusSearch, isSearchFocused, setIsProfileMenuOpen, onClose]);
|
||||
|
||||
// Close search and menus when clicking outside
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Element;
|
||||
if (!target.closest('[data-profile-menu]') && !target.closest('[data-search]')) {
|
||||
setIsProfileMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Render submenu overlay for collapsed sidebar
|
||||
const renderSubmenuOverlay = (item: NavigationItem) => {
|
||||
if (!item.children || item.children.length === 0) return null;
|
||||
@@ -597,6 +660,55 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
)}
|
||||
aria-label={t('common:accessibility.menu', 'Main navigation')}
|
||||
>
|
||||
{/* Search */}
|
||||
{!isCollapsed && (
|
||||
<div className="px-4 pt-4" data-search>
|
||||
<form
|
||||
onSubmit={handleSearchSubmit}
|
||||
className="relative"
|
||||
>
|
||||
<div className="absolute left-3 top-0 bottom-0 flex items-center pointer-events-none">
|
||||
<Search className="h-4 w-4 text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
value={searchValue}
|
||||
onChange={handleSearchChange}
|
||||
onFocus={() => setIsSearchFocused(true)}
|
||||
onBlur={() => setIsSearchFocused(false)}
|
||||
placeholder={t('common:forms.search_placeholder', 'Search...')}
|
||||
className={clsx(
|
||||
'w-full pl-10 pr-12 py-2.5 text-sm',
|
||||
'bg-[var(--bg-secondary)] border border-[var(--border-primary)]',
|
||||
'rounded-lg transition-colors duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20',
|
||||
'focus:border-[var(--color-primary)]',
|
||||
'placeholder:text-[var(--text-tertiary)]',
|
||||
'h-9'
|
||||
)}
|
||||
aria-label={t('common:accessibility.search', 'Search in the application')}
|
||||
/>
|
||||
{searchValue ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearSearch}
|
||||
className="absolute right-3 top-0 bottom-0 flex items-center p-1 hover:bg-[var(--bg-tertiary)] rounded-full transition-colors"
|
||||
aria-label={t('common:actions.clear', 'Clear search')}
|
||||
>
|
||||
<X className="h-3 w-3 text-[var(--text-tertiary)]" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="absolute right-3 top-0 bottom-0 flex items-center pointer-events-none">
|
||||
<kbd className="hidden lg:inline-flex items-center justify-center h-5 px-1.5 text-xs text-[var(--text-tertiary)] font-mono bg-[var(--bg-tertiary)] rounded border border-[var(--border-primary)]">
|
||||
⌘K
|
||||
</kbd>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className={clsx('flex-1 overflow-y-auto', isCollapsed ? 'px-2 py-4' : 'p-4')}>
|
||||
<ul className={clsx(isCollapsed ? 'space-y-2' : 'space-y-2')}>
|
||||
@@ -750,6 +862,53 @@ export const Sidebar = forwardRef<SidebarRef, SidebarProps>(({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mobile search - always visible in mobile view */}
|
||||
<div className="p-4 border-b border-[var(--border-primary)]">
|
||||
<form
|
||||
onSubmit={handleSearchSubmit}
|
||||
className="relative"
|
||||
>
|
||||
<div className="absolute left-3 top-0 bottom-0 flex items-center pointer-events-none">
|
||||
<Search className="h-4 w-4 text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
value={searchValue}
|
||||
onChange={handleSearchChange}
|
||||
onFocus={() => setIsSearchFocused(true)}
|
||||
onBlur={() => setIsSearchFocused(false)}
|
||||
placeholder={t('common:forms.search_placeholder', 'Search...')}
|
||||
className={clsx(
|
||||
'w-full pl-10 pr-12 py-2.5 text-sm',
|
||||
'bg-[var(--bg-secondary)] border border-[var(--border-primary)]',
|
||||
'rounded-lg transition-colors duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20',
|
||||
'focus:border-[var(--color-primary)]',
|
||||
'placeholder:text-[var(--text-tertiary)]',
|
||||
'h-9'
|
||||
)}
|
||||
aria-label={t('common:accessibility.search', 'Search in the application')}
|
||||
/>
|
||||
{searchValue ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearSearch}
|
||||
className="absolute right-3 top-0 bottom-0 flex items-center p-1 hover:bg-[var(--bg-tertiary)] rounded-full transition-colors"
|
||||
aria-label={t('common:actions.clear', 'Clear search')}
|
||||
>
|
||||
<X className="h-3 w-3 text-[var(--text-tertiary)]" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="absolute right-3 top-0 bottom-0 flex items-center pointer-events-none">
|
||||
<kbd className="hidden lg:inline-flex items-center justify-center h-5 px-1.5 text-xs text-[var(--text-tertiary)] font-mono bg-[var(--bg-tertiary)] rounded border border-[var(--border-primary)]">
|
||||
⌘K
|
||||
</kbd>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-4 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
|
||||
<ul className="space-y-2 pb-4">
|
||||
|
||||
Reference in New Issue
Block a user