diff --git a/src/components/GlobalSearch.tsx b/src/components/GlobalSearch.tsx index e32a19c..f5e2f90 100644 --- a/src/components/GlobalSearch.tsx +++ b/src/components/GlobalSearch.tsx @@ -1,60 +1,19 @@ -import { useState, useMemo, useEffect } from 'react' -import { useKV } from '@/hooks/use-kv' +import { CommandDialog, CommandInput, CommandList } from '@/components/ui/command' +import { EmptyState } from '@/components/global-search/EmptyState' +import { RecentSearches } from '@/components/global-search/RecentSearches' +import { SearchResults } from '@/components/global-search/SearchResults' +import { useGlobalSearchData } from '@/components/global-search/useGlobalSearchData' import { - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - CommandSeparator, -} from '@/components/ui/command' -import { - Code, - Database, - Tree, - PaintBrush, - Flask, - Play, - BookOpen, - Cube, - FlowArrow, - Wrench, - FileText, - Gear, - DeviceMobile, - Faders, - ChartBar, - Image, - File, - Folder, - MagnifyingGlass, - ClockCounterClockwise, - X, - Lightbulb, -} from '@phosphor-icons/react' -import { ProjectFile, PrismaModel, ComponentNode, ComponentTree, Workflow, Lambda, PlaywrightTest, StorybookStory, UnitTest } from '@/types/project' -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' - -interface SearchResult { - id: string - title: string - subtitle?: string - category: string - icon: React.ReactNode - action: () => void - tags?: string[] -} - -interface SearchHistoryItem { - id: string - query: string - timestamp: number - resultId?: string - resultTitle?: string - resultCategory?: string -} + ComponentNode, + ComponentTree, + Lambda, + PlaywrightTest, + PrismaModel, + ProjectFile, + StorybookStory, + UnitTest, + Workflow, +} from '@/types/project' interface GlobalSearchProps { open: boolean @@ -87,361 +46,18 @@ export function GlobalSearch({ onNavigate, onFileSelect, }: GlobalSearchProps) { - const [searchQuery, setSearchQuery] = useState('') - const [searchHistory, setSearchHistory] = useKV('search-history', []) - - useEffect(() => { - if (!open) { - setSearchQuery('') - } - }, [open]) - - const addToHistory = (query: string, result?: SearchResult) => { - if (!query.trim()) return - - const historyItem: SearchHistoryItem = { - id: `history-${Date.now()}`, - query: query.trim(), - timestamp: Date.now(), - resultId: result?.id, - resultTitle: result?.title, - resultCategory: result?.category, - } - - setSearchHistory((currentHistory) => { - const filtered = (currentHistory || []).filter( - (item) => item.query.toLowerCase() !== query.toLowerCase() - ) - return [historyItem, ...filtered].slice(0, 20) - }) - } - - const clearHistory = () => { - setSearchHistory([]) - } - - const removeHistoryItem = (id: string) => { - setSearchHistory((currentHistory) => - (currentHistory || []).filter((item) => item.id !== id) - ) - } - - const allResults = useMemo(() => { - const results: SearchResult[] = [] - - results.push({ - id: 'nav-dashboard', - title: 'Dashboard', - subtitle: 'View project overview and statistics', - category: 'Navigation', - icon: , - action: () => onNavigate('dashboard'), - tags: ['home', 'overview', 'stats', 'metrics'], - }) - - results.push({ - id: 'nav-code', - title: 'Code Editor', - subtitle: 'Edit project files with Monaco', - category: 'Navigation', - icon: , - action: () => onNavigate('code'), - tags: ['editor', 'monaco', 'typescript', 'javascript'], - }) - - results.push({ - id: 'nav-models', - title: 'Models', - subtitle: 'Design Prisma database models', - category: 'Navigation', - icon: , - action: () => onNavigate('models'), - tags: ['prisma', 'database', 'schema', 'orm'], - }) - - results.push({ - id: 'nav-components', - title: 'Components', - subtitle: 'Build React components', - category: 'Navigation', - icon: , - action: () => onNavigate('components'), - tags: ['react', 'mui', 'ui', 'design'], - }) - - results.push({ - id: 'nav-component-trees', - title: 'Component Trees', - subtitle: 'Manage component hierarchies', - category: 'Navigation', - icon: , - action: () => onNavigate('component-trees'), - tags: ['hierarchy', 'structure', 'layout'], - }) - - results.push({ - id: 'nav-workflows', - title: 'Workflows', - subtitle: 'Design n8n-style workflows', - category: 'Navigation', - icon: , - action: () => onNavigate('workflows'), - tags: ['automation', 'n8n', 'flow', 'pipeline'], - }) - - results.push({ - id: 'nav-lambdas', - title: 'Lambdas', - subtitle: 'Create serverless functions', - category: 'Navigation', - icon: , - action: () => onNavigate('lambdas'), - tags: ['serverless', 'functions', 'api'], - }) - - results.push({ - id: 'nav-styling', - title: 'Styling', - subtitle: 'Design themes and colors', - category: 'Navigation', - icon: , - action: () => onNavigate('styling'), - tags: ['theme', 'colors', 'css', 'design'], - }) - - results.push({ - id: 'nav-flask', - title: 'Flask API', - subtitle: 'Configure Flask backend', - category: 'Navigation', - icon: , - action: () => onNavigate('flask'), - tags: ['python', 'backend', 'api', 'rest'], - }) - - results.push({ - id: 'nav-playwright', - title: 'Playwright Tests', - subtitle: 'E2E testing configuration', - category: 'Navigation', - icon: , - action: () => onNavigate('playwright'), - tags: ['testing', 'e2e', 'automation'], - }) - - results.push({ - id: 'nav-storybook', - title: 'Storybook', - subtitle: 'Component documentation', - category: 'Navigation', - icon: , - action: () => onNavigate('storybook'), - tags: ['documentation', 'components', 'stories'], - }) - - results.push({ - id: 'nav-unit-tests', - title: 'Unit Tests', - subtitle: 'Configure unit testing', - category: 'Navigation', - icon: , - action: () => onNavigate('unit-tests'), - tags: ['testing', 'jest', 'vitest'], - }) - - results.push({ - id: 'nav-errors', - title: 'Error Repair', - subtitle: 'Auto-detect and fix errors', - category: 'Navigation', - icon: , - action: () => onNavigate('errors'), - tags: ['debugging', 'errors', 'fixes'], - }) - - results.push({ - id: 'nav-docs', - title: 'Documentation', - subtitle: 'View project documentation', - category: 'Navigation', - icon: , - action: () => onNavigate('docs'), - tags: ['readme', 'guide', 'help'], - }) - - results.push({ - id: 'nav-sass', - title: 'Sass Styles', - subtitle: 'Custom Sass styling', - category: 'Navigation', - icon: , - action: () => onNavigate('sass'), - tags: ['sass', 'scss', 'styles', 'css'], - }) - - results.push({ - id: 'nav-favicon', - title: 'Favicon Designer', - subtitle: 'Design app icons', - category: 'Navigation', - icon: , - action: () => onNavigate('favicon'), - tags: ['icon', 'logo', 'design'], - }) - - results.push({ - id: 'nav-settings', - title: 'Settings', - subtitle: 'Configure Next.js and npm', - category: 'Navigation', - icon: , - action: () => onNavigate('settings'), - tags: ['config', 'nextjs', 'npm', 'packages'], - }) - - results.push({ - id: 'nav-pwa', - title: 'PWA Settings', - subtitle: 'Progressive Web App config', - category: 'Navigation', - icon: , - action: () => onNavigate('pwa'), - tags: ['mobile', 'install', 'offline'], - }) - - results.push({ - id: 'nav-features', - title: 'Feature Toggles', - subtitle: 'Enable or disable features', - category: 'Navigation', - icon: , - action: () => onNavigate('features'), - tags: ['settings', 'toggles', 'enable'], - }) - - results.push({ - id: 'nav-ideas', - title: 'Feature Ideas', - subtitle: 'Brainstorm and organize ideas', - category: 'Navigation', - icon: , - action: () => onNavigate('ideas'), - tags: ['brainstorm', 'ideas', 'planning'], - }) - - files.forEach((file) => { - results.push({ - id: `file-${file.id}`, - title: file.name, - subtitle: file.path, - category: 'Files', - icon: , - action: () => { - onNavigate('code') - onFileSelect(file.id) - }, - tags: [file.language, file.path, 'code', 'file'], - }) - }) - - models.forEach((model) => { - results.push({ - id: `model-${model.id}`, - title: model.name, - subtitle: `${model.fields.length} fields`, - category: 'Models', - icon: , - action: () => onNavigate('models', model.id), - tags: ['prisma', 'database', 'schema', model.name.toLowerCase()], - }) - }) - - components.forEach((component) => { - results.push({ - id: `component-${component.id}`, - title: component.name, - subtitle: component.type, - category: 'Components', - icon: , - action: () => onNavigate('components', component.id), - tags: ['react', 'component', component.type.toLowerCase(), component.name.toLowerCase()], - }) - }) - - componentTrees.forEach((tree) => { - results.push({ - id: `tree-${tree.id}`, - title: tree.name, - subtitle: tree.description || `${tree.rootNodes.length} root nodes`, - category: 'Component Trees', - icon: , - action: () => onNavigate('component-trees', tree.id), - tags: ['hierarchy', 'structure', tree.name.toLowerCase()], - }) - }) - - workflows.forEach((workflow) => { - results.push({ - id: `workflow-${workflow.id}`, - title: workflow.name, - subtitle: workflow.description || `${workflow.nodes.length} nodes`, - category: 'Workflows', - icon: , - action: () => onNavigate('workflows', workflow.id), - tags: ['automation', 'flow', workflow.name.toLowerCase()], - }) - }) - - lambdas.forEach((lambda) => { - results.push({ - id: `lambda-${lambda.id}`, - title: lambda.name, - subtitle: lambda.description || lambda.runtime, - category: 'Lambdas', - icon: , - action: () => onNavigate('lambdas', lambda.id), - tags: ['serverless', 'function', lambda.runtime, lambda.name.toLowerCase()], - }) - }) - - playwrightTests.forEach((test) => { - results.push({ - id: `playwright-${test.id}`, - title: test.name, - subtitle: test.description, - category: 'Playwright Tests', - icon: , - action: () => onNavigate('playwright', test.id), - tags: ['testing', 'e2e', test.name.toLowerCase()], - }) - }) - - storybookStories.forEach((story) => { - results.push({ - id: `storybook-${story.id}`, - title: story.storyName, - subtitle: story.componentName, - category: 'Storybook Stories', - icon: , - action: () => onNavigate('storybook', story.id), - tags: ['documentation', 'story', story.componentName.toLowerCase()], - }) - }) - - unitTests.forEach((test) => { - results.push({ - id: `unit-test-${test.id}`, - title: test.name, - subtitle: test.description, - category: 'Unit Tests', - icon: , - action: () => onNavigate('unit-tests', test.id), - tags: ['testing', 'unit', test.name.toLowerCase()], - }) - }) - - return results - }, [ + const { + searchQuery, + setSearchQuery, + recentSearches, + groupedResults, + clearHistory, + removeHistoryItem, + handleSelect, + handleHistorySelect, + } = useGlobalSearchData({ + open, + onOpenChange, files, models, components, @@ -453,201 +69,26 @@ export function GlobalSearch({ unitTests, onNavigate, onFileSelect, - ]) - - const filteredResults = useMemo(() => { - if (!searchQuery.trim()) { - return [] - } - - const query = searchQuery.toLowerCase().trim() - const queryWords = query.split(/\s+/) - - return allResults - .map((result) => { - let score = 0 - const titleLower = result.title.toLowerCase() - const subtitleLower = result.subtitle?.toLowerCase() || '' - const categoryLower = result.category.toLowerCase() - const tagsLower = result.tags?.map(t => t.toLowerCase()) || [] - - if (titleLower === query) score += 100 - else if (titleLower.startsWith(query)) score += 50 - else if (titleLower.includes(query)) score += 30 - - if (subtitleLower.includes(query)) score += 20 - if (categoryLower.includes(query)) score += 15 - - tagsLower.forEach(tag => { - if (tag === query) score += 40 - else if (tag.includes(query)) score += 10 - }) - - queryWords.forEach(word => { - if (titleLower.includes(word)) score += 5 - if (subtitleLower.includes(word)) score += 3 - if (tagsLower.some(tag => tag.includes(word))) score += 2 - }) - - return { result, score } - }) - .filter(({ score }) => score > 0) - .sort((a, b) => b.score - a.score) - .slice(0, 50) - .map(({ result }) => result) - }, [allResults, searchQuery]) - - const recentSearches = useMemo(() => { - const safeHistory = searchHistory || [] - return safeHistory - .slice(0, 10) - .map((item) => { - const result = allResults.find((r) => r.id === item.resultId) - return { - historyItem: item, - result, - } - }) - }, [searchHistory, allResults]) - - const groupedResults = useMemo(() => { - const groups: Record = {} - filteredResults.forEach((result) => { - if (!groups[result.category]) { - groups[result.category] = [] - } - groups[result.category].push(result) - }) - return groups - }, [filteredResults]) - - const handleSelect = (result: SearchResult) => { - addToHistory(searchQuery, result) - result.action() - onOpenChange(false) - } - - const handleHistorySelect = (historyItem: SearchHistoryItem, result?: SearchResult) => { - if (result) { - result.action() - onOpenChange(false) - } else { - setSearchQuery(historyItem.query) - } - } + }) return ( - - -
- -

No results found

-
-
- - {!searchQuery.trim() && recentSearches.length > 0 && ( - <> - - Recent Searches - - - } - > - {recentSearches.map(({ historyItem, result }) => ( - handleHistorySelect(historyItem, result)} - className="flex items-center gap-3 px-4 py-3 cursor-pointer group" - > -
- -
-
- {result ? ( - <> -
{result.title}
-
- Searched: {historyItem.query} -
- - ) : ( - <> -
{historyItem.query}
-
- Search again -
- - )} -
- {result && ( - - {result.category} - - )} - -
- ))} -
- - + + {!searchQuery.trim() && ( + )} - - {Object.entries(groupedResults).map(([category, results], index) => ( -
- {index > 0 && } - - {results.map((result) => ( - handleSelect(result)} - className="flex items-center gap-3 px-4 py-3 cursor-pointer" - > -
- {result.icon} -
-
-
{result.title}
- {result.subtitle && ( -
- {result.subtitle} -
- )} -
- - {category} - -
- ))} -
-
- ))} +
) diff --git a/src/components/global-search/EmptyState.tsx b/src/components/global-search/EmptyState.tsx new file mode 100644 index 0000000..6973576 --- /dev/null +++ b/src/components/global-search/EmptyState.tsx @@ -0,0 +1,13 @@ +import { MagnifyingGlass } from '@phosphor-icons/react' +import { CommandEmpty } from '@/components/ui/command' + +export function EmptyState() { + return ( + +
+ +

No results found

+
+
+ ) +} diff --git a/src/components/global-search/RecentSearches.tsx b/src/components/global-search/RecentSearches.tsx new file mode 100644 index 0000000..99b2728 --- /dev/null +++ b/src/components/global-search/RecentSearches.tsx @@ -0,0 +1,106 @@ +import { ClockCounterClockwise, X } from '@phosphor-icons/react' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { CommandGroup, CommandItem, CommandSeparator } from '@/components/ui/command' + +interface SearchHistoryItem { + id: string + query: string + timestamp: number + resultId?: string + resultTitle?: string + resultCategory?: string +} + +interface SearchResult { + id: string + title: string + subtitle?: string + category: string + icon: React.ReactNode + action: () => void + tags?: string[] +} + +interface RecentSearchesProps { + recentSearches: Array<{ historyItem: SearchHistoryItem; result?: SearchResult }> + onClear: () => void + onSelect: (historyItem: SearchHistoryItem, result?: SearchResult) => void + onRemove: (id: string) => void +} + +export function RecentSearches({ + recentSearches, + onClear, + onSelect, + onRemove, +}: RecentSearchesProps) { + if (recentSearches.length === 0) { + return null + } + + return ( + <> + + Recent Searches + + + } + > + {recentSearches.map(({ historyItem, result }) => ( + onSelect(historyItem, result)} + className="flex items-center gap-3 px-4 py-3 cursor-pointer group" + > +
+ +
+
+ {result ? ( + <> +
{result.title}
+
+ Searched: {historyItem.query} +
+ + ) : ( + <> +
{historyItem.query}
+
Search again
+ + )} +
+ {result && ( + + {result.category} + + )} + +
+ ))} +
+ + + ) +} diff --git a/src/components/global-search/SearchResults.tsx b/src/components/global-search/SearchResults.tsx new file mode 100644 index 0000000..87a8dc1 --- /dev/null +++ b/src/components/global-search/SearchResults.tsx @@ -0,0 +1,54 @@ +import { Badge } from '@/components/ui/badge' +import { CommandGroup, CommandItem, CommandSeparator } from '@/components/ui/command' + +interface SearchResult { + id: string + title: string + subtitle?: string + category: string + icon: React.ReactNode + action: () => void + tags?: string[] +} + +interface SearchResultsProps { + groupedResults: Record + onSelect: (result: SearchResult) => void +} + +export function SearchResults({ groupedResults, onSelect }: SearchResultsProps) { + return ( + <> + {Object.entries(groupedResults).map(([category, results], index) => ( +
+ {index > 0 && } + + {results.map((result) => ( + onSelect(result)} + className="flex items-center gap-3 px-4 py-3 cursor-pointer" + > +
+ {result.icon} +
+
+
{result.title}
+ {result.subtitle && ( +
+ {result.subtitle} +
+ )} +
+ + {category} + +
+ ))} +
+
+ ))} + + ) +} diff --git a/src/components/global-search/useGlobalSearchData.tsx b/src/components/global-search/useGlobalSearchData.tsx new file mode 100644 index 0000000..cb217b3 --- /dev/null +++ b/src/components/global-search/useGlobalSearchData.tsx @@ -0,0 +1,391 @@ +import { useEffect, useMemo, useState } from 'react' +import { useKV } from '@/hooks/use-kv' +import { + BookOpen, + ChartBar, + Code, + Cube, + Database, + DeviceMobile, + Faders, + File, + FileText, + Flask, + FlowArrow, + Folder, + Gear, + Image, + Lightbulb, + PaintBrush, + Play, + Tree, + Wrench, +} from '@phosphor-icons/react' +import { + ComponentNode, + ComponentTree, + Lambda, + PlaywrightTest, + PrismaModel, + ProjectFile, + StorybookStory, + UnitTest, + Workflow, +} from '@/types/project' +import navigationData from '@/data/global-search.json' + +export interface SearchResult { + id: string + title: string + subtitle?: string + category: string + icon: React.ReactNode + action: () => void + tags?: string[] +} + +export interface SearchHistoryItem { + id: string + query: string + timestamp: number + resultId?: string + resultTitle?: string + resultCategory?: string +} + +const navigationIconMap = { + BookOpen, + ChartBar, + Code, + Cube, + Database, + DeviceMobile, + Faders, + FileText, + Flask, + FlowArrow, + Gear, + Image, + Lightbulb, + PaintBrush, + Play, + Tree, + Wrench, +} + +type NavigationIconName = keyof typeof navigationIconMap + +interface NavigationMeta { + id: string + title: string + subtitle: string + category: string + icon: NavigationIconName + tab: string + tags: string[] +} + +interface UseGlobalSearchDataProps { + open: boolean + onOpenChange: (open: boolean) => void + files: ProjectFile[] + models: PrismaModel[] + components: ComponentNode[] + componentTrees: ComponentTree[] + workflows: Workflow[] + lambdas: Lambda[] + playwrightTests: PlaywrightTest[] + storybookStories: StorybookStory[] + unitTests: UnitTest[] + onNavigate: (tab: string, itemId?: string) => void + onFileSelect: (fileId: string) => void +} + +export function useGlobalSearchData({ + open, + onOpenChange, + files, + models, + components, + componentTrees, + workflows, + lambdas, + playwrightTests, + storybookStories, + unitTests, + onNavigate, + onFileSelect, +}: UseGlobalSearchDataProps) { + const [searchQuery, setSearchQuery] = useState('') + const [searchHistory, setSearchHistory] = useKV('search-history', []) + + useEffect(() => { + if (!open) { + setSearchQuery('') + } + }, [open]) + + const addToHistory = (query: string, result?: SearchResult) => { + if (!query.trim()) return + + const historyItem: SearchHistoryItem = { + id: `history-${Date.now()}`, + query: query.trim(), + timestamp: Date.now(), + resultId: result?.id, + resultTitle: result?.title, + resultCategory: result?.category, + } + + setSearchHistory((currentHistory) => { + const filtered = (currentHistory || []).filter( + (item) => item.query.toLowerCase() !== query.toLowerCase() + ) + return [historyItem, ...filtered].slice(0, 20) + }) + } + + const clearHistory = () => { + setSearchHistory([]) + } + + const removeHistoryItem = (id: string) => { + setSearchHistory((currentHistory) => + (currentHistory || []).filter((item) => item.id !== id) + ) + } + + const allResults = useMemo(() => { + const results: SearchResult[] = [] + const navigationResults = (navigationData as NavigationMeta[]).map((item) => { + const Icon = navigationIconMap[item.icon] + + return { + id: item.id, + title: item.title, + subtitle: item.subtitle, + category: item.category, + icon: , + action: () => onNavigate(item.tab), + tags: item.tags, + } + }) + + results.push(...navigationResults) + + files.forEach((file) => { + results.push({ + id: `file-${file.id}`, + title: file.name, + subtitle: file.path, + category: 'Files', + icon: , + action: () => { + onNavigate('code') + onFileSelect(file.id) + }, + tags: [file.language, file.path, 'code', 'file'], + }) + }) + + models.forEach((model) => { + results.push({ + id: `model-${model.id}`, + title: model.name, + subtitle: `${model.fields.length} fields`, + category: 'Models', + icon: , + action: () => onNavigate('models', model.id), + tags: ['prisma', 'database', 'schema', model.name.toLowerCase()], + }) + }) + + components.forEach((component) => { + results.push({ + id: `component-${component.id}`, + title: component.name, + subtitle: component.type, + category: 'Components', + icon: , + action: () => onNavigate('components', component.id), + tags: ['react', 'component', component.type.toLowerCase(), component.name.toLowerCase()], + }) + }) + + componentTrees.forEach((tree) => { + results.push({ + id: `tree-${tree.id}`, + title: tree.name, + subtitle: tree.description || `${tree.rootNodes.length} root nodes`, + category: 'Component Trees', + icon: , + action: () => onNavigate('component-trees', tree.id), + tags: ['hierarchy', 'structure', tree.name.toLowerCase()], + }) + }) + + workflows.forEach((workflow) => { + results.push({ + id: `workflow-${workflow.id}`, + title: workflow.name, + subtitle: workflow.description || `${workflow.nodes.length} nodes`, + category: 'Workflows', + icon: , + action: () => onNavigate('workflows', workflow.id), + tags: ['automation', 'flow', workflow.name.toLowerCase()], + }) + }) + + lambdas.forEach((lambda) => { + results.push({ + id: `lambda-${lambda.id}`, + title: lambda.name, + subtitle: lambda.description || lambda.runtime, + category: 'Lambdas', + icon: , + action: () => onNavigate('lambdas', lambda.id), + tags: ['serverless', 'function', lambda.runtime, lambda.name.toLowerCase()], + }) + }) + + playwrightTests.forEach((test) => { + results.push({ + id: `playwright-${test.id}`, + title: test.name, + subtitle: test.description, + category: 'Playwright Tests', + icon: , + action: () => onNavigate('playwright', test.id), + tags: ['testing', 'e2e', test.name.toLowerCase()], + }) + }) + + storybookStories.forEach((story) => { + results.push({ + id: `storybook-${story.id}`, + title: story.storyName, + subtitle: story.componentName, + category: 'Storybook Stories', + icon: , + action: () => onNavigate('storybook', story.id), + tags: ['documentation', 'story', story.componentName.toLowerCase()], + }) + }) + + unitTests.forEach((test) => { + results.push({ + id: `unit-test-${test.id}`, + title: test.name, + subtitle: test.description, + category: 'Unit Tests', + icon: , + action: () => onNavigate('unit-tests', test.id), + tags: ['testing', 'unit', test.name.toLowerCase()], + }) + }) + + return results + }, [ + files, + models, + components, + componentTrees, + workflows, + lambdas, + playwrightTests, + storybookStories, + unitTests, + onNavigate, + onFileSelect, + ]) + + const filteredResults = useMemo(() => { + if (!searchQuery.trim()) { + return [] + } + + const query = searchQuery.toLowerCase().trim() + const queryWords = query.split(/\s+/) + + return allResults + .map((result) => { + let score = 0 + const titleLower = result.title.toLowerCase() + const subtitleLower = result.subtitle?.toLowerCase() || '' + const categoryLower = result.category.toLowerCase() + const tagsLower = result.tags?.map((tag) => tag.toLowerCase()) || [] + + if (titleLower === query) score += 100 + else if (titleLower.startsWith(query)) score += 50 + else if (titleLower.includes(query)) score += 30 + + if (subtitleLower.includes(query)) score += 20 + if (categoryLower.includes(query)) score += 15 + + tagsLower.forEach((tag) => { + if (tag === query) score += 40 + else if (tag.includes(query)) score += 10 + }) + + queryWords.forEach((word) => { + if (titleLower.includes(word)) score += 5 + if (subtitleLower.includes(word)) score += 3 + if (tagsLower.some((tag) => tag.includes(word))) score += 2 + }) + + return { result, score } + }) + .filter(({ score }) => score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, 50) + .map(({ result }) => result) + }, [allResults, searchQuery]) + + const recentSearches = useMemo(() => { + const safeHistory = searchHistory || [] + return safeHistory.slice(0, 10).map((item) => { + const result = allResults.find((searchResult) => searchResult.id === item.resultId) + return { + historyItem: item, + result, + } + }) + }, [searchHistory, allResults]) + + const groupedResults = useMemo(() => { + const groups: Record = {} + filteredResults.forEach((result) => { + if (!groups[result.category]) { + groups[result.category] = [] + } + groups[result.category].push(result) + }) + return groups + }, [filteredResults]) + + const handleSelect = (result: SearchResult) => { + addToHistory(searchQuery, result) + result.action() + onOpenChange(false) + } + + const handleHistorySelect = (historyItem: SearchHistoryItem, result?: SearchResult) => { + if (result) { + result.action() + onOpenChange(false) + } else { + setSearchQuery(historyItem.query) + } + } + + return { + searchQuery, + setSearchQuery, + recentSearches, + groupedResults, + clearHistory, + removeHistoryItem, + handleSelect, + handleHistorySelect, + } +} diff --git a/src/data/global-search.json b/src/data/global-search.json new file mode 100644 index 0000000..1b34f78 --- /dev/null +++ b/src/data/global-search.json @@ -0,0 +1,182 @@ +[ + { + "id": "nav-dashboard", + "title": "Dashboard", + "subtitle": "View project overview and statistics", + "category": "Navigation", + "icon": "ChartBar", + "tab": "dashboard", + "tags": ["home", "overview", "stats", "metrics"] + }, + { + "id": "nav-code", + "title": "Code Editor", + "subtitle": "Edit project files with Monaco", + "category": "Navigation", + "icon": "Code", + "tab": "code", + "tags": ["editor", "monaco", "typescript", "javascript"] + }, + { + "id": "nav-models", + "title": "Models", + "subtitle": "Design Prisma database models", + "category": "Navigation", + "icon": "Database", + "tab": "models", + "tags": ["prisma", "database", "schema", "orm"] + }, + { + "id": "nav-components", + "title": "Components", + "subtitle": "Build React components", + "category": "Navigation", + "icon": "Tree", + "tab": "components", + "tags": ["react", "mui", "ui", "design"] + }, + { + "id": "nav-component-trees", + "title": "Component Trees", + "subtitle": "Manage component hierarchies", + "category": "Navigation", + "icon": "Tree", + "tab": "component-trees", + "tags": ["hierarchy", "structure", "layout"] + }, + { + "id": "nav-workflows", + "title": "Workflows", + "subtitle": "Design n8n-style workflows", + "category": "Navigation", + "icon": "FlowArrow", + "tab": "workflows", + "tags": ["automation", "n8n", "flow", "pipeline"] + }, + { + "id": "nav-lambdas", + "title": "Lambdas", + "subtitle": "Create serverless functions", + "category": "Navigation", + "icon": "Code", + "tab": "lambdas", + "tags": ["serverless", "functions", "api"] + }, + { + "id": "nav-styling", + "title": "Styling", + "subtitle": "Design themes and colors", + "category": "Navigation", + "icon": "PaintBrush", + "tab": "styling", + "tags": ["theme", "colors", "css", "design"] + }, + { + "id": "nav-flask", + "title": "Flask API", + "subtitle": "Configure Flask backend", + "category": "Navigation", + "icon": "Flask", + "tab": "flask", + "tags": ["python", "backend", "api", "rest"] + }, + { + "id": "nav-playwright", + "title": "Playwright Tests", + "subtitle": "E2E testing configuration", + "category": "Navigation", + "icon": "Play", + "tab": "playwright", + "tags": ["testing", "e2e", "automation"] + }, + { + "id": "nav-storybook", + "title": "Storybook", + "subtitle": "Component documentation", + "category": "Navigation", + "icon": "BookOpen", + "tab": "storybook", + "tags": ["documentation", "components", "stories"] + }, + { + "id": "nav-unit-tests", + "title": "Unit Tests", + "subtitle": "Configure unit testing", + "category": "Navigation", + "icon": "Cube", + "tab": "unit-tests", + "tags": ["testing", "jest", "vitest"] + }, + { + "id": "nav-errors", + "title": "Error Repair", + "subtitle": "Auto-detect and fix errors", + "category": "Navigation", + "icon": "Wrench", + "tab": "errors", + "tags": ["debugging", "errors", "fixes"] + }, + { + "id": "nav-docs", + "title": "Documentation", + "subtitle": "View project documentation", + "category": "Navigation", + "icon": "FileText", + "tab": "docs", + "tags": ["readme", "guide", "help"] + }, + { + "id": "nav-sass", + "title": "Sass Styles", + "subtitle": "Custom Sass styling", + "category": "Navigation", + "icon": "PaintBrush", + "tab": "sass", + "tags": ["sass", "scss", "styles", "css"] + }, + { + "id": "nav-favicon", + "title": "Favicon Designer", + "subtitle": "Design app icons", + "category": "Navigation", + "icon": "Image", + "tab": "favicon", + "tags": ["icon", "logo", "design"] + }, + { + "id": "nav-settings", + "title": "Settings", + "subtitle": "Configure Next.js and npm", + "category": "Navigation", + "icon": "Gear", + "tab": "settings", + "tags": ["config", "nextjs", "npm", "packages"] + }, + { + "id": "nav-pwa", + "title": "PWA Settings", + "subtitle": "Progressive Web App config", + "category": "Navigation", + "icon": "DeviceMobile", + "tab": "pwa", + "tags": ["mobile", "install", "offline"] + }, + { + "id": "nav-features", + "title": "Feature Toggles", + "subtitle": "Enable or disable features", + "category": "Navigation", + "icon": "Faders", + "tab": "features", + "tags": ["settings", "toggles", "enable"] + }, + { + "id": "nav-ideas", + "title": "Feature Ideas", + "subtitle": "Brainstorm and organize ideas", + "category": "Navigation", + "icon": "Lightbulb", + "tab": "ideas", + "tags": ["brainstorm", "ideas", "planning"] + } +]