diff --git a/src/components/DocumentationView.tsx b/src/components/DocumentationView.tsx index 58d29dc..7fbdcc9 100644 --- a/src/components/DocumentationView.tsx +++ b/src/components/DocumentationView.tsx @@ -7,25 +7,20 @@ import { FileCode, GitBranch, MagnifyingGlass, - MapPin, - PaintBrush, - Rocket + GitBranch, + FileCode, + Sparkle, + CheckCircle } from '@phosphor-icons/react' -import { ReadmeTab } from './DocumentationView/ReadmeTab' -import { RoadmapTab } from './DocumentationView/RoadmapTab' -import { AgentsTab } from './DocumentationView/AgentsTab' -import { PwaTab } from './DocumentationView/PwaTab' -import { SassTab } from './DocumentationView/SassTab' -import { CicdTab } from './DocumentationView/CicdTab' - -const tabs = [ - { value: 'readme', label: 'README', icon: }, - { value: 'roadmap', label: 'Roadmap', icon: }, - { value: 'agents', label: 'Agents Files', icon: }, - { value: 'pwa', label: 'PWA Guide', icon: }, - { value: 'sass', label: 'Sass Styles Guide', icon: }, - { value: 'cicd', label: 'CI/CD Guide', icon: } -] +import { AIFeatureCard } from './DocumentationView/AIFeatureCard' +import { AgentFileItem } from './DocumentationView/AgentFileItem' +import { AnimationItem } from './DocumentationView/AnimationItem' +import { CICDPlatformItem } from './DocumentationView/CICDPlatformItem' +import { FeatureItem } from './DocumentationView/FeatureItem' +import { IntegrationPoint } from './DocumentationView/IntegrationPoint' +import { PipelineStageCard } from './DocumentationView/PipelineStageCard' +import { RoadmapItem } from './DocumentationView/RoadmapItem' +import { SassComponentItem } from './DocumentationView/SassComponentItem' export function DocumentationView() { const [activeTab, setActiveTab] = useState('readme') diff --git a/src/components/DocumentationView/AIFeatureCard.tsx b/src/components/DocumentationView/AIFeatureCard.tsx new file mode 100644 index 0000000..3cfc368 --- /dev/null +++ b/src/components/DocumentationView/AIFeatureCard.tsx @@ -0,0 +1,18 @@ +import { Card, CardContent } from '@/components/ui/card' +import { Sparkle } from '@phosphor-icons/react' + +export function AIFeatureCard({ title, description }: { title: string; description: string }) { + return ( + + +
+ +
+

{title}

+

{description}

+
+
+
+
+ ) +} diff --git a/src/components/DocumentationView/AgentFileItem.tsx b/src/components/DocumentationView/AgentFileItem.tsx new file mode 100644 index 0000000..66d98da --- /dev/null +++ b/src/components/DocumentationView/AgentFileItem.tsx @@ -0,0 +1,32 @@ +import { FileCode, CheckCircle } from '@phosphor-icons/react' + +export function AgentFileItem({ filename, path, description, features }: { + filename: string + path: string + description: string + features: string[] +}) { + return ( +
+
+
+ + {filename} +
+

{path}

+

{description}

+
+
+

Key Features:

+
    + {features.map((feature, idx) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ ) +} diff --git a/src/components/DocumentationView/AnimationItem.tsx b/src/components/DocumentationView/AnimationItem.tsx new file mode 100644 index 0000000..b19077d --- /dev/null +++ b/src/components/DocumentationView/AnimationItem.tsx @@ -0,0 +1,8 @@ +export function AnimationItem({ name, description }: { name: string; description: string }) { + return ( +
+ {name} +

{description}

+
+ ) +} diff --git a/src/components/DocumentationView/CICDPlatformItem.tsx b/src/components/DocumentationView/CICDPlatformItem.tsx new file mode 100644 index 0000000..b19839a --- /dev/null +++ b/src/components/DocumentationView/CICDPlatformItem.tsx @@ -0,0 +1,32 @@ +import { CheckCircle, GitBranch } from '@phosphor-icons/react' + +export function CICDPlatformItem({ name, file, description, features }: { + name: string + file: string + description: string + features: string[] +}) { + return ( +
+
+
+ +

{name}

+
+ {file} +

{description}

+
+
+

Key Features:

+
    + {features.map((feature, idx) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ ) +} diff --git a/src/components/DocumentationView/FeatureItem.tsx b/src/components/DocumentationView/FeatureItem.tsx new file mode 100644 index 0000000..74368fa --- /dev/null +++ b/src/components/DocumentationView/FeatureItem.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from 'react' + +export function FeatureItem({ icon, title, description }: { icon: ReactNode; title: string; description: string }) { + return ( +
+
{icon}
+
+

{title}

+

{description}

+
+
+ ) +} diff --git a/src/components/DocumentationView/IntegrationPoint.tsx b/src/components/DocumentationView/IntegrationPoint.tsx new file mode 100644 index 0000000..490822c --- /dev/null +++ b/src/components/DocumentationView/IntegrationPoint.tsx @@ -0,0 +1,20 @@ +import { Sparkle } from '@phosphor-icons/react' + +export function IntegrationPoint({ component, capabilities }: { component: string; capabilities: string[] }) { + return ( +
+

+ + {component} +

+
    + {capabilities.map((capability, idx) => ( +
  • + + {capability} +
  • + ))} +
+
+ ) +} diff --git a/src/components/DocumentationView/PipelineStageCard.tsx b/src/components/DocumentationView/PipelineStageCard.tsx new file mode 100644 index 0000000..434b630 --- /dev/null +++ b/src/components/DocumentationView/PipelineStageCard.tsx @@ -0,0 +1,24 @@ +import { Card, CardContent } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' + +export function PipelineStageCard({ stage, description, duration }: { + stage: string + description: string + duration: string +}) { + return ( + + +
+
+

{stage}

+

{description}

+
+ + {duration} + +
+
+
+ ) +} diff --git a/src/components/DocumentationView/ReadmeTab.tsx b/src/components/DocumentationView/ReadmeTab.tsx index b1c1efc..7d8ce82 100644 --- a/src/components/DocumentationView/ReadmeTab.tsx +++ b/src/components/DocumentationView/ReadmeTab.tsx @@ -1,7 +1,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Separator } from '@/components/ui/separator' import { Code, Database, Tree, PaintBrush, Flask, Play, Cube, Wrench, Gear, Rocket, Lightbulb, CheckCircle } from '@phosphor-icons/react' -import { FeatureItem, AIFeatureCard } from './FeatureItems' +import { AIFeatureCard } from './AIFeatureCard' +import { FeatureItem } from './FeatureItem' import readmeData from '@/data/documentation/readme-data.json' const Sparkle = ({ size }: { size: number }) => diff --git a/src/components/DocumentationView/SassComponentItem.tsx b/src/components/DocumentationView/SassComponentItem.tsx new file mode 100644 index 0000000..a34905b --- /dev/null +++ b/src/components/DocumentationView/SassComponentItem.tsx @@ -0,0 +1,13 @@ +export function SassComponentItem({ name, classes, description }: { name: string; classes: string[]; description: string }) { + return ( +
+

{name}

+

{description}

+
+ {classes.map((cls, idx) => ( + {cls} + ))} +
+
+ ) +} 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"] + } +]