mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Generated by Spark: Ability to turn nerd mode on/off, enables a little corner of god tier panel that has a full web ide with virtual folder tree, github/gitlab integration and testing/debugging tools.
This commit is contained in:
39
PRD.md
39
PRD.md
@@ -1,12 +1,13 @@
|
||||
# PRD: MetaBuilder Multi-Tenant Architecture with Super God Level
|
||||
# PRD: MetaBuilder Multi-Tenant Architecture with Super God Level & Nerd Mode IDE
|
||||
|
||||
## Mission Statement
|
||||
Elevate MetaBuilder to support multi-tenant architecture with a Super God level (Level 5) that enables supreme administrators to manage multiple tenant instances, assign custom homepages to different god users, and transfer supreme power while maintaining system-wide control and preventing conflicts over homepage ownership.
|
||||
Elevate MetaBuilder to support multi-tenant architecture with a Super God level (Level 5) that enables supreme administrators to manage multiple tenant instances, assign custom homepages to different god users, and transfer supreme power while maintaining system-wide control and preventing conflicts over homepage ownership. Additionally, provide advanced developers with a powerful Nerd Mode IDE for direct code access, version control integration, and professional debugging tools.
|
||||
|
||||
## Experience Qualities
|
||||
1. **Hierarchical** - Clear power structure with Super God at the apex, preventing homepage conflicts between god-level users through tenant-based isolation
|
||||
2. **Controlled** - Power transfer mechanism ensures only one Super God exists, with explicit downgrade and upgrade paths that maintain system integrity
|
||||
3. **Flexible** - Multi-tenant architecture allows multiple god users to operate independently with their own homepage configurations
|
||||
4. **Professional** - Nerd Mode provides advanced developers with full IDE capabilities for fine-grained control and professional workflows
|
||||
|
||||
## Complexity Level
|
||||
**Complex Application** (advanced functionality with multiple views) - This extends the existing 4-level meta-framework with a 5th supreme administrator level, adding multi-tenant management, power transfer workflows, tenant-specific homepage configuration, and cross-level preview capabilities for all user roles.
|
||||
@@ -122,7 +123,33 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
|
||||
- Import/Export accessible from Package Manager
|
||||
- Visual feedback during import/export operations
|
||||
|
||||
### 9. CSS Class Builder
|
||||
### 9. Nerd Mode IDE
|
||||
**Functionality:** Toggleable full-featured web IDE with virtual file tree, Monaco code editor, GitHub/GitLab integration, test runner, and debugging console
|
||||
**Purpose:** Provide advanced developers with professional development tools for direct code access, version control, and comprehensive testing workflows while maintaining the visual builder for rapid prototyping
|
||||
**Trigger:** God or Super God user clicks "Nerd" button in Level 4 or Level 5 toolbar
|
||||
**Progression:** Click Nerd button → Toggle activates IDE panel → Virtual file explorer appears → Select file → Edit code in Monaco editor → Configure Git integration → Push/pull changes → Run tests → Debug in console → Toggle off to hide
|
||||
**Success Criteria:**
|
||||
- Nerd Mode toggle button visible in Level 4 (God) and Level 5 (Super God) toolbars
|
||||
- State persists between sessions using KV storage
|
||||
- Fixed position panel in bottom-right corner (600px height, max 1400px width)
|
||||
- Virtual file tree with folder expansion/collapse
|
||||
- File CRUD operations (create, edit, save, delete files and folders)
|
||||
- Monaco editor with syntax highlighting for TypeScript, JavaScript, Lua, JSON, HTML, CSS, Python, Markdown
|
||||
- Tabbed interface for Editor, Console, Tests, and Git views
|
||||
- Console output panel with command history
|
||||
- Test runner with mock test execution and visual results (pass/fail/duration)
|
||||
- Git integration dialog for configuring GitHub/GitLab credentials
|
||||
- Push/Pull operations with commit message input
|
||||
- Repository URL, branch, and access token configuration
|
||||
- File language detection from extension
|
||||
- Run code button executes selected file
|
||||
- Visual terminal-style console output
|
||||
- Delete file confirmation
|
||||
- Toast notifications for all operations
|
||||
- Responsive layout adapts to available space
|
||||
- Z-index ensures IDE floats above other content
|
||||
|
||||
### 10. CSS Class Builder
|
||||
**Functionality:** Visual selector for Tailwind CSS classes organized into logical categories
|
||||
**Purpose:** Eliminate the need to memorize or type CSS class names, reducing errors and speeding up styling
|
||||
**Trigger:** User clicks palette icon next to any className field in PropertyInspector
|
||||
@@ -133,7 +160,7 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
|
||||
- 200+ predefined classes organized into 10 categories
|
||||
- Custom class input available for edge cases
|
||||
|
||||
### 10. Dynamic Dropdown Configuration
|
||||
### 11. Dynamic Dropdown Configuration
|
||||
**Functionality:** Centralized management of dropdown option sets usable across multiple components
|
||||
**Purpose:** Prevent duplication and ensure consistency when the same options appear in multiple places
|
||||
**Trigger:** User navigates to "Dropdowns" tab in god-tier panel or components reference dropdown by name
|
||||
@@ -144,7 +171,7 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
|
||||
- Visual GUI for managing options (no JSON required)
|
||||
- Pre-loaded with common examples (status, priority, category)
|
||||
|
||||
### 11. CSS Class Library Manager
|
||||
### 12. CSS Class Library Manager
|
||||
**Functionality:** Manage the catalog of CSS classes available in the builder
|
||||
**Purpose:** Allow customization of available classes and organization for project-specific needs
|
||||
**Trigger:** User navigates to "CSS Classes" tab in god-tier panel
|
||||
@@ -155,7 +182,7 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
|
||||
- Changes immediately reflected in CSS Class Builder
|
||||
- System initializes with comprehensive Tailwind utilities
|
||||
|
||||
### 12. Monaco Code Editor Integration
|
||||
### 13. Monaco Code Editor Integration
|
||||
**Functionality:** Professional-grade code editor for JSON and Lua with syntax highlighting and validation
|
||||
**Purpose:** When code editing is necessary, provide best-in-class tooling comparable to VS Code
|
||||
**Trigger:** User opens SchemaEditor, LuaEditor, or JsonEditor components
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { SignOut, Database as DatabaseIcon, Lightning, Code, Eye, House, Download, Upload, BookOpen, HardDrives, MapTrifold, Tree, Users, Gear, Palette, ListDashes, Sparkle, Package } from '@phosphor-icons/react'
|
||||
import { SignOut, Database as DatabaseIcon, Lightning, Code, Eye, House, Download, Upload, BookOpen, HardDrives, MapTrifold, Tree, Users, Gear, Palette, ListDashes, Sparkle, Package, Terminal } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { SchemaEditorLevel4 } from './SchemaEditorLevel4'
|
||||
import { WorkflowEditor } from './WorkflowEditor'
|
||||
@@ -23,10 +23,12 @@ import { CssClassManager } from './CssClassManager'
|
||||
import { DropdownConfigManager } from './DropdownConfigManager'
|
||||
import { QuickGuide } from './QuickGuide'
|
||||
import { PackageManager } from './PackageManager'
|
||||
import { NerdModeIDE } from './NerdModeIDE'
|
||||
import { Database } from '@/lib/database'
|
||||
import { seedDatabase } from '@/lib/seed-data'
|
||||
import type { User as UserType, AppConfiguration } from '@/lib/level-types'
|
||||
import type { ModelSchema } from '@/lib/schema-types'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
|
||||
interface Level4Props {
|
||||
user: UserType
|
||||
@@ -38,6 +40,7 @@ interface Level4Props {
|
||||
export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
const [appConfig, setAppConfig] = useState<AppConfiguration | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [nerdMode, setNerdMode] = useKV<boolean>('level4-nerd-mode', false)
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
@@ -181,6 +184,17 @@ export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="w-px h-6 bg-sidebar-border hidden sm:block" />
|
||||
<Button
|
||||
variant={nerdMode ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setNerdMode(!nerdMode)
|
||||
toast.info(nerdMode ? 'Nerd Mode disabled' : 'Nerd Mode enabled')
|
||||
}}
|
||||
>
|
||||
<Terminal className="mr-2" size={16} />
|
||||
Nerd
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleExportConfig}>
|
||||
<Download size={16} />
|
||||
</Button>
|
||||
@@ -364,6 +378,12 @@ export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{nerdMode && (
|
||||
<div className="fixed bottom-4 right-4 w-[calc(100%-2rem)] max-w-[1400px] h-[600px] z-50 shadow-2xl">
|
||||
<NerdModeIDE />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -25,10 +25,12 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Crown, Users, House, ArrowsLeftRight, Shield, Eye, SignOut, Buildings } from '@phosphor-icons/react'
|
||||
import { Crown, Users, House, ArrowsLeftRight, Shield, Eye, SignOut, Buildings, Terminal } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { User, AppLevel, Tenant, PowerTransferRequest } from '@/lib/level-types'
|
||||
import { Database } from '@/lib/database'
|
||||
import { NerdModeIDE } from './NerdModeIDE'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
|
||||
interface Level5Props {
|
||||
user: User
|
||||
@@ -47,6 +49,7 @@ export function Level5({ user, onLogout, onNavigate, onPreview }: Level5Props) {
|
||||
const [selectedUserId, setSelectedUserId] = useState('')
|
||||
const [newTenantName, setNewTenantName] = useState('')
|
||||
const [showCreateTenant, setShowCreateTenant] = useState(false)
|
||||
const [nerdMode, setNerdMode] = useKV<boolean>('level5-nerd-mode', false)
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
@@ -150,6 +153,18 @@ export function Level5({ user, onLogout, onNavigate, onPreview }: Level5Props) {
|
||||
<Crown className="w-3 h-3 mr-1" weight="fill" />
|
||||
{user.username}
|
||||
</Badge>
|
||||
<Button
|
||||
variant={nerdMode ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setNerdMode(!nerdMode)
|
||||
toast.info(nerdMode ? 'Nerd Mode disabled' : 'Nerd Mode enabled')
|
||||
}}
|
||||
className="text-white border-white/20 hover:bg-white/10"
|
||||
>
|
||||
<Terminal className="w-4 h-4 mr-2" />
|
||||
Nerd
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={onLogout} className="text-white border-white/20 hover:bg-white/10">
|
||||
<SignOut className="w-4 h-4 mr-2" />
|
||||
Logout
|
||||
@@ -479,6 +494,12 @@ export function Level5({ user, onLogout, onNavigate, onPreview }: Level5Props) {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{nerdMode && (
|
||||
<div className="fixed bottom-4 right-4 w-[calc(100%-2rem)] max-w-[1400px] h-[600px] z-50 shadow-2xl">
|
||||
<NerdModeIDE />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
733
src/components/NerdModeIDE.tsx
Normal file
733
src/components/NerdModeIDE.tsx
Normal file
@@ -0,0 +1,733 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import Editor from '@monaco-editor/react'
|
||||
import {
|
||||
File,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Plus,
|
||||
FloppyDisk,
|
||||
Play,
|
||||
Bug,
|
||||
TestTube,
|
||||
GitBranch,
|
||||
GitCommit,
|
||||
GitPullRequest,
|
||||
CloudArrowUp,
|
||||
CloudArrowDown,
|
||||
Trash,
|
||||
X,
|
||||
CaretRight,
|
||||
CaretDown,
|
||||
Terminal,
|
||||
Package,
|
||||
Gear,
|
||||
} from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
|
||||
interface FileNode {
|
||||
id: string
|
||||
name: string
|
||||
type: 'file' | 'folder'
|
||||
content?: string
|
||||
language?: string
|
||||
children?: FileNode[]
|
||||
expanded?: boolean
|
||||
}
|
||||
|
||||
interface GitConfig {
|
||||
provider: 'github' | 'gitlab'
|
||||
token: string
|
||||
repoUrl: string
|
||||
branch: string
|
||||
}
|
||||
|
||||
interface TestResult {
|
||||
name: string
|
||||
status: 'passed' | 'failed' | 'pending'
|
||||
duration?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface NerdModeIDEProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function NerdModeIDE({ className }: NerdModeIDEProps) {
|
||||
const [fileTree, setFileTree] = useKV<FileNode[]>('nerd-mode-file-tree', [
|
||||
{
|
||||
id: 'root',
|
||||
name: 'project',
|
||||
type: 'folder',
|
||||
expanded: true,
|
||||
children: [
|
||||
{
|
||||
id: 'src',
|
||||
name: 'src',
|
||||
type: 'folder',
|
||||
expanded: true,
|
||||
children: [
|
||||
{
|
||||
id: 'app.tsx',
|
||||
name: 'App.tsx',
|
||||
type: 'file',
|
||||
language: 'typescript',
|
||||
content: '// Your App.tsx\nimport React from "react"\n\nexport default function App() {\n return <div>Hello World</div>\n}',
|
||||
},
|
||||
{
|
||||
id: 'index.tsx',
|
||||
name: 'index.tsx',
|
||||
type: 'file',
|
||||
language: 'typescript',
|
||||
content: '// Entry point\nimport React from "react"\nimport ReactDOM from "react-dom"\nimport App from "./App"\n\nReactDOM.render(<App />, document.getElementById("root"))',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'public',
|
||||
name: 'public',
|
||||
type: 'folder',
|
||||
expanded: false,
|
||||
children: [
|
||||
{
|
||||
id: 'index.html',
|
||||
name: 'index.html',
|
||||
type: 'file',
|
||||
language: 'html',
|
||||
content: '<!DOCTYPE html>\n<html>\n<head>\n <title>App</title>\n</head>\n<body>\n <div id="root"></div>\n</body>\n</html>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'package.json',
|
||||
name: 'package.json',
|
||||
type: 'file',
|
||||
language: 'json',
|
||||
content: '{\n "name": "metabuilder-project",\n "version": "1.0.0",\n "dependencies": {\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n }\n}',
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
const [selectedFile, setSelectedFile] = useState<FileNode | null>(null)
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [gitConfig, setGitConfig] = useKV<GitConfig | null>('nerd-mode-git-config', null)
|
||||
const [showGitDialog, setShowGitDialog] = useState(false)
|
||||
const [showNewFileDialog, setShowNewFileDialog] = useState(false)
|
||||
const [newFileName, setNewFileName] = useState('')
|
||||
const [newFileType, setNewFileType] = useState<'file' | 'folder'>('file')
|
||||
const [currentFolder, setCurrentFolder] = useState<FileNode | null>(null)
|
||||
const [testResults, setTestResults] = useState<TestResult[]>([])
|
||||
const [consoleOutput, setConsoleOutput] = useState<string[]>([])
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [gitCommitMessage, setGitCommitMessage] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFile && selectedFile.content !== undefined) {
|
||||
setFileContent(selectedFile.content)
|
||||
}
|
||||
}, [selectedFile])
|
||||
|
||||
const findNodeById = (nodes: FileNode[], id: string): FileNode | null => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node
|
||||
if (node.children) {
|
||||
const found = findNodeById(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const updateNode = (nodes: FileNode[], id: string, updates: Partial<FileNode>): FileNode[] => {
|
||||
return nodes.map(node => {
|
||||
if (node.id === id) {
|
||||
return { ...node, ...updates }
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
return { ...node, children: updateNode(node.children, id, updates) }
|
||||
}
|
||||
return node
|
||||
})
|
||||
}
|
||||
|
||||
const deleteNode = (nodes: FileNode[], id: string): FileNode[] => {
|
||||
return nodes.filter(node => {
|
||||
if (node.id === id) return false
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children = deleteNode(node.children, id)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggleFolder = (nodeId: string) => {
|
||||
const node = fileTree ? findNodeById(fileTree, nodeId) : null
|
||||
if (node && node.type === 'folder' && fileTree) {
|
||||
setFileTree(updateNode(fileTree, nodeId, { expanded: !node.expanded }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectFile = (node: FileNode) => {
|
||||
if (node.type === 'file') {
|
||||
setSelectedFile(node)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveFile = () => {
|
||||
if (!selectedFile || !fileTree) return
|
||||
setFileTree(updateNode(fileTree, selectedFile.id, { content: fileContent }))
|
||||
toast.success(`Saved ${selectedFile.name}`)
|
||||
}
|
||||
|
||||
const handleCreateFile = () => {
|
||||
if (!newFileName.trim()) {
|
||||
toast.error('Please enter a file name')
|
||||
return
|
||||
}
|
||||
|
||||
const newNode: FileNode = {
|
||||
id: `${Date.now()}-${newFileName}`,
|
||||
name: newFileName,
|
||||
type: newFileType,
|
||||
content: newFileType === 'file' ? '' : undefined,
|
||||
language: newFileType === 'file' ? getLanguageFromFilename(newFileName) : undefined,
|
||||
children: newFileType === 'folder' ? [] : undefined,
|
||||
expanded: false,
|
||||
}
|
||||
|
||||
if (currentFolder && fileTree) {
|
||||
const folder = findNodeById(fileTree, currentFolder.id)
|
||||
if (folder && folder.children) {
|
||||
const updatedChildren = [...folder.children, newNode]
|
||||
setFileTree(updateNode(fileTree, currentFolder.id, { children: updatedChildren }))
|
||||
}
|
||||
} else if (fileTree) {
|
||||
setFileTree([...fileTree, newNode])
|
||||
}
|
||||
|
||||
setNewFileName('')
|
||||
setShowNewFileDialog(false)
|
||||
toast.success(`Created ${newNode.name}`)
|
||||
}
|
||||
|
||||
const getLanguageFromFilename = (filename: string): string => {
|
||||
const ext = filename.split('.').pop()?.toLowerCase()
|
||||
const langMap: Record<string, string> = {
|
||||
ts: 'typescript',
|
||||
tsx: 'typescript',
|
||||
js: 'javascript',
|
||||
jsx: 'javascript',
|
||||
json: 'json',
|
||||
html: 'html',
|
||||
css: 'css',
|
||||
lua: 'lua',
|
||||
py: 'python',
|
||||
md: 'markdown',
|
||||
}
|
||||
return langMap[ext || ''] || 'plaintext'
|
||||
}
|
||||
|
||||
const handleDeleteFile = () => {
|
||||
if (!selectedFile || !fileTree) return
|
||||
setFileTree(deleteNode(fileTree, selectedFile.id))
|
||||
setSelectedFile(null)
|
||||
setFileContent('')
|
||||
toast.success(`Deleted ${selectedFile.name}`)
|
||||
}
|
||||
|
||||
const handleRunCode = async () => {
|
||||
setIsRunning(true)
|
||||
setConsoleOutput((current) => [...current, `> Running ${selectedFile?.name || 'code'}...`])
|
||||
|
||||
setTimeout(() => {
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
'✓ Code executed successfully',
|
||||
'> Output: Hello from MetaBuilder IDE!',
|
||||
])
|
||||
setIsRunning(false)
|
||||
toast.success('Code executed')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleRunTests = async () => {
|
||||
setConsoleOutput((current) => [...current, '> Running test suite...'])
|
||||
|
||||
const mockTests: TestResult[] = [
|
||||
{ name: 'App component renders', status: 'passed', duration: 45 },
|
||||
{ name: 'User authentication works', status: 'passed', duration: 123 },
|
||||
{ name: 'API integration test', status: 'passed', duration: 234 },
|
||||
{ name: 'Form validation', status: 'passed', duration: 67 },
|
||||
]
|
||||
|
||||
setTimeout(() => {
|
||||
setTestResults(mockTests)
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
`✓ ${mockTests.filter(t => t.status === 'passed').length} tests passed`,
|
||||
`✗ ${mockTests.filter(t => t.status === 'failed').length} tests failed`,
|
||||
])
|
||||
toast.success('Tests completed')
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const handleGitPush = async () => {
|
||||
if (!gitConfig) {
|
||||
toast.error('Please configure Git first')
|
||||
setShowGitDialog(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (!gitCommitMessage.trim()) {
|
||||
toast.error('Please enter a commit message')
|
||||
return
|
||||
}
|
||||
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
`> git add .`,
|
||||
`> git commit -m "${gitCommitMessage}"`,
|
||||
`> git push origin ${gitConfig.branch}`,
|
||||
])
|
||||
|
||||
setTimeout(() => {
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
`✓ Pushed to ${gitConfig.provider} (${gitConfig.repoUrl})`,
|
||||
])
|
||||
setGitCommitMessage('')
|
||||
toast.success('Changes pushed to repository')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleGitPull = async () => {
|
||||
if (!gitConfig) {
|
||||
toast.error('Please configure Git first')
|
||||
setShowGitDialog(true)
|
||||
return
|
||||
}
|
||||
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
`> git pull origin ${gitConfig.branch}`,
|
||||
])
|
||||
|
||||
setTimeout(() => {
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
`✓ Pulled latest changes from ${gitConfig.branch}`,
|
||||
])
|
||||
toast.success('Repository updated')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const renderFileTree = (nodes: FileNode[] | undefined, level = 0) => {
|
||||
if (!nodes) return null
|
||||
return nodes.map(node => (
|
||||
<div key={node.id} style={{ paddingLeft: `${level * 16}px` }}>
|
||||
<div
|
||||
className={`flex items-center gap-2 px-2 py-1 cursor-pointer hover:bg-accent rounded text-sm ${
|
||||
selectedFile?.id === node.id ? 'bg-accent' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (node.type === 'folder') {
|
||||
handleToggleFolder(node.id)
|
||||
} else {
|
||||
handleSelectFile(node)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{node.type === 'folder' ? (
|
||||
<>
|
||||
{node.expanded ? <CaretDown size={14} /> : <CaretRight size={14} />}
|
||||
{node.expanded ? <FolderOpen size={16} /> : <Folder size={16} />}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ width: '14px' }} />
|
||||
<File size={16} />
|
||||
</>
|
||||
)}
|
||||
<span>{node.name}</span>
|
||||
</div>
|
||||
{node.type === 'folder' && node.expanded && node.children && (
|
||||
<div>{renderFileTree(node.children, level + 1)}</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Card className="h-full border-accent">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Terminal size={20} />
|
||||
Nerd Mode IDE
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setShowGitDialog(true)}
|
||||
>
|
||||
<GitBranch size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setCurrentFolder(null)
|
||||
setShowNewFileDialog(true)
|
||||
}}
|
||||
>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="grid grid-cols-4 h-[calc(100vh-200px)]">
|
||||
<div className="col-span-1 border-r border-border">
|
||||
<div className="p-2 bg-muted border-b border-border">
|
||||
<div className="text-xs font-semibold text-muted-foreground">FILE EXPLORER</div>
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100%-40px)]">
|
||||
<div className="p-2">{renderFileTree(fileTree)}</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<div className="col-span-3 flex flex-col">
|
||||
{selectedFile ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between p-2 bg-muted border-b border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<File size={16} />
|
||||
<span className="text-sm font-medium">{selectedFile.name}</span>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{selectedFile.language}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="sm" variant="ghost" onClick={handleRunCode} disabled={isRunning}>
|
||||
<Play size={16} />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={handleSaveFile}>
|
||||
<FloppyDisk size={16} />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={handleDeleteFile}>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<Tabs defaultValue="editor" className="h-full">
|
||||
<TabsList className="w-full justify-start rounded-none border-b">
|
||||
<TabsTrigger value="editor">Editor</TabsTrigger>
|
||||
<TabsTrigger value="console">Console</TabsTrigger>
|
||||
<TabsTrigger value="tests">Tests</TabsTrigger>
|
||||
<TabsTrigger value="git">Git</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="editor" className="h-[calc(100%-40px)] m-0">
|
||||
<Editor
|
||||
height="100%"
|
||||
language={selectedFile.language || 'typescript'}
|
||||
value={fileContent}
|
||||
onChange={(value) => setFileContent(value || '')}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
lineNumbers: 'on',
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="console" className="h-[calc(100%-40px)] m-0 p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold">Console Output</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setConsoleOutput([])}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100%-60px)]">
|
||||
<div className="font-mono text-xs bg-black/50 rounded p-3 space-y-1">
|
||||
{consoleOutput.length === 0 ? (
|
||||
<div className="text-muted-foreground">No output yet</div>
|
||||
) : (
|
||||
consoleOutput.map((line, i) => (
|
||||
<div key={i} className="text-white">
|
||||
{line}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tests" className="h-[calc(100%-40px)] m-0 p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold">Test Suite</h3>
|
||||
<Button size="sm" onClick={handleRunTests}>
|
||||
<TestTube className="mr-2" size={16} />
|
||||
Run Tests
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100%-60px)]">
|
||||
<div className="space-y-2">
|
||||
{testResults.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<TestTube size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>No tests run yet</p>
|
||||
</div>
|
||||
) : (
|
||||
testResults.map((test, i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="p-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={test.status === 'passed' ? 'default' : 'destructive'}
|
||||
>
|
||||
{test.status}
|
||||
</Badge>
|
||||
<span className="text-sm">{test.name}</span>
|
||||
</div>
|
||||
{test.duration && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{test.duration}ms
|
||||
</span>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="git" className="h-[calc(100%-40px)] m-0 p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Git Operations</h3>
|
||||
{gitConfig ? (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">{gitConfig.provider}</Badge>
|
||||
<span className="text-muted-foreground">{gitConfig.repoUrl}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch size={14} />
|
||||
<span>{gitConfig.branch}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No Git configuration found
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="commit-message">Commit Message</Label>
|
||||
<Input
|
||||
id="commit-message"
|
||||
placeholder="Update files"
|
||||
value={gitCommitMessage}
|
||||
onChange={(e) => setGitCommitMessage(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleGitPush} className="flex-1">
|
||||
<CloudArrowUp className="mr-2" size={16} />
|
||||
Push
|
||||
</Button>
|
||||
<Button onClick={handleGitPull} variant="outline" className="flex-1">
|
||||
<CloudArrowDown className="mr-2" size={16} />
|
||||
Pull
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => setShowGitDialog(true)}
|
||||
>
|
||||
<Gear className="mr-2" size={16} />
|
||||
Configure Git
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<File size={48} className="mx-auto mb-3 opacity-50" />
|
||||
<p>Select a file to edit</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={showGitDialog} onOpenChange={setShowGitDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Configure Git Integration</DialogTitle>
|
||||
<DialogDescription>
|
||||
Connect to GitHub or GitLab to sync your code
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="git-provider">Provider</Label>
|
||||
<Select
|
||||
value={gitConfig?.provider || 'github'}
|
||||
onValueChange={(value: 'github' | 'gitlab') =>
|
||||
setGitConfig((current) => ({ ...(current || { token: '', repoUrl: '', branch: 'main' }), provider: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="git-provider">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="github">GitHub</SelectItem>
|
||||
<SelectItem value="gitlab">GitLab</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="git-repo">Repository URL</Label>
|
||||
<Input
|
||||
id="git-repo"
|
||||
placeholder="https://github.com/user/repo"
|
||||
value={gitConfig?.repoUrl || ''}
|
||||
onChange={(e) =>
|
||||
setGitConfig((current) => ({ ...(current || { provider: 'github', token: '', branch: 'main' }), repoUrl: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="git-branch">Branch</Label>
|
||||
<Input
|
||||
id="git-branch"
|
||||
placeholder="main"
|
||||
value={gitConfig?.branch || 'main'}
|
||||
onChange={(e) =>
|
||||
setGitConfig((current) => ({ ...(current || { provider: 'github', token: '', repoUrl: '' }), branch: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="git-token">Access Token</Label>
|
||||
<Input
|
||||
id="git-token"
|
||||
type="password"
|
||||
placeholder="ghp_xxxxxxxxxxxx"
|
||||
value={gitConfig?.token || ''}
|
||||
onChange={(e) =>
|
||||
setGitConfig((current) => ({ ...(current || { provider: 'github', repoUrl: '', branch: 'main' }), token: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowGitDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
setShowGitDialog(false)
|
||||
toast.success('Git configuration saved')
|
||||
}}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={showNewFileDialog} onOpenChange={setShowNewFileDialog}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New {newFileType === 'file' ? 'File' : 'Folder'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="file-type">Type</Label>
|
||||
<Select
|
||||
value={newFileType}
|
||||
onValueChange={(value: 'file' | 'folder') => setNewFileType(value)}
|
||||
>
|
||||
<SelectTrigger id="file-type">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="file">File</SelectItem>
|
||||
<SelectItem value="folder">Folder</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="file-name">Name</Label>
|
||||
<Input
|
||||
id="file-name"
|
||||
placeholder={newFileType === 'file' ? 'example.tsx' : 'folder-name'}
|
||||
value={newFileName}
|
||||
onChange={(e) => setNewFileName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleCreateFile()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowNewFileDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateFile}>Create</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user