Skip to main content
Glama

SFCC Development MCP Server

by taurgis
Search.tsx9.29 kB
import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { searchDocs, SearchResult } from '../utils/search'; import { SearchIcon } from './icons'; const Highlight: React.FC<{ text: string; query: string }> = ({ text, query }) => { if (!query) return <>{text}</>; const parts = text.split(new RegExp(`(${query})`, 'gi')); return ( <> {parts.map((part, i) => part.toLowerCase() === query.toLowerCase() ? ( <mark key={i} className="bg-orange-200 text-orange-800 font-semibold rounded px-0.5"> {part} </mark> ) : ( part ) )} </> ); }; const Search: React.FC = () => { const [query, setQuery] = useState(''); const [results, setResults] = useState<SearchResult[]>([]); const [isOpen, setIsOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const inputRef = useRef<HTMLInputElement>(null); const resultsRef = useRef<HTMLUListElement>(null); const navigate = useNavigate(); const location = useLocation(); const openSearch = useCallback(() => { setIsOpen(true); }, []); const closeSearch = useCallback(() => { setIsOpen(false); setQuery(''); setResults([]); setActiveIndex(-1); }, []); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key === 'k') { event.preventDefault(); openSearch(); } if (event.key === 'Escape' && isOpen) { closeSearch(); } if (isOpen && results.length > 0) { if (event.key === 'ArrowDown') { event.preventDefault(); setActiveIndex(prev => (prev + 1) % results.length); } else if (event.key === 'ArrowUp') { event.preventDefault(); setActiveIndex(prev => (prev - 1 + results.length) % results.length); } else if (event.key === 'Enter' && activeIndex >= 0) { event.preventDefault(); const result = results[activeIndex]; handleNavigation(result.path, result.heading, result.headingId); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isOpen, results.length, activeIndex, openSearch, closeSearch]); useEffect(() => { if (isOpen && inputRef.current) { inputRef.current.focus(); } }, [isOpen]); useEffect(() => { closeSearch(); }, [location.pathname, closeSearch]); useEffect(() => { if (activeIndex >= 0 && resultsRef.current) { const activeElement = resultsRef.current.children[activeIndex] as HTMLLIElement; if (activeElement) { activeElement.scrollIntoView({ block: 'nearest' }); } } }, [activeIndex]); const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => { const newQuery = e.target.value; setQuery(newQuery); if (newQuery.length > 1) { setResults(searchDocs(newQuery)); } else { setResults([]); } setActiveIndex(0); }; const handleNavigation = (path: string, heading?: string, headingId?: string) => { // Use the actual headingId if available, otherwise generate one from the heading let targetPath = path; let hashFragment = ''; if (headingId) { hashFragment = headingId; } else if (heading && heading !== 'Introduction' && heading !== path.split('/').pop()) { // Convert heading to a URL-safe ID as fallback const generatedId = heading .toLowerCase() .replace(/[^a-z0-9\s-]/g, '') // Remove special characters except spaces and hyphens .replace(/\s+/g, '-') // Replace spaces with hyphens .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens if (generatedId) { hashFragment = generatedId; } } // Navigate to the path first, then handle hash navigation if (hashFragment) { // If we have a hash fragment, navigate to path first then add hash navigate(targetPath, { replace: false }); // Use setTimeout to ensure navigation completes before adding hash setTimeout(() => { window.location.hash = `#${targetPath}#${hashFragment}`; }, 100); } else { navigate(targetPath); } closeSearch(); }; return ( <> <div className="relative"> <SearchIcon className="absolute top-1/2 left-3 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" /> <button type="button" onClick={openSearch} className="w-full bg-slate-100 border border-slate-200 rounded-lg py-2 pl-9 pr-12 sm:pr-3 text-sm text-left text-slate-500 hover:border-slate-300 transition-colors focus:outline-none focus:ring-2 focus:ring-orange-400" > Search... </button> <div className="absolute top-1/2 right-3 -translate-y-1/2 text-xs text-slate-400 border border-slate-300 rounded-md px-1.5 py-0.5 pointer-events-none hidden sm:block"> ⌘K </div> </div> {isOpen && ( <div className="fixed inset-0 z-50 flex justify-center items-start pt-4 sm:pt-20 p-4" aria-modal="true"> <div className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm" onClick={closeSearch}></div> <div className="relative bg-white w-full max-w-2xl rounded-lg shadow-lg max-h-[90vh] flex flex-col"> <div className="relative flex-shrink-0"> <SearchIcon className="absolute top-1/2 left-4 -translate-y-1/2 w-5 h-5 text-slate-400" /> <input ref={inputRef} type="text" value={query} onChange={handleSearch} placeholder="Search documentation..." className="w-full text-base sm:text-lg py-3 sm:py-4 pl-12 pr-4 border-b border-slate-200 focus:outline-none" /> </div> {query.length > 1 && ( <div className="flex-1 overflow-y-auto min-h-0"> {results.length > 0 ? ( <ul ref={resultsRef} className="p-3 sm:p-4 space-y-2"> {results.map((result, index) => ( <li key={`${result.path}-${result.heading}`}> <button onClick={() => handleNavigation(result.path, result.heading, result.headingId)} className={`w-full text-left p-3 rounded-md transition-colors ${activeIndex === index ? 'bg-orange-100' : 'hover:bg-slate-100'}`} > <div className="font-semibold text-slate-800 text-sm sm:text-base"> <Highlight text={result.pageTitle} query={query} /> </div> <div className="text-xs sm:text-sm text-slate-600 mb-1"> <Highlight text={result.heading} query={query} /> </div> <p className="text-xs sm:text-sm text-slate-500 line-clamp-2"> <Highlight text={result.snippet} query={query} /> </p> </button> </li> ))} </ul> ) : ( <p className="p-6 sm:p-8 text-center text-slate-500 text-sm sm:text-base">No results found for "{query}"</p> )} </div> )} </div> </div> )} </> ); }; export default Search;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/taurgis/sfcc-dev-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server