Move the serach box to teh sidebar

This commit is contained in:
Urtzi Alfaro
2025-09-24 19:40:51 +02:00
parent be1fec17c4
commit e978d04800
3 changed files with 174 additions and 104 deletions

View File

@@ -212,10 +212,10 @@ export const AppShell = forwardRef<AppShellRef, AppShellProps>(({
{/* Header */}
{shouldShowHeader && (
<Header
onMenuClick={toggleSidebar}
sidebarCollapsed={isSidebarCollapsed}
className="z-[var(--z-fixed)]"
/>
onMenuClick={toggleSidebar}
sidebarCollapsed={isSidebarCollapsed}
className="z-[var(--z-fixed)]"
/>
)}
<div className="flex flex-1 relative">

View File

@@ -13,7 +13,6 @@ import { NotificationPanel } from '../../ui/NotificationPanel/NotificationPanel'
import { CompactLanguageSelector } from '../../ui/LanguageSelector';
import {
Menu,
Search,
Bell,
X
} from 'lucide-react';
@@ -59,7 +58,7 @@ export interface HeaderProps {
}
export interface HeaderRef {
focusSearch: () => void;
// No search-related methods anymore
}
/**
@@ -100,63 +99,28 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
clearAll
} = useNotifications();
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false);
const searchInputRef = React.useRef<HTMLInputElement>(null);
const defaultSearchPlaceholder = searchPlaceholder || t('common:forms.search_placeholder', 'Search...');
// Focus search input
const focusSearch = useCallback(() => {
searchInputRef.current?.focus();
}, []);
// Expose ref methods
React.useImperativeHandle(ref, () => ({
focusSearch,
}), [focusSearch]);
// 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();
}, []);
// No search functions available anymore
}), []);
// Keyboard shortcuts
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') {
setIsNotificationPanelOpen(false);
if (isSearchFocused) {
searchInputRef.current?.blur();
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [focusSearch, isSearchFocused]);
}, []);
// Close menus when clicking outside
React.useEffect(() => {
@@ -238,71 +202,18 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
</div>
)}
{/* Search */}
{showSearch && isAuthenticated && (
<form
onSubmit={handleSearchSubmit}
className="hidden md:flex items-center flex-1 max-w-md mx-4"
>
<div className="relative w-full">
<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={defaultSearchPlaceholder}
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>
)}
</div>
</form>
{/* Space for potential future content */ }
{isAuthenticated && (
<div className="hidden md:flex items-center flex-1 max-w-md mx-4">
&nbsp; {/* Empty space to maintain layout consistency */}
</div>
)}
</div>
{/* Right section */}
{isAuthenticated && (
<div className="flex items-center gap-1">
{/* Mobile search */}
{showSearch && (
<Button
variant="ghost"
size="sm"
onClick={focusSearch}
className="md:hidden w-10 h-10 p-0 flex items-center justify-center"
aria-label={t('common:actions.search', 'Search')}
>
<Search className="h-5 w-5" />
</Button>
)}
{/* Placeholder for potential future items */ }
{/* Language selector */}
<CompactLanguageSelector className="w-auto min-w-[60px]" />

View File

@@ -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">