mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 10:27:49 +00:00
Improve global search in AdminSettings
- Search across all settings regardless of active section - Search by translated names, descriptions, and categories - Add clear search button (X icon) - Show search results count
This commit is contained in:
80
src/components/admin/SettingsSearch.tsx
Normal file
80
src/components/admin/SettingsSearch.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { SearchIcon, CloseIcon } from './icons'
|
||||||
|
|
||||||
|
interface SettingsSearchProps {
|
||||||
|
searchQuery: string
|
||||||
|
setSearchQuery: (query: string) => void
|
||||||
|
resultsCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsSearch({ searchQuery, setSearchQuery, resultsCount }: SettingsSearchProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Search - desktop */}
|
||||||
|
<div className="relative hidden sm:block">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder={t('admin.settings.searchPlaceholder')}
|
||||||
|
className="w-48 lg:w-64 pl-10 pr-10 py-2 rounded-xl bg-dark-800 border border-dark-700 text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500 text-sm"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-dark-500">
|
||||||
|
<SearchIcon />
|
||||||
|
</div>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-dark-500 hover:text-dark-300 transition-colors"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsSearchMobile({ searchQuery, setSearchQuery }: Omit<SettingsSearchProps, 'resultsCount'>) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative mt-3 sm:hidden">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder={t('admin.settings.searchPlaceholder')}
|
||||||
|
className="w-full pl-10 pr-10 py-2 rounded-xl bg-dark-800 border border-dark-700 text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500 text-sm"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-dark-500">
|
||||||
|
<SearchIcon />
|
||||||
|
</div>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-dark-500 hover:text-dark-300 transition-colors"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsSearchResults({ searchQuery, resultsCount }: { searchQuery: string; resultsCount: number }) {
|
||||||
|
if (!searchQuery.trim()) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-3 flex items-center gap-2 text-sm">
|
||||||
|
<span className="text-dark-400">
|
||||||
|
{resultsCount > 0 ? `Найдено: ${resultsCount}` : 'Ничего не найдено'}
|
||||||
|
</span>
|
||||||
|
{resultsCount > 0 && (
|
||||||
|
<span className="text-dark-500">по запросу «{searchQuery}»</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
81
src/components/admin/SettingsSidebar.tsx
Normal file
81
src/components/admin/SettingsSidebar.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { BackIcon, StarIcon, CloseIcon, MENU_SECTIONS } from './index'
|
||||||
|
|
||||||
|
interface SettingsSidebarProps {
|
||||||
|
activeSection: string
|
||||||
|
setActiveSection: (section: string) => void
|
||||||
|
mobileMenuOpen: boolean
|
||||||
|
setMobileMenuOpen: (open: boolean) => void
|
||||||
|
favoritesCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsSidebar({
|
||||||
|
activeSection,
|
||||||
|
setActiveSection,
|
||||||
|
mobileMenuOpen,
|
||||||
|
setMobileMenuOpen,
|
||||||
|
favoritesCount
|
||||||
|
}: SettingsSidebarProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className={`
|
||||||
|
fixed lg:sticky lg:top-0 inset-y-0 left-0 z-50
|
||||||
|
w-64 h-screen bg-dark-900 border-r border-dark-700/50 flex-shrink-0
|
||||||
|
transform transition-transform duration-200 ease-in-out
|
||||||
|
${mobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
||||||
|
`}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-4 border-b border-dark-700/50">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link to="/admin" className="p-2 rounded-xl bg-dark-800 hover:bg-dark-700 transition-colors">
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-lg font-bold text-dark-100">{t('admin.settings.title')}</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
className="ml-auto p-2 rounded-xl bg-dark-800 hover:bg-dark-700 transition-colors lg:hidden"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Menu */}
|
||||||
|
<nav className="p-2 space-y-1 overflow-y-auto max-h-[calc(100vh-80px)]">
|
||||||
|
{MENU_SECTIONS.map((section, sectionIdx) => (
|
||||||
|
<div key={section.id}>
|
||||||
|
{sectionIdx > 0 && <div className="my-3 border-t border-dark-700/50" />}
|
||||||
|
{section.items.map((item) => {
|
||||||
|
const isActive = activeSection === item.id
|
||||||
|
const hasIcon = item.iconType === 'star'
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveSection(item.id)
|
||||||
|
setMobileMenuOpen(false)
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all ${
|
||||||
|
isActive
|
||||||
|
? 'bg-accent-500/10 text-accent-400'
|
||||||
|
: 'text-dark-400 hover:text-dark-200 hover:bg-dark-800/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{hasIcon && <StarIcon filled={isActive && item.id === 'favorites'} />}
|
||||||
|
<span className="font-medium">{t(`admin.settings.${item.id}`)}</span>
|
||||||
|
{item.id === 'favorites' && favoritesCount > 0 && (
|
||||||
|
<span className="ml-auto px-2 py-0.5 text-xs rounded-full bg-warning-500/20 text-warning-400">
|
||||||
|
{favoritesCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -72,20 +72,44 @@ export default function AdminSettings() {
|
|||||||
}))
|
}))
|
||||||
}, [currentMenuItem, allSettings, t])
|
}, [currentMenuItem, allSettings, t])
|
||||||
|
|
||||||
// Filter settings for search
|
// Helper to format setting key for translation lookup
|
||||||
|
const formatSettingKey = (name: string): string => {
|
||||||
|
return name
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.split(' ')
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter settings for search - GLOBAL search across all settings
|
||||||
const filteredSettings = useMemo(() => {
|
const filteredSettings = useMemo(() => {
|
||||||
if (!allSettings || !Array.isArray(allSettings) || !searchQuery) return []
|
if (!allSettings || !Array.isArray(allSettings) || !searchQuery) return []
|
||||||
|
|
||||||
const q = searchQuery.toLowerCase()
|
const q = searchQuery.toLowerCase().trim()
|
||||||
let settings = allSettings.filter((s: SettingDefinition) =>
|
if (!q) return []
|
||||||
currentMenuItem?.categories?.includes(s.category.key)
|
|
||||||
)
|
|
||||||
|
|
||||||
return settings.filter((s: SettingDefinition) =>
|
return allSettings.filter((s: SettingDefinition) => {
|
||||||
s.key.toLowerCase().includes(q) ||
|
// Search by key
|
||||||
s.name?.toLowerCase().includes(q)
|
if (s.key.toLowerCase().includes(q)) return true
|
||||||
)
|
|
||||||
}, [allSettings, currentMenuItem, searchQuery])
|
// Search by original name
|
||||||
|
if (s.name?.toLowerCase().includes(q)) return true
|
||||||
|
|
||||||
|
// Search by translated name
|
||||||
|
const formattedKey = formatSettingKey(s.name || s.key)
|
||||||
|
const translatedName = t(`admin.settings.settingNames.${formattedKey}`, formattedKey)
|
||||||
|
if (translatedName.toLowerCase().includes(q)) return true
|
||||||
|
|
||||||
|
// Search by description
|
||||||
|
if (s.hint?.description?.toLowerCase().includes(q)) return true
|
||||||
|
|
||||||
|
// Search by category
|
||||||
|
const categoryLabel = t(`admin.settings.categories.${s.category.key}`, s.category.key)
|
||||||
|
if (categoryLabel.toLowerCase().includes(q)) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}, [allSettings, searchQuery, t])
|
||||||
|
|
||||||
// Favorite settings
|
// Favorite settings
|
||||||
const favoriteSettings = useMemo(() => {
|
const favoriteSettings = useMemo(() => {
|
||||||
@@ -95,6 +119,19 @@ export default function AdminSettings() {
|
|||||||
|
|
||||||
// Render content based on active section
|
// Render content based on active section
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
|
// If searching, always show search results regardless of active section
|
||||||
|
if (searchQuery.trim()) {
|
||||||
|
return (
|
||||||
|
<SettingsTab
|
||||||
|
categories={[]}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
filteredSettings={filteredSettings}
|
||||||
|
isFavorite={isFavorite}
|
||||||
|
toggleFavorite={toggleFavorite}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
switch (activeSection) {
|
switch (activeSection) {
|
||||||
case 'branding':
|
case 'branding':
|
||||||
return <BrandingTab accentColor={themeColors?.accent} />
|
return <BrandingTab accentColor={themeColors?.accent} />
|
||||||
@@ -218,11 +255,19 @@ export default function AdminSettings() {
|
|||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder={t('admin.settings.searchPlaceholder')}
|
placeholder={t('admin.settings.searchPlaceholder')}
|
||||||
className="w-48 lg:w-64 pl-10 pr-4 py-2 rounded-xl bg-dark-800 border border-dark-700 text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500 text-sm"
|
className="w-48 lg:w-64 pl-10 pr-10 py-2 rounded-xl bg-dark-800 border border-dark-700 text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500 text-sm"
|
||||||
/>
|
/>
|
||||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-dark-500">
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-dark-500">
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</div>
|
</div>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-dark-500 hover:text-dark-300 transition-colors"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -233,12 +278,34 @@ export default function AdminSettings() {
|
|||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder={t('admin.settings.searchPlaceholder')}
|
placeholder={t('admin.settings.searchPlaceholder')}
|
||||||
className="w-full pl-10 pr-4 py-2 rounded-xl bg-dark-800 border border-dark-700 text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500 text-sm"
|
className="w-full pl-10 pr-10 py-2 rounded-xl bg-dark-800 border border-dark-700 text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500 text-sm"
|
||||||
/>
|
/>
|
||||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-dark-500">
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-dark-500">
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</div>
|
</div>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-dark-500 hover:text-dark-300 transition-colors"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Search results count */}
|
||||||
|
{searchQuery.trim() && (
|
||||||
|
<div className="mt-3 flex items-center gap-2 text-sm">
|
||||||
|
<span className="text-dark-400">
|
||||||
|
{filteredSettings.length > 0
|
||||||
|
? `Найдено: ${filteredSettings.length}`
|
||||||
|
: 'Ничего не найдено'}
|
||||||
|
</span>
|
||||||
|
{filteredSettings.length > 0 && (
|
||||||
|
<span className="text-dark-500">по запросу «{searchQuery}»</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
|||||||
Reference in New Issue
Block a user