- Rename directory keep-notes -> memento-note with all code references - Prisma: SQLite -> PostgreSQL (both app and MCP server schemas) - Sync MCP schema with main app (add missing fields, relations, indexes) - Delete 17 SQLite migrations (clean slate for PostgreSQL) - Remove SQLite dependencies (@libsql/client, better-sqlite3, etc.) - Fix MCP server: hardcoded Windows DB paths -> DATABASE_URL env var - Fix MCP server: .dockerignore excluded index-sse.js (SSE mode broken) - MCP Dockerfile: node:20 -> node:22 - Docker Compose: add postgres service, remove SQLite volume - Generate favicon.ico, icon-192.png, icon-512.png, apple-icon.png - Update layout.tsx icons and manifest.json for PNG icons - Update all .env files for PostgreSQL - Rewrite README.md with updated sections - Remove mcp-server/node_modules and prisma/client-generated from git tracking Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
102 lines
2.6 KiB
TypeScript
102 lines
2.6 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { Search, X } from 'lucide-react'
|
|
import { Input } from '@/components/ui/input'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
export interface Section {
|
|
id: string
|
|
label: string
|
|
description: string
|
|
icon: React.ReactNode
|
|
href: string
|
|
}
|
|
|
|
interface SettingsSearchProps {
|
|
sections: Section[]
|
|
onFilter: (filteredSections: Section[]) => void
|
|
placeholder?: string
|
|
className?: string
|
|
}
|
|
|
|
export function SettingsSearch({
|
|
sections,
|
|
onFilter,
|
|
placeholder = 'Search settings...',
|
|
className
|
|
}: SettingsSearchProps) {
|
|
const [query, setQuery] = useState('')
|
|
const [filteredSections, setFilteredSections] = useState<Section[]>(sections)
|
|
|
|
useEffect(() => {
|
|
if (!query.trim()) {
|
|
setFilteredSections(sections)
|
|
return
|
|
}
|
|
|
|
const queryLower = query.toLowerCase()
|
|
const filtered = sections.filter(section => {
|
|
const labelMatch = section.label.toLowerCase().includes(queryLower)
|
|
const descMatch = section.description.toLowerCase().includes(queryLower)
|
|
return labelMatch || descMatch
|
|
})
|
|
setFilteredSections(filtered)
|
|
}, [query, sections])
|
|
|
|
const handleClearSearch = () => {
|
|
setQuery('')
|
|
setFilteredSections(sections)
|
|
}
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
handleClearSearch()
|
|
e.stopPropagation()
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
return () => {
|
|
document.removeEventListener('keydown', handleKeyDown)
|
|
}
|
|
}, [])
|
|
|
|
const handleSearchChange = (value: string) => {
|
|
setQuery(value)
|
|
}
|
|
|
|
const hasResults = query.trim() && filteredSections.length < sections.length
|
|
const isEmptySearch = query.trim() && filteredSections.length === 0
|
|
|
|
return (
|
|
<div className={cn('relative', className)}>
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
<Input
|
|
type="text"
|
|
value={query}
|
|
onChange={(e) => handleSearchChange(e.target.value)}
|
|
placeholder={placeholder}
|
|
className="pl-10"
|
|
autoFocus
|
|
/>
|
|
{hasResults && (
|
|
<button
|
|
type="button"
|
|
onClick={handleClearSearch}
|
|
className="absolute right-2 top-1/2 text-gray-400 hover:text-gray-600"
|
|
aria-label="Clear search"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
{isEmptySearch && (
|
|
<div className="absolute top-full left-0 right-0 mt-1 p-2 bg-white rounded-lg shadow-lg border z-50">
|
|
<p className="text-sm text-gray-600">No settings found</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|