mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 21:54:56 +00:00
Merge branch 'main' into codex/refactor-oversized-documentationview-component
This commit is contained in:
@@ -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: <BookOpen size={18} /> },
|
||||
{ value: 'roadmap', label: 'Roadmap', icon: <MapPin size={18} /> },
|
||||
{ value: 'agents', label: 'Agents Files', icon: <FileCode size={18} /> },
|
||||
{ value: 'pwa', label: 'PWA Guide', icon: <Rocket size={18} /> },
|
||||
{ value: 'sass', label: 'Sass Styles Guide', icon: <PaintBrush size={18} /> },
|
||||
{ value: 'cicd', label: 'CI/CD Guide', icon: <GitBranch size={18} /> }
|
||||
]
|
||||
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')
|
||||
|
||||
18
src/components/DocumentationView/AIFeatureCard.tsx
Normal file
18
src/components/DocumentationView/AIFeatureCard.tsx
Normal file
@@ -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 (
|
||||
<Card className="bg-primary/5 border-primary/20">
|
||||
<CardContent className="pt-4 pb-4">
|
||||
<div className="flex gap-3">
|
||||
<Sparkle size={20} weight="duotone" className="text-accent flex-shrink-0 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-semibold text-sm">{title}</h4>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
32
src/components/DocumentationView/AgentFileItem.tsx
Normal file
32
src/components/DocumentationView/AgentFileItem.tsx
Normal file
@@ -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 (
|
||||
<div className="space-y-3 border-l-2 border-accent pl-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileCode size={18} className="text-accent" />
|
||||
<code className="text-sm font-semibold text-accent">{filename}</code>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground font-mono">{path}</p>
|
||||
<p className="text-sm text-foreground/90">{description}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Key Features:</p>
|
||||
<ul className="space-y-1">
|
||||
{features.map((feature, idx) => (
|
||||
<li key={idx} className="text-sm text-foreground/80 flex items-start gap-2">
|
||||
<CheckCircle size={14} weight="fill" className="text-accent mt-1 flex-shrink-0" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
8
src/components/DocumentationView/AnimationItem.tsx
Normal file
8
src/components/DocumentationView/AnimationItem.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export function AnimationItem({ name, description }: { name: string; description: string }) {
|
||||
return (
|
||||
<div className="space-y-1 p-3 border rounded-lg bg-card">
|
||||
<code className="text-xs font-mono text-accent">{name}</code>
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
32
src/components/DocumentationView/CICDPlatformItem.tsx
Normal file
32
src/components/DocumentationView/CICDPlatformItem.tsx
Normal file
@@ -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 (
|
||||
<div className="space-y-3 border-l-2 border-accent pl-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch size={18} className="text-accent" />
|
||||
<h3 className="text-base font-semibold">{name}</h3>
|
||||
</div>
|
||||
<code className="text-xs text-muted-foreground font-mono">{file}</code>
|
||||
<p className="text-sm text-foreground/90">{description}</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Key Features:</p>
|
||||
<ul className="space-y-1">
|
||||
{features.map((feature, idx) => (
|
||||
<li key={idx} className="text-sm text-foreground/80 flex items-start gap-2">
|
||||
<CheckCircle size={14} weight="fill" className="text-accent mt-1 flex-shrink-0" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
13
src/components/DocumentationView/FeatureItem.tsx
Normal file
13
src/components/DocumentationView/FeatureItem.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export function FeatureItem({ icon, title, description }: { icon: ReactNode; title: string; description: string }) {
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<div className="text-accent mt-0.5">{icon}</div>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-semibold text-sm">{title}</h4>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
20
src/components/DocumentationView/IntegrationPoint.tsx
Normal file
20
src/components/DocumentationView/IntegrationPoint.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Sparkle } from '@phosphor-icons/react'
|
||||
|
||||
export function IntegrationPoint({ component, capabilities }: { component: string; capabilities: string[] }) {
|
||||
return (
|
||||
<div className="space-y-2 border rounded-lg p-4 bg-card">
|
||||
<h4 className="font-semibold text-sm flex items-center gap-2">
|
||||
<Sparkle size={16} weight="duotone" className="text-accent" />
|
||||
{component}
|
||||
</h4>
|
||||
<ul className="space-y-1">
|
||||
{capabilities.map((capability, idx) => (
|
||||
<li key={idx} className="text-sm text-muted-foreground flex items-start gap-2">
|
||||
<span className="text-accent">•</span>
|
||||
<span>{capability}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
24
src/components/DocumentationView/PipelineStageCard.tsx
Normal file
24
src/components/DocumentationView/PipelineStageCard.tsx
Normal file
@@ -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 (
|
||||
<Card className="bg-primary/5 border-primary/20">
|
||||
<CardContent className="pt-4 pb-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1 flex-1">
|
||||
<h4 className="font-semibold text-sm">{stage}</h4>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-xs whitespace-nowrap">
|
||||
{duration}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -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 }) => <span style={{ fontSize: size }}>✨</span>
|
||||
|
||||
13
src/components/DocumentationView/SassComponentItem.tsx
Normal file
13
src/components/DocumentationView/SassComponentItem.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export function SassComponentItem({ name, classes, description }: { name: string; classes: string[]; description: string }) {
|
||||
return (
|
||||
<div className="space-y-2 p-4 border rounded-lg bg-card">
|
||||
<h4 className="font-semibold">{name}</h4>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
<div className="space-y-1">
|
||||
{classes.map((cls, idx) => (
|
||||
<code key={idx} className="text-xs font-mono text-accent block">{cls}</code>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<SearchHistoryItem[]>('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<SearchResult[]>(() => {
|
||||
const results: SearchResult[] = []
|
||||
|
||||
results.push({
|
||||
id: 'nav-dashboard',
|
||||
title: 'Dashboard',
|
||||
subtitle: 'View project overview and statistics',
|
||||
category: 'Navigation',
|
||||
icon: <ChartBar size={18} weight="duotone" />,
|
||||
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: <Code size={18} weight="duotone" />,
|
||||
action: () => onNavigate('code'),
|
||||
tags: ['editor', 'monaco', 'typescript', 'javascript'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-models',
|
||||
title: 'Models',
|
||||
subtitle: 'Design Prisma database models',
|
||||
category: 'Navigation',
|
||||
icon: <Database size={18} weight="duotone" />,
|
||||
action: () => onNavigate('models'),
|
||||
tags: ['prisma', 'database', 'schema', 'orm'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-components',
|
||||
title: 'Components',
|
||||
subtitle: 'Build React components',
|
||||
category: 'Navigation',
|
||||
icon: <Tree size={18} weight="duotone" />,
|
||||
action: () => onNavigate('components'),
|
||||
tags: ['react', 'mui', 'ui', 'design'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-component-trees',
|
||||
title: 'Component Trees',
|
||||
subtitle: 'Manage component hierarchies',
|
||||
category: 'Navigation',
|
||||
icon: <Tree size={18} weight="duotone" />,
|
||||
action: () => onNavigate('component-trees'),
|
||||
tags: ['hierarchy', 'structure', 'layout'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-workflows',
|
||||
title: 'Workflows',
|
||||
subtitle: 'Design n8n-style workflows',
|
||||
category: 'Navigation',
|
||||
icon: <FlowArrow size={18} weight="duotone" />,
|
||||
action: () => onNavigate('workflows'),
|
||||
tags: ['automation', 'n8n', 'flow', 'pipeline'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-lambdas',
|
||||
title: 'Lambdas',
|
||||
subtitle: 'Create serverless functions',
|
||||
category: 'Navigation',
|
||||
icon: <Code size={18} weight="duotone" />,
|
||||
action: () => onNavigate('lambdas'),
|
||||
tags: ['serverless', 'functions', 'api'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-styling',
|
||||
title: 'Styling',
|
||||
subtitle: 'Design themes and colors',
|
||||
category: 'Navigation',
|
||||
icon: <PaintBrush size={18} weight="duotone" />,
|
||||
action: () => onNavigate('styling'),
|
||||
tags: ['theme', 'colors', 'css', 'design'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-flask',
|
||||
title: 'Flask API',
|
||||
subtitle: 'Configure Flask backend',
|
||||
category: 'Navigation',
|
||||
icon: <Flask size={18} weight="duotone" />,
|
||||
action: () => onNavigate('flask'),
|
||||
tags: ['python', 'backend', 'api', 'rest'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-playwright',
|
||||
title: 'Playwright Tests',
|
||||
subtitle: 'E2E testing configuration',
|
||||
category: 'Navigation',
|
||||
icon: <Play size={18} weight="duotone" />,
|
||||
action: () => onNavigate('playwright'),
|
||||
tags: ['testing', 'e2e', 'automation'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-storybook',
|
||||
title: 'Storybook',
|
||||
subtitle: 'Component documentation',
|
||||
category: 'Navigation',
|
||||
icon: <BookOpen size={18} weight="duotone" />,
|
||||
action: () => onNavigate('storybook'),
|
||||
tags: ['documentation', 'components', 'stories'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-unit-tests',
|
||||
title: 'Unit Tests',
|
||||
subtitle: 'Configure unit testing',
|
||||
category: 'Navigation',
|
||||
icon: <Cube size={18} weight="duotone" />,
|
||||
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: <Wrench size={18} weight="duotone" />,
|
||||
action: () => onNavigate('errors'),
|
||||
tags: ['debugging', 'errors', 'fixes'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-docs',
|
||||
title: 'Documentation',
|
||||
subtitle: 'View project documentation',
|
||||
category: 'Navigation',
|
||||
icon: <FileText size={18} weight="duotone" />,
|
||||
action: () => onNavigate('docs'),
|
||||
tags: ['readme', 'guide', 'help'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-sass',
|
||||
title: 'Sass Styles',
|
||||
subtitle: 'Custom Sass styling',
|
||||
category: 'Navigation',
|
||||
icon: <PaintBrush size={18} weight="duotone" />,
|
||||
action: () => onNavigate('sass'),
|
||||
tags: ['sass', 'scss', 'styles', 'css'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-favicon',
|
||||
title: 'Favicon Designer',
|
||||
subtitle: 'Design app icons',
|
||||
category: 'Navigation',
|
||||
icon: <Image size={18} weight="duotone" />,
|
||||
action: () => onNavigate('favicon'),
|
||||
tags: ['icon', 'logo', 'design'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-settings',
|
||||
title: 'Settings',
|
||||
subtitle: 'Configure Next.js and npm',
|
||||
category: 'Navigation',
|
||||
icon: <Gear size={18} weight="duotone" />,
|
||||
action: () => onNavigate('settings'),
|
||||
tags: ['config', 'nextjs', 'npm', 'packages'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-pwa',
|
||||
title: 'PWA Settings',
|
||||
subtitle: 'Progressive Web App config',
|
||||
category: 'Navigation',
|
||||
icon: <DeviceMobile size={18} weight="duotone" />,
|
||||
action: () => onNavigate('pwa'),
|
||||
tags: ['mobile', 'install', 'offline'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-features',
|
||||
title: 'Feature Toggles',
|
||||
subtitle: 'Enable or disable features',
|
||||
category: 'Navigation',
|
||||
icon: <Faders size={18} weight="duotone" />,
|
||||
action: () => onNavigate('features'),
|
||||
tags: ['settings', 'toggles', 'enable'],
|
||||
})
|
||||
|
||||
results.push({
|
||||
id: 'nav-ideas',
|
||||
title: 'Feature Ideas',
|
||||
subtitle: 'Brainstorm and organize ideas',
|
||||
category: 'Navigation',
|
||||
icon: <Lightbulb size={18} weight="duotone" />,
|
||||
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: <File size={18} weight="duotone" />,
|
||||
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: <Database size={18} weight="duotone" />,
|
||||
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: <Tree size={18} weight="duotone" />,
|
||||
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: <Folder size={18} weight="duotone" />,
|
||||
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: <FlowArrow size={18} weight="duotone" />,
|
||||
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: <Code size={18} weight="duotone" />,
|
||||
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: <Play size={18} weight="duotone" />,
|
||||
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: <BookOpen size={18} weight="duotone" />,
|
||||
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: <Cube size={18} weight="duotone" />,
|
||||
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<string, SearchResult[]> = {}
|
||||
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 (
|
||||
<CommandDialog open={open} onOpenChange={onOpenChange}>
|
||||
<CommandInput
|
||||
placeholder="Search features, files, models, components..."
|
||||
<CommandInput
|
||||
placeholder="Search features, files, models, components..."
|
||||
value={searchQuery}
|
||||
onValueChange={setSearchQuery}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<div className="flex flex-col items-center gap-2 py-6">
|
||||
<MagnifyingGlass size={48} weight="duotone" className="text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">No results found</p>
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
|
||||
{!searchQuery.trim() && recentSearches.length > 0 && (
|
||||
<>
|
||||
<CommandGroup
|
||||
heading={
|
||||
<div className="flex items-center justify-between w-full pr-2">
|
||||
<span>Recent Searches</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearHistory}
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{recentSearches.map(({ historyItem, result }) => (
|
||||
<CommandItem
|
||||
key={historyItem.id}
|
||||
value={historyItem.id}
|
||||
onSelect={() => handleHistorySelect(historyItem, result)}
|
||||
className="flex items-center gap-3 px-4 py-3 cursor-pointer group"
|
||||
>
|
||||
<div className="flex-shrink-0 text-muted-foreground">
|
||||
<ClockCounterClockwise size={18} weight="duotone" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
{result ? (
|
||||
<>
|
||||
<div className="font-medium truncate">{result.title}</div>
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
Searched: {historyItem.query}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="font-medium truncate">{historyItem.query}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Search again
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{result && (
|
||||
<Badge variant="outline" className="flex-shrink-0 text-xs">
|
||||
{result.category}
|
||||
</Badge>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 opacity-0 group-hover:opacity-100 flex-shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
removeHistoryItem(historyItem.id)
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
</>
|
||||
<EmptyState />
|
||||
{!searchQuery.trim() && (
|
||||
<RecentSearches
|
||||
recentSearches={recentSearches}
|
||||
onClear={clearHistory}
|
||||
onSelect={handleHistorySelect}
|
||||
onRemove={removeHistoryItem}
|
||||
/>
|
||||
)}
|
||||
|
||||
{Object.entries(groupedResults).map(([category, results], index) => (
|
||||
<div key={category}>
|
||||
{index > 0 && <CommandSeparator />}
|
||||
<CommandGroup heading={category}>
|
||||
{results.map((result) => (
|
||||
<CommandItem
|
||||
key={result.id}
|
||||
value={result.id}
|
||||
onSelect={() => handleSelect(result)}
|
||||
className="flex items-center gap-3 px-4 py-3 cursor-pointer"
|
||||
>
|
||||
<div className="flex-shrink-0 text-muted-foreground">
|
||||
{result.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{result.title}</div>
|
||||
{result.subtitle && (
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
{result.subtitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant="outline" className="flex-shrink-0 text-xs">
|
||||
{category}
|
||||
</Badge>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</div>
|
||||
))}
|
||||
<SearchResults groupedResults={groupedResults} onSelect={handleSelect} />
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
)
|
||||
|
||||
13
src/components/global-search/EmptyState.tsx
Normal file
13
src/components/global-search/EmptyState.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { MagnifyingGlass } from '@phosphor-icons/react'
|
||||
import { CommandEmpty } from '@/components/ui/command'
|
||||
|
||||
export function EmptyState() {
|
||||
return (
|
||||
<CommandEmpty>
|
||||
<div className="flex flex-col items-center gap-2 py-6">
|
||||
<MagnifyingGlass size={48} weight="duotone" className="text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">No results found</p>
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
)
|
||||
}
|
||||
106
src/components/global-search/RecentSearches.tsx
Normal file
106
src/components/global-search/RecentSearches.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<CommandGroup
|
||||
heading={
|
||||
<div className="flex items-center justify-between w-full pr-2">
|
||||
<span>Recent Searches</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClear}
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{recentSearches.map(({ historyItem, result }) => (
|
||||
<CommandItem
|
||||
key={historyItem.id}
|
||||
value={historyItem.id}
|
||||
onSelect={() => onSelect(historyItem, result)}
|
||||
className="flex items-center gap-3 px-4 py-3 cursor-pointer group"
|
||||
>
|
||||
<div className="flex-shrink-0 text-muted-foreground">
|
||||
<ClockCounterClockwise size={18} weight="duotone" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
{result ? (
|
||||
<>
|
||||
<div className="font-medium truncate">{result.title}</div>
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
Searched: {historyItem.query}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="font-medium truncate">{historyItem.query}</div>
|
||||
<div className="text-xs text-muted-foreground">Search again</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{result && (
|
||||
<Badge variant="outline" className="flex-shrink-0 text-xs">
|
||||
{result.category}
|
||||
</Badge>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 opacity-0 group-hover:opacity-100 flex-shrink-0"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onRemove(historyItem.id)
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
</>
|
||||
)
|
||||
}
|
||||
54
src/components/global-search/SearchResults.tsx
Normal file
54
src/components/global-search/SearchResults.tsx
Normal file
@@ -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<string, SearchResult[]>
|
||||
onSelect: (result: SearchResult) => void
|
||||
}
|
||||
|
||||
export function SearchResults({ groupedResults, onSelect }: SearchResultsProps) {
|
||||
return (
|
||||
<>
|
||||
{Object.entries(groupedResults).map(([category, results], index) => (
|
||||
<div key={category}>
|
||||
{index > 0 && <CommandSeparator />}
|
||||
<CommandGroup heading={category}>
|
||||
{results.map((result) => (
|
||||
<CommandItem
|
||||
key={result.id}
|
||||
value={result.id}
|
||||
onSelect={() => onSelect(result)}
|
||||
className="flex items-center gap-3 px-4 py-3 cursor-pointer"
|
||||
>
|
||||
<div className="flex-shrink-0 text-muted-foreground">
|
||||
{result.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">{result.title}</div>
|
||||
{result.subtitle && (
|
||||
<div className="text-xs text-muted-foreground truncate">
|
||||
{result.subtitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant="outline" className="flex-shrink-0 text-xs">
|
||||
{category}
|
||||
</Badge>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
391
src/components/global-search/useGlobalSearchData.tsx
Normal file
391
src/components/global-search/useGlobalSearchData.tsx
Normal file
@@ -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<SearchHistoryItem[]>('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<SearchResult[]>(() => {
|
||||
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: <Icon size={18} weight="duotone" />,
|
||||
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: <File size={18} weight="duotone" />,
|
||||
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: <Database size={18} weight="duotone" />,
|
||||
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: <Tree size={18} weight="duotone" />,
|
||||
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: <Folder size={18} weight="duotone" />,
|
||||
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: <FlowArrow size={18} weight="duotone" />,
|
||||
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: <Code size={18} weight="duotone" />,
|
||||
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: <Play size={18} weight="duotone" />,
|
||||
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: <BookOpen size={18} weight="duotone" />,
|
||||
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: <Cube size={18} weight="duotone" />,
|
||||
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<string, SearchResult[]> = {}
|
||||
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,
|
||||
}
|
||||
}
|
||||
182
src/data/global-search.json
Normal file
182
src/data/global-search.json
Normal file
@@ -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"]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user