mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-30 08:44:57 +00:00
refactor: modularize nerd mode ide layout
This commit is contained in:
@@ -1,246 +1,58 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Card, CardContent } from '@/components/ui'
|
||||
import { toast } from 'sonner'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import type { FileNode } from '@/lib/nerd-mode-ide'
|
||||
import {
|
||||
appendExportPath,
|
||||
buildZipFromFileTree,
|
||||
fileTreeOperations,
|
||||
getPackageTemplateById,
|
||||
getPackageTemplates,
|
||||
} from '@/lib/nerd-mode-ide'
|
||||
import { Card, CardContent } from '@/components/ui'
|
||||
import { GitConfigDialog } from '../dialogs/GitConfigDialog'
|
||||
import { NerdModeEditorPanel } from '../panels/NerdModeEditorPanel'
|
||||
import { NerdModeIDEFileExplorer } from '../components/NerdModeIDEFileExplorer'
|
||||
import { NerdModeIDEHeader } from '../components/NerdModeIDEHeader'
|
||||
import { NewItemDialog } from '../dialogs/NewItemDialog'
|
||||
import { TemplateDialog } from '../dialogs/TemplateDialog'
|
||||
import type { GitConfig, TestResult } from './types'
|
||||
import { EditorPane } from './NerdModeIDE/EditorPane'
|
||||
import { Sidebar } from './NerdModeIDE/Sidebar'
|
||||
import { useNerdIdeState } from './NerdModeIDE/useNerdIdeState'
|
||||
|
||||
interface NerdModeIDEProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function NerdModeIDE({ className }: NerdModeIDEProps) {
|
||||
const templates = useMemo(() => getPackageTemplates(), [])
|
||||
const defaultTemplate = templates[0]
|
||||
|
||||
const [fileTree, setFileTree] = useKV<FileNode[]>('nerd-mode-file-tree', defaultTemplate.tree)
|
||||
const [workspaceName, setWorkspaceName] = useKV<string>('nerd-mode-workspace', defaultTemplate.rootName)
|
||||
const [selectedFileId, setSelectedFileId] = useState<string | null>(null)
|
||||
const [activeFolderId, setActiveFolderId] = useState<string | null>(fileTree?.[0]?.id ?? null)
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [gitConfig, setGitConfig] = useKV<GitConfig | null>('nerd-mode-git-config', null)
|
||||
const [showGitDialog, setShowGitDialog] = useState(false)
|
||||
const [showNewItemDialog, setShowNewItemDialog] = useState(false)
|
||||
const [showTemplateDialog, setShowTemplateDialog] = useState(false)
|
||||
const [newItemName, setNewItemName] = useState('')
|
||||
const [newItemType, setNewItemType] = useState<'file' | 'folder'>('file')
|
||||
const [testResults, setTestResults] = useState<TestResult[]>([])
|
||||
const [consoleOutput, setConsoleOutput] = useState<string[]>([])
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [gitCommitMessage, setGitCommitMessage] = useState('')
|
||||
|
||||
const selectedFile = useMemo(() => {
|
||||
if (!fileTree || !selectedFileId) return null
|
||||
return fileTreeOperations.findNodeById(fileTree, selectedFileId)
|
||||
}, [fileTree, selectedFileId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!fileTree) return
|
||||
if (!selectedFileId) {
|
||||
const firstFile = fileTreeOperations.findFirstFile(fileTree)
|
||||
if (firstFile) {
|
||||
setSelectedFileId(firstFile.id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const fileNode = fileTreeOperations.findNodeById(fileTree, selectedFileId)
|
||||
if (fileNode?.type === 'file') {
|
||||
setFileContent(fileNode.content ?? '')
|
||||
}
|
||||
}, [fileTree, selectedFileId])
|
||||
|
||||
const handleToggleFolder = (nodeId: string) => {
|
||||
if (!fileTree) return
|
||||
const node = fileTreeOperations.findNodeById(fileTree, nodeId)
|
||||
if (node?.type === 'folder') {
|
||||
setFileTree(fileTreeOperations.updateNode(fileTree, nodeId, { expanded: !node.expanded }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveFile = () => {
|
||||
if (!fileTree || !selectedFileId) return
|
||||
setFileTree(fileTreeOperations.updateNode(fileTree, selectedFileId, { content: fileContent }))
|
||||
toast.success(`Saved ${selectedFile?.name || 'file'}`)
|
||||
}
|
||||
|
||||
const handleDeleteFile = () => {
|
||||
if (!fileTree || !selectedFileId) return
|
||||
setFileTree(fileTreeOperations.deleteNode(fileTree, selectedFileId))
|
||||
setSelectedFileId(null)
|
||||
setFileContent('')
|
||||
toast.success(`Deleted ${selectedFile?.name || 'file'}`)
|
||||
}
|
||||
|
||||
const handleCreateItem = () => {
|
||||
if (!newItemName.trim() || !fileTree) {
|
||||
toast.error('Please enter a name')
|
||||
return
|
||||
}
|
||||
|
||||
const parentNode = activeFolderId
|
||||
? fileTreeOperations.findNodeById(fileTree, activeFolderId)
|
||||
: null
|
||||
|
||||
const exportPath = parentNode?.exportPath
|
||||
? appendExportPath(parentNode.exportPath, newItemName)
|
||||
: undefined
|
||||
|
||||
const newNode = newItemType === 'file'
|
||||
? fileTreeOperations.createFileNode({ name: newItemName, exportPath })
|
||||
: fileTreeOperations.createFolderNode({ name: newItemName, exportPath, expanded: true })
|
||||
|
||||
setFileTree(fileTreeOperations.appendNode(fileTree, activeFolderId, newNode))
|
||||
setNewItemName('')
|
||||
setShowNewItemDialog(false)
|
||||
|
||||
if (newNode.type === 'file') {
|
||||
setSelectedFileId(newNode.id)
|
||||
setFileContent(newNode.content ?? '')
|
||||
}
|
||||
|
||||
toast.success(`Created ${newNode.name}`)
|
||||
}
|
||||
|
||||
const handleRunCode = () => {
|
||||
setIsRunning(true)
|
||||
setConsoleOutput((current) => [...current, `> Running ${selectedFile?.name || 'code'}...`])
|
||||
|
||||
setTimeout(() => {
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
'OK Code executed successfully',
|
||||
'> Output: Hello from MetaBuilder IDE!',
|
||||
])
|
||||
setIsRunning(false)
|
||||
toast.success('Code executed')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleRunTests = () => {
|
||||
setConsoleOutput((current) => [...current, '> Running test suite...'])
|
||||
|
||||
const mockTests: TestResult[] = [
|
||||
{ name: 'Feed component renders', status: 'passed', duration: 45 },
|
||||
{ name: 'Package export bundles files', status: 'passed', duration: 123 },
|
||||
{ name: 'Lua manifest loads', status: 'passed', duration: 234 },
|
||||
{ name: 'Permissions hook', status: 'passed', duration: 67 },
|
||||
]
|
||||
|
||||
setTimeout(() => {
|
||||
setTestResults(mockTests)
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
`OK ${mockTests.filter((test) => test.status === 'passed').length} tests passed`,
|
||||
`FAIL ${mockTests.filter((test) => test.status === 'failed').length} tests failed`,
|
||||
])
|
||||
toast.success('Tests completed')
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const handleGitPush = () => {
|
||||
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,
|
||||
`OK Pushed to ${gitConfig.provider} (${gitConfig.repoUrl})`,
|
||||
])
|
||||
setGitCommitMessage('')
|
||||
toast.success('Changes pushed to repository')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleGitPull = () => {
|
||||
if (!gitConfig) {
|
||||
toast.error('Please configure Git first')
|
||||
setShowGitDialog(true)
|
||||
return
|
||||
}
|
||||
|
||||
setConsoleOutput((current) => [...current, `> git pull origin ${gitConfig.branch}`])
|
||||
|
||||
setTimeout(() => {
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
`OK Pulled latest changes from ${gitConfig.branch}`,
|
||||
])
|
||||
toast.success('Repository updated')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleExportZip = async () => {
|
||||
if (!fileTree || fileTree.length === 0) {
|
||||
toast.error('No files to export')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await buildZipFromFileTree(fileTree)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = `${workspaceName || 'workspace'}.zip`
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
toast.success('Zip exported')
|
||||
} catch (error) {
|
||||
toast.error('Failed to export zip')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTemplateSelect = (templateId: string) => {
|
||||
const template = getPackageTemplateById(templateId)
|
||||
if (!template) return
|
||||
|
||||
setFileTree(template.tree)
|
||||
setWorkspaceName(template.rootName)
|
||||
setActiveFolderId(template.tree[0]?.id ?? null)
|
||||
const firstFile = fileTreeOperations.findFirstFile(template.tree)
|
||||
setSelectedFileId(firstFile?.id ?? null)
|
||||
setFileContent(firstFile?.content ?? '')
|
||||
setShowTemplateDialog(false)
|
||||
toast.success(`Loaded ${template.name}`)
|
||||
}
|
||||
|
||||
const handleUpdateGitConfig = (updates: Partial<GitConfig>) => {
|
||||
setGitConfig((current) => ({
|
||||
provider: current?.provider ?? 'github',
|
||||
repoUrl: current?.repoUrl ?? '',
|
||||
branch: current?.branch ?? 'main',
|
||||
token: current?.token ?? '',
|
||||
...updates,
|
||||
}))
|
||||
}
|
||||
const {
|
||||
activeFolderId,
|
||||
consoleOutput,
|
||||
fileContent,
|
||||
fileTree,
|
||||
gitCommitMessage,
|
||||
gitConfig,
|
||||
isRunning,
|
||||
newItemName,
|
||||
newItemType,
|
||||
selectedFile,
|
||||
selectedFileId,
|
||||
showGitDialog,
|
||||
showNewItemDialog,
|
||||
showTemplateDialog,
|
||||
templates,
|
||||
testResults,
|
||||
workspaceName,
|
||||
handleCreateItem,
|
||||
handleDeleteFile,
|
||||
handleExportZip,
|
||||
handleGitPull,
|
||||
handleGitPush,
|
||||
handleRunCode,
|
||||
handleRunTests,
|
||||
handleSaveFile,
|
||||
handleTemplateSelect,
|
||||
handleToggleFolder,
|
||||
handleUpdateGitConfig,
|
||||
setActiveFolderId,
|
||||
setConsoleOutput,
|
||||
setFileContent,
|
||||
setGitCommitMessage,
|
||||
setNewItemName,
|
||||
setNewItemType,
|
||||
setSelectedFileId,
|
||||
setShowGitDialog,
|
||||
setShowNewItemDialog,
|
||||
setShowTemplateDialog,
|
||||
} = useNerdIdeState()
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
@@ -254,7 +66,7 @@ export function NerdModeIDE({ className }: NerdModeIDEProps) {
|
||||
/>
|
||||
<CardContent className="p-0">
|
||||
<div className="grid grid-cols-4 h-[calc(100vh-200px)]">
|
||||
<NerdModeIDEFileExplorer
|
||||
<Sidebar
|
||||
nodes={fileTree || []}
|
||||
selectedFileId={selectedFileId}
|
||||
activeFolderId={activeFolderId}
|
||||
@@ -262,7 +74,7 @@ export function NerdModeIDE({ className }: NerdModeIDEProps) {
|
||||
onSelectFile={setSelectedFileId}
|
||||
onSelectFolder={setActiveFolderId}
|
||||
/>
|
||||
<NerdModeEditorPanel
|
||||
<EditorPane
|
||||
selectedFile={selectedFile && selectedFile.type === 'file' ? selectedFile : null}
|
||||
fileContent={fileContent}
|
||||
isRunning={isRunning}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { FileNode } from '@/lib/nerd-mode-ide'
|
||||
import type { GitConfig, TestResult } from '../types'
|
||||
import { NerdModeEditorPanel } from '../../panels/NerdModeEditorPanel'
|
||||
|
||||
interface EditorPaneProps {
|
||||
selectedFile: FileNode | null
|
||||
fileContent: string
|
||||
isRunning: boolean
|
||||
consoleOutput: string[]
|
||||
testResults: TestResult[]
|
||||
gitConfig: GitConfig | null
|
||||
gitCommitMessage: string
|
||||
onFileChange: (value: string) => void
|
||||
onRunCode: () => void
|
||||
onSaveFile: () => void
|
||||
onDeleteFile: () => void
|
||||
onClearConsole: () => void
|
||||
onRunTests: () => void
|
||||
onCommitMessageChange: (value: string) => void
|
||||
onGitPush: () => void
|
||||
onGitPull: () => void
|
||||
onOpenGitConfig: () => void
|
||||
}
|
||||
|
||||
export function EditorPane(props: EditorPaneProps) {
|
||||
return <NerdModeEditorPanel {...props} />
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { FileNode } from '@/lib/nerd-mode-ide'
|
||||
import { NerdModeIDEFileExplorer } from '../../components/NerdModeIDEFileExplorer'
|
||||
|
||||
interface SidebarProps {
|
||||
nodes: FileNode[]
|
||||
selectedFileId: string | null
|
||||
activeFolderId: string | null
|
||||
onToggleFolder: (nodeId: string) => void
|
||||
onSelectFile: (nodeId: string) => void
|
||||
onSelectFolder: (nodeId: string | null) => void
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
nodes,
|
||||
selectedFileId,
|
||||
activeFolderId,
|
||||
onToggleFolder,
|
||||
onSelectFile,
|
||||
onSelectFolder,
|
||||
}: SidebarProps) {
|
||||
return (
|
||||
<NerdModeIDEFileExplorer
|
||||
nodes={nodes}
|
||||
selectedFileId={selectedFileId}
|
||||
activeFolderId={activeFolderId}
|
||||
onToggleFolder={onToggleFolder}
|
||||
onSelectFile={onSelectFile}
|
||||
onSelectFolder={onSelectFolder}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import type { FileNode } from '@/lib/nerd-mode-ide'
|
||||
import {
|
||||
appendExportPath,
|
||||
buildZipFromFileTree,
|
||||
fileTreeOperations,
|
||||
getPackageTemplateById,
|
||||
getPackageTemplates,
|
||||
} from '@/lib/nerd-mode-ide'
|
||||
import type { GitConfig, TestResult } from '../types'
|
||||
|
||||
export function useNerdIdeState() {
|
||||
const templates = useMemo(() => getPackageTemplates(), [])
|
||||
const defaultTemplate = templates[0]
|
||||
|
||||
const [fileTree, setFileTree] = useKV<FileNode[]>('nerd-mode-file-tree', defaultTemplate.tree)
|
||||
const [workspaceName, setWorkspaceName] = useKV<string>('nerd-mode-workspace', defaultTemplate.rootName)
|
||||
const [selectedFileId, setSelectedFileId] = useState<string | null>(null)
|
||||
const [activeFolderId, setActiveFolderId] = useState<string | null>(fileTree?.[0]?.id ?? null)
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [gitConfig, setGitConfig] = useKV<GitConfig | null>('nerd-mode-git-config', null)
|
||||
const [showGitDialog, setShowGitDialog] = useState(false)
|
||||
const [showNewItemDialog, setShowNewItemDialog] = useState(false)
|
||||
const [showTemplateDialog, setShowTemplateDialog] = useState(false)
|
||||
const [newItemName, setNewItemName] = useState('')
|
||||
const [newItemType, setNewItemType] = useState<'file' | 'folder'>('file')
|
||||
const [testResults, setTestResults] = useState<TestResult[]>([])
|
||||
const [consoleOutput, setConsoleOutput] = useState<string[]>([])
|
||||
const [isRunning, setIsRunning] = useState(false)
|
||||
const [gitCommitMessage, setGitCommitMessage] = useState('')
|
||||
|
||||
const selectedFile = useMemo(() => {
|
||||
if (!fileTree || !selectedFileId) return null
|
||||
return fileTreeOperations.findNodeById(fileTree, selectedFileId)
|
||||
}, [fileTree, selectedFileId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!fileTree) return
|
||||
if (!selectedFileId) {
|
||||
const firstFile = fileTreeOperations.findFirstFile(fileTree)
|
||||
if (firstFile) {
|
||||
setSelectedFileId(firstFile.id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const fileNode = fileTreeOperations.findNodeById(fileTree, selectedFileId)
|
||||
if (fileNode?.type === 'file') {
|
||||
setFileContent(fileNode.content ?? '')
|
||||
}
|
||||
}, [fileTree, selectedFileId])
|
||||
|
||||
const handleToggleFolder = (nodeId: string) => {
|
||||
if (!fileTree) return
|
||||
const node = fileTreeOperations.findNodeById(fileTree, nodeId)
|
||||
if (node?.type === 'folder') {
|
||||
setFileTree(fileTreeOperations.updateNode(fileTree, nodeId, { expanded: !node.expanded }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveFile = () => {
|
||||
if (!fileTree || !selectedFileId) return
|
||||
setFileTree(fileTreeOperations.updateNode(fileTree, selectedFileId, { content: fileContent }))
|
||||
toast.success(`Saved ${selectedFile?.name || 'file'}`)
|
||||
}
|
||||
|
||||
const handleDeleteFile = () => {
|
||||
if (!fileTree || !selectedFileId) return
|
||||
setFileTree(fileTreeOperations.deleteNode(fileTree, selectedFileId))
|
||||
setSelectedFileId(null)
|
||||
setFileContent('')
|
||||
toast.success(`Deleted ${selectedFile?.name || 'file'}`)
|
||||
}
|
||||
|
||||
const handleCreateItem = () => {
|
||||
if (!newItemName.trim() || !fileTree) {
|
||||
toast.error('Please enter a name')
|
||||
return
|
||||
}
|
||||
|
||||
const parentNode = activeFolderId
|
||||
? fileTreeOperations.findNodeById(fileTree, activeFolderId)
|
||||
: null
|
||||
|
||||
const exportPath = parentNode?.exportPath
|
||||
? appendExportPath(parentNode.exportPath, newItemName)
|
||||
: undefined
|
||||
|
||||
const newNode = newItemType === 'file'
|
||||
? fileTreeOperations.createFileNode({ name: newItemName, exportPath })
|
||||
: fileTreeOperations.createFolderNode({ name: newItemName, exportPath, expanded: true })
|
||||
|
||||
setFileTree(fileTreeOperations.appendNode(fileTree, activeFolderId, newNode))
|
||||
setNewItemName('')
|
||||
setShowNewItemDialog(false)
|
||||
|
||||
if (newNode.type === 'file') {
|
||||
setSelectedFileId(newNode.id)
|
||||
setFileContent(newNode.content ?? '')
|
||||
}
|
||||
|
||||
toast.success(`Created ${newNode.name}`)
|
||||
}
|
||||
|
||||
const handleRunCode = () => {
|
||||
setIsRunning(true)
|
||||
setConsoleOutput((current) => [...current, `> Running ${selectedFile?.name || 'code'}...`])
|
||||
|
||||
setTimeout(() => {
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
'OK Code executed successfully',
|
||||
'> Output: Hello from MetaBuilder IDE!',
|
||||
])
|
||||
setIsRunning(false)
|
||||
toast.success('Code executed')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleRunTests = () => {
|
||||
setConsoleOutput((current) => [...current, '> Running test suite...'])
|
||||
|
||||
const mockTests: TestResult[] = [
|
||||
{ name: 'Feed component renders', status: 'passed', duration: 45 },
|
||||
{ name: 'Package export bundles files', status: 'passed', duration: 123 },
|
||||
{ name: 'Lua manifest loads', status: 'passed', duration: 234 },
|
||||
{ name: 'Permissions hook', status: 'passed', duration: 67 },
|
||||
]
|
||||
|
||||
setTimeout(() => {
|
||||
setTestResults(mockTests)
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
`OK ${mockTests.filter((test) => test.status === 'passed').length} tests passed`,
|
||||
`FAIL ${mockTests.filter((test) => test.status === 'failed').length} tests failed`,
|
||||
])
|
||||
toast.success('Tests completed')
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const handleGitPush = () => {
|
||||
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,
|
||||
`OK Pushed to ${gitConfig.provider} (${gitConfig.repoUrl})`,
|
||||
])
|
||||
setGitCommitMessage('')
|
||||
toast.success('Changes pushed to repository')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleGitPull = () => {
|
||||
if (!gitConfig) {
|
||||
toast.error('Please configure Git first')
|
||||
setShowGitDialog(true)
|
||||
return
|
||||
}
|
||||
|
||||
setConsoleOutput((current) => [...current, `> git pull origin ${gitConfig.branch}`])
|
||||
|
||||
setTimeout(() => {
|
||||
setConsoleOutput((current) => [
|
||||
...current,
|
||||
`OK Pulled latest changes from ${gitConfig.branch}`,
|
||||
])
|
||||
toast.success('Repository updated')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleExportZip = async () => {
|
||||
if (!fileTree || fileTree.length === 0) {
|
||||
toast.error('No files to export')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await buildZipFromFileTree(fileTree)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = `${workspaceName || 'workspace'}.zip`
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
toast.success('Zip exported')
|
||||
} catch (error) {
|
||||
toast.error('Failed to export zip')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTemplateSelect = (templateId: string) => {
|
||||
const template = getPackageTemplateById(templateId)
|
||||
if (!template) return
|
||||
|
||||
setFileTree(template.tree)
|
||||
setWorkspaceName(template.rootName)
|
||||
setActiveFolderId(template.tree[0]?.id ?? null)
|
||||
const firstFile = fileTreeOperations.findFirstFile(template.tree)
|
||||
setSelectedFileId(firstFile?.id ?? null)
|
||||
setFileContent(firstFile?.content ?? '')
|
||||
setShowTemplateDialog(false)
|
||||
toast.success(`Loaded ${template.name}`)
|
||||
}
|
||||
|
||||
const handleUpdateGitConfig = (updates: Partial<GitConfig>) => {
|
||||
setGitConfig((current) => ({
|
||||
provider: current?.provider ?? 'github',
|
||||
repoUrl: current?.repoUrl ?? '',
|
||||
branch: current?.branch ?? 'main',
|
||||
token: current?.token ?? '',
|
||||
...updates,
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
activeFolderId,
|
||||
consoleOutput,
|
||||
fileContent,
|
||||
fileTree,
|
||||
gitCommitMessage,
|
||||
gitConfig,
|
||||
isRunning,
|
||||
newItemName,
|
||||
newItemType,
|
||||
selectedFile,
|
||||
selectedFileId,
|
||||
showGitDialog,
|
||||
showNewItemDialog,
|
||||
showTemplateDialog,
|
||||
templates,
|
||||
testResults,
|
||||
workspaceName,
|
||||
handleCreateItem,
|
||||
handleDeleteFile,
|
||||
handleExportZip,
|
||||
handleGitPull,
|
||||
handleGitPush,
|
||||
handleRunCode,
|
||||
handleRunTests,
|
||||
handleSaveFile,
|
||||
handleTemplateSelect,
|
||||
handleToggleFolder,
|
||||
handleUpdateGitConfig,
|
||||
setActiveFolderId,
|
||||
setConsoleOutput,
|
||||
setFileContent,
|
||||
setGitCommitMessage,
|
||||
setNewItemName,
|
||||
setNewItemType,
|
||||
setSelectedFileId,
|
||||
setShowGitDialog,
|
||||
setShowNewItemDialog,
|
||||
setShowTemplateDialog,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user