diff --git a/src/components/admin/SettingsSearch.tsx b/src/components/admin/SettingsSearch.tsx index 4be0c80..033d744 100644 --- a/src/components/admin/SettingsSearch.tsx +++ b/src/components/admin/SettingsSearch.tsx @@ -1,54 +1,222 @@ +import { useState, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { SettingDefinition } from '../../api/adminSettings'; import { SearchIcon, CloseIcon } from './icons'; +import { formatSettingKey } from './utils'; interface SettingsSearchProps { searchQuery: string; setSearchQuery: (query: string) => void; resultsCount?: number; + allSettings?: SettingDefinition[]; + onSelectSetting?: (setting: SettingDefinition) => void; } -export function SettingsSearch({ searchQuery, setSearchQuery }: SettingsSearchProps) { +export function SettingsSearch({ + searchQuery, + setSearchQuery, + allSettings, + onSelectSetting, +}: SettingsSearchProps) { const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + const [highlightedIndex, setHighlightedIndex] = useState(0); + const containerRef = useRef(null); + const inputRef = useRef(null); + + // Filter settings for autocomplete + const suggestions = + searchQuery.trim() && allSettings + ? allSettings + .filter((s) => { + const q = searchQuery.toLowerCase().trim(); + if (s.key.toLowerCase().includes(q)) return true; + if (s.name?.toLowerCase().includes(q)) return true; + const formattedKey = formatSettingKey(s.name || s.key); + const translatedName = t(`admin.settings.settingNames.${formattedKey}`, formattedKey); + if (translatedName.toLowerCase().includes(q)) return true; + if (s.hint?.description?.toLowerCase().includes(q)) return true; + const categoryLabel = t(`admin.settings.categories.${s.category.key}`, s.category.key); + if (categoryLabel.toLowerCase().includes(q)) return true; + return false; + }) + .slice(0, 8) + : []; + + // Close on click outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // Reset highlighted index when suggestions change + useEffect(() => { + setHighlightedIndex(0); + }, [suggestions.length, searchQuery]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!isOpen || suggestions.length === 0) return; + + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setHighlightedIndex((i) => (i + 1) % suggestions.length); + break; + case 'ArrowUp': + e.preventDefault(); + setHighlightedIndex((i) => (i - 1 + suggestions.length) % suggestions.length); + break; + case 'Enter': + e.preventDefault(); + if (suggestions[highlightedIndex]) { + handleSelect(suggestions[highlightedIndex]); + } + break; + case 'Escape': + setIsOpen(false); + inputRef.current?.blur(); + break; + } + }; + + const handleSelect = (setting: SettingDefinition) => { + setIsOpen(false); + setSearchQuery(setting.name || setting.key); + onSelectSetting?.(setting); + }; + + const getSettingDisplayName = (setting: SettingDefinition) => { + const formattedKey = formatSettingKey(setting.name || setting.key); + return t(`admin.settings.settingNames.${formattedKey}`, formattedKey); + }; return ( - <> - {/* Search - desktop */} -
- setSearchQuery(e.target.value)} - placeholder={t('admin.settings.searchPlaceholder')} - className="w-48 rounded-xl border border-dark-700 bg-dark-800 py-2 pl-10 pr-10 text-sm text-dark-100 placeholder-dark-500 focus:border-accent-500 focus:outline-none lg:w-64" - /> -
- -
- {searchQuery && ( - - )} +
+ { + setSearchQuery(e.target.value); + setIsOpen(true); + }} + onFocus={() => setIsOpen(true)} + onKeyDown={handleKeyDown} + placeholder={t('admin.settings.searchPlaceholder')} + className="w-48 rounded-xl border border-dark-700 bg-dark-800 py-2 pl-10 pr-10 text-sm text-dark-100 placeholder-dark-500 focus:border-accent-500 focus:outline-none lg:w-64" + /> +
+
- + {searchQuery && ( + + )} + + {/* Autocomplete dropdown */} + {isOpen && suggestions.length > 0 && ( +
+ {suggestions.map((setting, index) => ( + + ))} +
+ )} +
); } export function SettingsSearchMobile({ searchQuery, setSearchQuery, + allSettings, + onSelectSetting, }: Omit) { const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + const [highlightedIndex, setHighlightedIndex] = useState(0); + const containerRef = useRef(null); + + // Filter settings for autocomplete + const suggestions = + searchQuery.trim() && allSettings + ? allSettings + .filter((s) => { + const q = searchQuery.toLowerCase().trim(); + if (s.key.toLowerCase().includes(q)) return true; + if (s.name?.toLowerCase().includes(q)) return true; + const formattedKey = formatSettingKey(s.name || s.key); + const translatedName = t(`admin.settings.settingNames.${formattedKey}`, formattedKey); + if (translatedName.toLowerCase().includes(q)) return true; + if (s.hint?.description?.toLowerCase().includes(q)) return true; + const categoryLabel = t(`admin.settings.categories.${s.category.key}`, s.category.key); + if (categoryLabel.toLowerCase().includes(q)) return true; + return false; + }) + .slice(0, 8) + : []; + + // Close on click outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + useEffect(() => { + setHighlightedIndex(0); + }, [suggestions.length, searchQuery]); + + const handleSelect = (setting: SettingDefinition) => { + setIsOpen(false); + setSearchQuery(setting.name || setting.key); + onSelectSetting?.(setting); + }; + + const getSettingDisplayName = (setting: SettingDefinition) => { + const formattedKey = formatSettingKey(setting.name || setting.key); + return t(`admin.settings.settingNames.${formattedKey}`, formattedKey); + }; return ( -
+
setSearchQuery(e.target.value)} + onChange={(e) => { + setSearchQuery(e.target.value); + setIsOpen(true); + }} + onFocus={() => setIsOpen(true)} placeholder={t('admin.settings.searchPlaceholder')} className="w-full rounded-xl border border-dark-700 bg-dark-800 py-2 pl-10 pr-10 text-sm text-dark-100 placeholder-dark-500 focus:border-accent-500 focus:outline-none" /> @@ -57,12 +225,37 @@ export function SettingsSearchMobile({
{searchQuery && ( )} + + {/* Autocomplete dropdown */} + {isOpen && suggestions.length > 0 && ( +
+ {suggestions.map((setting, index) => ( + + ))} +
+ )}
); } diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index b7d3b16..1ac3640 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo, useEffect, useCallback } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { adminSettingsApi, SettingDefinition } from '../api/adminSettings'; @@ -17,6 +17,18 @@ import { SettingsSearchResults, } from '../components/admin/SettingsSearch'; +// Find section ID by category key +function findSectionByCategory(categoryKey: string): string | null { + for (const section of MENU_SECTIONS) { + for (const item of section.items) { + if (item.categories?.includes(categoryKey)) { + return item.id; + } + } + } + return null; +} + export default function AdminSettings() { const { t } = useTranslation(); @@ -110,6 +122,19 @@ export default function AdminSettings() { return allSettings.filter((s: SettingDefinition) => favorites.includes(s.key)); }, [allSettings, favorites]); + // Handle setting selection from autocomplete + const handleSelectSetting = useCallback( + (setting: SettingDefinition) => { + const sectionId = findSectionByCategory(setting.category.key); + if (sectionId) { + setActiveSection(sectionId); + } + // Set search to setting key to filter to just this setting + setSearchQuery(setting.key); + }, + [setActiveSection, setSearchQuery], + ); + // Render content based on active section const renderContent = () => { // If searching, always show search results regardless of active section @@ -175,7 +200,12 @@ export default function AdminSettings() { setActiveSection={setActiveSection} favoritesCount={favorites.length} /> - + {renderContent()}
@@ -247,6 +277,8 @@ export default function AdminSettings() { searchQuery={searchQuery} setSearchQuery={setSearchQuery} resultsCount={filteredSettings.length} + allSettings={allSettings} + onSelectSetting={handleSelectSetting} />