Generated by Spark: Too risky making changes without refactoring now. Create hook library, All components <150LOC.

This commit is contained in:
2026-01-16 18:11:41 +00:00
committed by GitHub
parent c81277fadd
commit ba3dcf538a
21 changed files with 1152 additions and 155 deletions

View File

@@ -1,21 +1,12 @@
import { useState } from 'react'
import Editor from '@monaco-editor/react'
import { Card } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button'
import { ProjectFile } from '@/types/project'
import { FileCode, X, Sparkle, Info } from '@phosphor-icons/react'
import { AIService } from '@/lib/ai-service'
import { toast } from 'sonner'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Textarea } from '@/components/ui/textarea'
import { ScrollArea } from '@/components/ui/scroll-area'
import { useDialogState } from '@/hooks/use-dialog-state'
import { useFileFilters } from '@/hooks/use-file-filters'
import { useCodeExplanation } from '@/hooks/use-code-explanation'
import { useAIOperations } from '@/hooks/use-ai-operations'
import { EditorToolbar } from '@/components/molecules/EditorToolbar'
import { MonacoEditorPanel } from '@/components/molecules/MonacoEditorPanel'
import { EmptyEditorState } from '@/components/molecules/EmptyEditorState'
import { CodeExplanationDialog } from '@/components/molecules/CodeExplanationDialog'
interface CodeEditorProps {
files: ProjectFile[]
@@ -32,163 +23,65 @@ export function CodeEditor({
onFileSelect,
onFileClose,
}: CodeEditorProps) {
const [showExplainDialog, setShowExplainDialog] = useState(false)
const [explanation, setExplanation] = useState('')
const [isExplaining, setIsExplaining] = useState(false)
const { isOpen: showExplainDialog, setIsOpen: setShowExplainDialog } = useDialogState()
const { explanation, isExplaining, explain } = useCodeExplanation()
const { improveCode } = useAIOperations()
const { getOpenFiles, findFileById } = useFileFilters(files)
const activeFile = files.find((f) => f.id === activeFileId)
const openFiles = files.filter((f) => f.id === activeFileId || files.length < 5)
const activeFile = findFileById(activeFileId) || undefined
const openFiles = getOpenFiles(activeFileId)
const improveCodeWithAI = async () => {
const handleImproveCode = async () => {
if (!activeFile) return
const instruction = prompt('How would you like to improve this code?')
if (!instruction) return
try {
toast.info('Improving code with AI...')
const improvedCode = await AIService.improveCode(activeFile.content, instruction)
if (improvedCode) {
onFileChange(activeFile.id, improvedCode)
toast.success('Code improved successfully!')
} else {
toast.error('AI improvement failed. Please try again.')
}
} catch (error) {
toast.error('Failed to improve code')
console.error(error)
const improvedCode = await improveCode(activeFile.content, instruction)
if (improvedCode) {
onFileChange(activeFile.id, improvedCode)
}
}
const explainCode = async () => {
const handleExplainCode = async () => {
if (!activeFile) return
try {
setIsExplaining(true)
setShowExplainDialog(true)
setExplanation('Analyzing code...')
const codeExplanation = await AIService.explainCode(activeFile.content)
if (codeExplanation) {
setExplanation(codeExplanation)
} else {
setExplanation('Failed to generate explanation. Please try again.')
}
} catch (error) {
setExplanation('Error generating explanation.')
console.error(error)
} finally {
setIsExplaining(false)
}
setShowExplainDialog(true)
await explain(activeFile.content)
}
return (
<div className="h-full flex flex-col">
{openFiles.length > 0 ? (
<>
<div className="flex items-center gap-1 bg-secondary/50 border-b border-border px-2 py-1 justify-between">
<div className="flex items-center gap-1">
{openFiles.map((file) => (
<button
key={file.id}
onClick={() => onFileSelect(file.id)}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${
file.id === activeFileId
? 'bg-card text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
}`}
>
<FileCode size={16} />
<span>{file.name}</span>
<button
onClick={(e) => {
e.stopPropagation()
onFileClose(file.id)
}}
className="hover:text-destructive"
>
<X size={14} />
</button>
</button>
))}
</div>
{activeFile && (
<div className="flex gap-2">
<Button
size="sm"
variant="ghost"
onClick={explainCode}
className="h-7 text-xs"
>
<Info size={14} className="mr-1" />
Explain
</Button>
<Button
size="sm"
variant="ghost"
onClick={improveCodeWithAI}
className="h-7 text-xs"
>
<Sparkle size={14} className="mr-1" weight="duotone" />
Improve
</Button>
</div>
)}
</div>
<EditorToolbar
openFiles={openFiles}
activeFileId={activeFileId}
activeFile={activeFile}
onFileSelect={onFileSelect}
onFileClose={onFileClose}
onExplain={handleExplainCode}
onImprove={handleImproveCode}
/>
<div className="flex-1">
{activeFile && (
<Editor
height="100%"
language={activeFile.language}
value={activeFile.content}
onChange={(value) => onFileChange(activeFile.id, value || '')}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 14,
fontFamily: 'JetBrains Mono, monospace',
fontLigatures: true,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
<MonacoEditorPanel
file={activeFile}
onChange={(content) => onFileChange(activeFile.id, content)}
/>
)}
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<div className="text-center">
<FileCode size={48} className="mx-auto mb-4 opacity-50" />
<p>Select a file to edit</p>
</div>
</div>
<EmptyEditorState />
)}
<Dialog open={showExplainDialog} onOpenChange={setShowExplainDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Code Explanation</DialogTitle>
<DialogDescription>
AI-generated explanation of {activeFile?.name}
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-96">
<div className="p-4 bg-muted rounded-lg">
{isExplaining ? (
<div className="flex items-center gap-2 text-muted-foreground">
<Sparkle size={16} weight="duotone" className="animate-pulse" />
Analyzing code...
</div>
) : (
<p className="whitespace-pre-wrap text-sm">{explanation}</p>
)}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
<CodeExplanationDialog
open={showExplainDialog}
onOpenChange={setShowExplainDialog}
fileName={activeFile?.name}
explanation={explanation}
isLoading={isExplaining}
/>
</div>
)
}

View File

@@ -0,0 +1,50 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Sparkle } from '@phosphor-icons/react'
interface CodeExplanationDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
fileName: string | undefined
explanation: string
isLoading: boolean
}
export function CodeExplanationDialog({
open,
onOpenChange,
fileName,
explanation,
isLoading,
}: CodeExplanationDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Code Explanation</DialogTitle>
<DialogDescription>
AI-generated explanation of {fileName}
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-96">
<div className="p-4 bg-muted rounded-lg">
{isLoading ? (
<div className="flex items-center gap-2 text-muted-foreground">
<Sparkle size={16} weight="duotone" className="animate-pulse" />
Analyzing code...
</div>
) : (
<p className="whitespace-pre-wrap text-sm">{explanation}</p>
)}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,32 @@
import { Button } from '@/components/ui/button'
import { Info, Sparkle } from '@phosphor-icons/react'
interface EditorActionsProps {
onExplain: () => void
onImprove: () => void
}
export function EditorActions({ onExplain, onImprove }: EditorActionsProps) {
return (
<div className="flex gap-2">
<Button
size="sm"
variant="ghost"
onClick={onExplain}
className="h-7 text-xs"
>
<Info size={14} className="mr-1" />
Explain
</Button>
<Button
size="sm"
variant="ghost"
onClick={onImprove}
className="h-7 text-xs"
>
<Sparkle size={14} className="mr-1" weight="duotone" />
Improve
</Button>
</div>
)
}

View File

@@ -0,0 +1,40 @@
import { ProjectFile } from '@/types/project'
import { FileTabs } from './FileTabs'
import { EditorActions } from './EditorActions'
interface EditorToolbarProps {
openFiles: ProjectFile[]
activeFileId: string | null
activeFile: ProjectFile | undefined
onFileSelect: (fileId: string) => void
onFileClose: (fileId: string) => void
onExplain: () => void
onImprove: () => void
}
export function EditorToolbar({
openFiles,
activeFileId,
activeFile,
onFileSelect,
onFileClose,
onExplain,
onImprove,
}: EditorToolbarProps) {
return (
<div className="flex items-center gap-1 bg-secondary/50 border-b border-border px-2 py-1 justify-between">
<FileTabs
files={openFiles}
activeFileId={activeFileId}
onFileSelect={onFileSelect}
onFileClose={onFileClose}
/>
{activeFile && (
<EditorActions
onExplain={onExplain}
onImprove={onImprove}
/>
)}
</div>
)
}

View File

@@ -0,0 +1,12 @@
import { FileCode } from '@phosphor-icons/react'
export function EmptyEditorState() {
return (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<div className="text-center">
<FileCode size={48} className="mx-auto mb-4 opacity-50" />
<p>Select a file to edit</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,39 @@
import { ProjectFile } from '@/types/project'
import { FileCode, X } from '@phosphor-icons/react'
interface FileTabsProps {
files: ProjectFile[]
activeFileId: string | null
onFileSelect: (fileId: string) => void
onFileClose: (fileId: string) => void
}
export function FileTabs({ files, activeFileId, onFileSelect, onFileClose }: FileTabsProps) {
return (
<div className="flex items-center gap-1">
{files.map((file) => (
<button
key={file.id}
onClick={() => onFileSelect(file.id)}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${
file.id === activeFileId
? 'bg-card text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
}`}
>
<FileCode size={16} />
<span>{file.name}</span>
<button
onClick={(e) => {
e.stopPropagation()
onFileClose(file.id)
}}
className="hover:text-destructive"
>
<X size={14} />
</button>
</button>
))}
</div>
)
}

View File

@@ -0,0 +1,28 @@
import Editor from '@monaco-editor/react'
import { ProjectFile } from '@/types/project'
interface MonacoEditorPanelProps {
file: ProjectFile
onChange: (content: string) => void
}
export function MonacoEditorPanel({ file, onChange }: MonacoEditorPanelProps) {
return (
<Editor
height="100%"
language={file.language}
value={file.content}
onChange={(value) => onChange(value || '')}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 14,
fontFamily: 'JetBrains Mono, monospace',
fontLigatures: true,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
)
}

View File

@@ -1,13 +1,19 @@
export { SaveIndicator } from './SaveIndicator'
export { AppBranding } from './AppBranding'
export { PageHeaderContent } from './PageHeaderContent'
export { ToolbarButton } from './ToolbarButton'
export { NavigationItem } from './NavigationItem'
export { NavigationGroupHeader } from './NavigationGroupHeader'
export { CodeExplanationDialog } from './CodeExplanationDialog'
export { EditorActions } from './EditorActions'
export { EditorToolbar } from './EditorToolbar'
export { EmptyEditorState } from './EmptyEditorState'
export { EmptyState } from './EmptyState'
export { LoadingState } from './LoadingState'
export { StatCard } from './StatCard'
export { FileTabs } from './FileTabs'
export { LabelWithBadge } from './LabelWithBadge'
export { LoadingState } from './LoadingState'
export { MonacoEditorPanel } from './MonacoEditorPanel'
export { NavigationGroupHeader } from './NavigationGroupHeader'
export { NavigationItem } from './NavigationItem'
export { PageHeaderContent } from './PageHeaderContent'
export { SaveIndicator } from './SaveIndicator'
export { StatCard } from './StatCard'
export { ToolbarButton } from './ToolbarButton'
export { TreeCard } from './TreeCard'
export { TreeFormDialog } from './TreeFormDialog'
export { TreeListHeader } from './TreeListHeader'

15
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,15 @@
export { useProjectState } from './use-project-state'
export { useFileOperations } from './use-file-operations'
export { useAIOperations } from './use-ai-operations'
export { useProjectExport } from './use-project-export'
export { useDialogState, useMultipleDialogs } from './use-dialog-state'
export { useActiveSelection } from './use-active-selection'
export { useLastSaved } from './use-last-saved'
export { useTabNavigation } from './use-tab-navigation'
export { useCodeExplanation } from './use-code-explanation'
export { useFileFilters } from './use-file-filters'
export { useProjectLoader } from './use-project-loader'
export { useAutoRepair } from './use-auto-repair'
export { useKeyboardShortcuts } from './use-keyboard-shortcuts'
export { useIsMobile } from './use-mobile'
export { usePWA } from './use-pwa'

View File

@@ -0,0 +1,49 @@
import { useState, useEffect } from 'react'
export function useActiveSelection<T extends { id: string }>(items: T[], defaultId?: string | null) {
const [activeId, setActiveId] = useState<string | null>(defaultId || null)
useEffect(() => {
if (items.length > 0 && !activeId) {
setActiveId(items[0].id)
}
}, [items, activeId])
const activeItem = items.find(item => item.id === activeId)
const selectNext = () => {
if (!activeId || items.length === 0) return
const currentIndex = items.findIndex(item => item.id === activeId)
const nextIndex = (currentIndex + 1) % items.length
setActiveId(items[nextIndex].id)
}
const selectPrevious = () => {
if (!activeId || items.length === 0) return
const currentIndex = items.findIndex(item => item.id === activeId)
const previousIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1
setActiveId(items[previousIndex].id)
}
const selectFirst = () => {
if (items.length > 0) {
setActiveId(items[0].id)
}
}
const selectLast = () => {
if (items.length > 0) {
setActiveId(items[items.length - 1].id)
}
}
return {
activeId,
setActiveId,
activeItem,
selectNext,
selectPrevious,
selectFirst,
selectLast,
}
}

View File

@@ -0,0 +1,73 @@
import { useState } from 'react'
import { AIService } from '@/lib/ai-service'
import { toast } from 'sonner'
import { ProjectFile, PrismaModel, ThemeConfig } from '@/types/project'
export function useAIOperations() {
const [isProcessing, setIsProcessing] = useState(false)
const improveCode = async (content: string, instruction: string) => {
try {
setIsProcessing(true)
toast.info('Improving code with AI...')
const improvedCode = await AIService.improveCode(content, instruction)
if (improvedCode) {
toast.success('Code improved successfully!')
return improvedCode
} else {
toast.error('AI improvement failed. Please try again.')
return null
}
} catch (error) {
toast.error('Failed to improve code')
console.error(error)
return null
} finally {
setIsProcessing(false)
}
}
const explainCode = async (content: string) => {
try {
setIsProcessing(true)
const codeExplanation = await AIService.explainCode(content)
return codeExplanation || 'Failed to generate explanation. Please try again.'
} catch (error) {
console.error(error)
return 'Error generating explanation.'
} finally {
setIsProcessing(false)
}
}
const generateCompleteApp = async (description: string) => {
try {
setIsProcessing(true)
toast.info('Generating application with AI...')
const result = await AIService.generateCompleteApp(description)
if (result) {
toast.success('Application generated successfully!')
return result
} else {
toast.error('AI generation failed. Please try again.')
return null
}
} catch (error) {
toast.error('AI generation failed')
console.error(error)
return null
} finally {
setIsProcessing(false)
}
}
return {
isProcessing,
improveCode,
explainCode,
generateCompleteApp,
}
}

View File

@@ -0,0 +1,31 @@
import { useState } from 'react'
import { useAIOperations } from './use-ai-operations'
export function useCodeExplanation() {
const [explanation, setExplanation] = useState('')
const [isExplaining, setIsExplaining] = useState(false)
const { explainCode } = useAIOperations()
const explain = async (code: string) => {
try {
setIsExplaining(true)
setExplanation('Analyzing code...')
const result = await explainCode(code)
setExplanation(result)
} finally {
setIsExplaining(false)
}
}
const reset = () => {
setExplanation('')
setIsExplaining(false)
}
return {
explanation,
isExplaining,
explain,
reset,
}
}

View File

@@ -0,0 +1,43 @@
import { useState } from 'react'
export function useDialogState(initialOpen = false) {
const [isOpen, setIsOpen] = useState(initialOpen)
const open = () => setIsOpen(true)
const close = () => setIsOpen(false)
const toggle = () => setIsOpen(prev => !prev)
return {
isOpen,
open,
close,
toggle,
setIsOpen,
}
}
export function useMultipleDialogs() {
const [dialogs, setDialogs] = useState<Record<string, boolean>>({})
const openDialog = (name: string) => {
setDialogs(prev => ({ ...prev, [name]: true }))
}
const closeDialog = (name: string) => {
setDialogs(prev => ({ ...prev, [name]: false }))
}
const toggleDialog = (name: string) => {
setDialogs(prev => ({ ...prev, [name]: !prev[name] }))
}
const isDialogOpen = (name: string) => dialogs[name] || false
return {
dialogs,
openDialog,
closeDialog,
toggleDialog,
isDialogOpen,
}
}

View File

@@ -0,0 +1,27 @@
import { ProjectFile } from '@/types/project'
export function useFileFilters(files: ProjectFile[]) {
const getOpenFiles = (activeFileId: string | null, maxOpen = 5) => {
return files.filter((f) => f.id === activeFileId || files.length < maxOpen)
}
const findFileById = (fileId: string | null) => {
if (!fileId) return null
return files.find((f) => f.id === fileId) || null
}
const getFilesByLanguage = (language: string) => {
return files.filter((f) => f.language === language)
}
const getFilesByPath = (pathPrefix: string) => {
return files.filter((f) => f.path.startsWith(pathPrefix))
}
return {
getOpenFiles,
findFileById,
getFilesByLanguage,
getFilesByPath,
}
}

View File

@@ -0,0 +1,44 @@
import { useState } from 'react'
import { ProjectFile } from '@/types/project'
export function useFileOperations(
files: ProjectFile[],
setFiles: (updater: (files: ProjectFile[]) => ProjectFile[]) => void
) {
const [activeFileId, setActiveFileId] = useState<string | null>(null)
const handleFileChange = (fileId: string, content: string) => {
setFiles((currentFiles) =>
currentFiles.map((f) => (f.id === fileId ? { ...f, content } : f))
)
}
const handleFileAdd = (file: ProjectFile) => {
setFiles((currentFiles) => [...currentFiles, file])
setActiveFileId(file.id)
}
const handleFileClose = (fileId: string) => {
if (activeFileId === fileId) {
const currentIndex = files.findIndex((f) => f.id === fileId)
const nextFile = files[currentIndex + 1] || files[currentIndex - 1]
setActiveFileId(nextFile?.id || null)
}
}
const handleFileDelete = (fileId: string) => {
setFiles((currentFiles) => currentFiles.filter((f) => f.id !== fileId))
if (activeFileId === fileId) {
setActiveFileId(null)
}
}
return {
activeFileId,
setActiveFileId,
handleFileChange,
handleFileAdd,
handleFileClose,
handleFileDelete,
}
}

View File

@@ -0,0 +1,11 @@
import { useState, useEffect } from 'react'
export function useLastSaved(dependencies: any[]) {
const [lastSaved, setLastSaved] = useState<number | null>(Date.now())
useEffect(() => {
setLastSaved(Date.now())
}, dependencies)
return lastSaved
}

View File

@@ -0,0 +1,169 @@
import { useState } from 'react'
import { toast } from 'sonner'
import JSZip from 'jszip'
import {
ProjectFile,
PrismaModel,
ComponentNode,
ThemeConfig,
PlaywrightTest,
StorybookStory,
UnitTest,
FlaskConfig,
NextJsConfig,
NpmSettings,
} from '@/types/project'
import {
generateNextJSProject,
generatePrismaSchema,
generateMUITheme,
generatePlaywrightTests,
generateStorybookStories,
generateUnitTests,
generateFlaskApp,
} from '@/lib/generators'
export function useProjectExport(
files: ProjectFile[],
models: PrismaModel[],
components: ComponentNode[],
theme: ThemeConfig,
playwrightTests: PlaywrightTest[],
storybookStories: StorybookStory[],
unitTests: UnitTest[],
flaskConfig: FlaskConfig,
nextjsConfig: NextJsConfig,
npmSettings: NpmSettings
) {
const [generatedCode, setGeneratedCode] = useState<Record<string, string>>({})
const [exportDialogOpen, setExportDialogOpen] = useState(false)
const handleExportProject = () => {
const projectFiles = generateNextJSProject(nextjsConfig.appName, models, components, theme)
const prismaSchema = generatePrismaSchema(models)
const themeCode = generateMUITheme(theme)
const playwrightTestCode = generatePlaywrightTests(playwrightTests)
const storybookFiles = generateStorybookStories(storybookStories)
const unitTestFiles = generateUnitTests(unitTests)
const flaskFiles = generateFlaskApp(flaskConfig)
const packageJson = {
name: nextjsConfig.appName,
version: '0.1.0',
private: true,
scripts: npmSettings.scripts,
dependencies: npmSettings.packages
.filter(pkg => !pkg.isDev)
.reduce((acc, pkg) => {
acc[pkg.name] = pkg.version
return acc
}, {} as Record<string, string>),
devDependencies: npmSettings.packages
.filter(pkg => pkg.isDev)
.reduce((acc, pkg) => {
acc[pkg.name] = pkg.version
return acc
}, {} as Record<string, string>),
}
const allFiles: Record<string, string> = {
...projectFiles,
'package.json': JSON.stringify(packageJson, null, 2),
'prisma/schema.prisma': prismaSchema,
'src/theme.ts': themeCode,
'e2e/tests.spec.ts': playwrightTestCode,
...storybookFiles,
...unitTestFiles,
}
Object.entries(flaskFiles).forEach(([path, content]) => {
allFiles[`backend/${path}`] = content
})
files.forEach(file => {
allFiles[file.path] = file.content
})
setGeneratedCode(allFiles)
setExportDialogOpen(true)
toast.success('Project files generated!')
}
const handleDownloadZip = async () => {
try {
toast.info('Creating ZIP file...')
const zip = new JSZip()
Object.entries(generatedCode).forEach(([path, content]) => {
const cleanPath = path.startsWith('/') ? path.slice(1) : path
zip.file(cleanPath, content)
})
zip.file('README.md', `# ${nextjsConfig.appName}
Generated with CodeForge
## Getting Started
1. Install dependencies:
\`\`\`bash
npm install
\`\`\`
2. Set up Prisma (if using database):
\`\`\`bash
npx prisma generate
npx prisma db push
\`\`\`
3. Run the development server:
\`\`\`bash
npm run dev
\`\`\`
4. Open [http://localhost:3000](http://localhost:3000) in your browser.
## Testing
Run E2E tests:
\`\`\`bash
npm run test:e2e
\`\`\`
Run unit tests:
\`\`\`bash
npm run test
\`\`\`
## Flask Backend (Optional)
Navigate to the backend directory and follow the setup instructions.
`)
const blob = await zip.generateAsync({ type: 'blob' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${nextjsConfig.appName}.zip`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
toast.success('Project downloaded successfully!')
} catch (error) {
console.error('Failed to create ZIP:', error)
toast.error('Failed to create ZIP file')
}
}
return {
generatedCode,
exportDialogOpen,
setExportDialogOpen,
handleExportProject,
handleDownloadZip,
}
}

View File

@@ -0,0 +1,37 @@
import { Project } from '@/types/project'
export function useProjectLoader(
setFiles: (updater: any) => void,
setModels: (updater: any) => void,
setComponents: (updater: any) => void,
setComponentTrees: (updater: any) => void,
setWorkflows: (updater: any) => void,
setLambdas: (updater: any) => void,
setTheme: (updater: any) => void,
setPlaywrightTests: (updater: any) => void,
setStorybookStories: (updater: any) => void,
setUnitTests: (updater: any) => void,
setFlaskConfig: (updater: any) => void,
setNextjsConfig: (updater: any) => void,
setNpmSettings: (updater: any) => void,
setFeatureToggles: (updater: any) => void
) {
const loadProject = (project: Project) => {
if (project.files) setFiles(project.files)
if (project.models) setModels(project.models)
if (project.components) setComponents(project.components)
if (project.componentTrees) setComponentTrees(project.componentTrees)
if (project.workflows) setWorkflows(project.workflows)
if (project.lambdas) setLambdas(project.lambdas)
if (project.theme) setTheme(project.theme)
if (project.playwrightTests) setPlaywrightTests(project.playwrightTests)
if (project.storybookStories) setStorybookStories(project.storybookStories)
if (project.unitTests) setUnitTests(project.unitTests)
if (project.flaskConfig) setFlaskConfig(project.flaskConfig)
if (project.nextjsConfig) setNextjsConfig(project.nextjsConfig)
if (project.npmSettings) setNpmSettings(project.npmSettings)
if (project.featureToggles) setFeatureToggles(project.featureToggles)
}
return { loadProject }
}

View File

@@ -0,0 +1,214 @@
import { useKV } from '@github/spark/hooks'
import {
ProjectFile,
PrismaModel,
ComponentNode,
ComponentTree,
ThemeConfig,
PlaywrightTest,
StorybookStory,
UnitTest,
FlaskConfig,
NextJsConfig,
NpmSettings,
Workflow,
Lambda,
FeatureToggles
} from '@/types/project'
const DEFAULT_FLASK_CONFIG: FlaskConfig = {
blueprints: [],
corsOrigins: ['http://localhost:3000'],
enableSwagger: true,
port: 5000,
debug: true,
}
const DEFAULT_NEXTJS_CONFIG: NextJsConfig = {
appName: 'my-nextjs-app',
typescript: true,
eslint: true,
tailwind: true,
srcDirectory: true,
appRouter: true,
importAlias: '@/*',
turbopack: false,
}
const DEFAULT_NPM_SETTINGS: NpmSettings = {
packages: [
{ id: '1', name: 'react', version: '^18.2.0', isDev: false },
{ id: '2', name: 'react-dom', version: '^18.2.0', isDev: false },
{ id: '3', name: 'next', version: '^14.0.0', isDev: false },
{ id: '4', name: '@mui/material', version: '^5.14.0', isDev: false },
{ id: '5', name: 'typescript', version: '^5.0.0', isDev: true },
{ id: '6', name: '@types/react', version: '^18.2.0', isDev: true },
],
scripts: {
dev: 'next dev',
build: 'next build',
start: 'next start',
lint: 'next lint',
},
packageManager: 'npm',
}
const DEFAULT_FEATURE_TOGGLES: FeatureToggles = {
codeEditor: true,
models: true,
components: true,
componentTrees: true,
workflows: true,
lambdas: true,
styling: true,
flaskApi: true,
playwright: true,
storybook: true,
unitTests: true,
errorRepair: true,
documentation: true,
sassStyles: true,
faviconDesigner: true,
ideaCloud: true,
}
const DEFAULT_THEME: ThemeConfig = {
variants: [
{
id: 'light',
name: 'Light',
colors: {
primaryColor: '#1976d2',
secondaryColor: '#dc004e',
errorColor: '#f44336',
warningColor: '#ff9800',
successColor: '#4caf50',
background: '#ffffff',
surface: '#f5f5f5',
text: '#000000',
textSecondary: '#666666',
border: '#e0e0e0',
customColors: {},
},
},
{
id: 'dark',
name: 'Dark',
colors: {
primaryColor: '#90caf9',
secondaryColor: '#f48fb1',
errorColor: '#f44336',
warningColor: '#ffa726',
successColor: '#66bb6a',
background: '#121212',
surface: '#1e1e1e',
text: '#ffffff',
textSecondary: '#b0b0b0',
border: '#333333',
customColors: {},
},
},
],
activeVariantId: 'light',
fontFamily: 'Roboto, Arial, sans-serif',
fontSize: { small: 12, medium: 14, large: 20 },
spacing: 8,
borderRadius: 4,
}
const DEFAULT_FILES: ProjectFile[] = [
{
id: 'file-1',
name: 'page.tsx',
path: '/src/app/page.tsx',
content: `'use client'\n\nimport { ThemeProvider } from '@mui/material/styles'\nimport CssBaseline from '@mui/material/CssBaseline'\nimport { theme } from '@/theme'\nimport { Box, Typography, Button } from '@mui/material'\n\nexport default function Home() {\n return (\n <ThemeProvider theme={theme}>\n <CssBaseline />\n <Box sx={{ p: 4 }}>\n <Typography variant="h3" gutterBottom>\n Welcome to Your App\n </Typography>\n <Button variant="contained" color="primary">\n Get Started\n </Button>\n </Box>\n </ThemeProvider>\n )\n}`,
language: 'typescript',
},
{
id: 'file-2',
name: 'layout.tsx',
path: '/src/app/layout.tsx',
content: `export const metadata = {\n title: 'My Next.js App',\n description: 'Generated with CodeForge',\n}\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <html lang="en">\n <body>{children}</body>\n </html>\n )\n}`,
language: 'typescript',
},
]
export function useProjectState() {
const [files, setFiles] = useKV<ProjectFile[]>('project-files', DEFAULT_FILES)
const [models, setModels] = useKV<PrismaModel[]>('project-models', [])
const [components, setComponents] = useKV<ComponentNode[]>('project-components', [])
const [componentTrees, setComponentTrees] = useKV<ComponentTree[]>('project-component-trees', [
{
id: 'default-tree',
name: 'Main App',
description: 'Default component tree',
rootNodes: [],
createdAt: Date.now(),
updatedAt: Date.now(),
},
])
const [workflows, setWorkflows] = useKV<Workflow[]>('project-workflows', [])
const [lambdas, setLambdas] = useKV<Lambda[]>('project-lambdas', [])
const [theme, setTheme] = useKV<ThemeConfig>('project-theme', DEFAULT_THEME)
const [playwrightTests, setPlaywrightTests] = useKV<PlaywrightTest[]>('project-playwright-tests', [])
const [storybookStories, setStorybookStories] = useKV<StorybookStory[]>('project-storybook-stories', [])
const [unitTests, setUnitTests] = useKV<UnitTest[]>('project-unit-tests', [])
const [flaskConfig, setFlaskConfig] = useKV<FlaskConfig>('project-flask-config', DEFAULT_FLASK_CONFIG)
const [nextjsConfig, setNextjsConfig] = useKV<NextJsConfig>('project-nextjs-config', DEFAULT_NEXTJS_CONFIG)
const [npmSettings, setNpmSettings] = useKV<NpmSettings>('project-npm-settings', DEFAULT_NPM_SETTINGS)
const [featureToggles, setFeatureToggles] = useKV<FeatureToggles>('project-feature-toggles', DEFAULT_FEATURE_TOGGLES)
const safeFiles = Array.isArray(files) ? files : []
const safeModels = Array.isArray(models) ? models : []
const safeComponents = Array.isArray(components) ? components : []
const safeComponentTrees = Array.isArray(componentTrees) ? componentTrees : []
const safeWorkflows = Array.isArray(workflows) ? workflows : []
const safeLambdas = Array.isArray(lambdas) ? lambdas : []
const safeTheme = (theme && theme.variants && Array.isArray(theme.variants) && theme.variants.length > 0) ? theme : DEFAULT_THEME
const safePlaywrightTests = Array.isArray(playwrightTests) ? playwrightTests : []
const safeStorybookStories = Array.isArray(storybookStories) ? storybookStories : []
const safeUnitTests = Array.isArray(unitTests) ? unitTests : []
const safeFlaskConfig = flaskConfig || DEFAULT_FLASK_CONFIG
const safeNextjsConfig = nextjsConfig || DEFAULT_NEXTJS_CONFIG
const safeNpmSettings = npmSettings || DEFAULT_NPM_SETTINGS
const safeFeatureToggles = featureToggles || DEFAULT_FEATURE_TOGGLES
return {
files: safeFiles,
setFiles,
models: safeModels,
setModels,
components: safeComponents,
setComponents,
componentTrees: safeComponentTrees,
setComponentTrees,
workflows: safeWorkflows,
setWorkflows,
lambdas: safeLambdas,
setLambdas,
theme: safeTheme,
setTheme,
playwrightTests: safePlaywrightTests,
setPlaywrightTests,
storybookStories: safeStorybookStories,
setStorybookStories,
unitTests: safeUnitTests,
setUnitTests,
flaskConfig: safeFlaskConfig,
setFlaskConfig,
nextjsConfig: safeNextjsConfig,
setNextjsConfig,
npmSettings: safeNpmSettings,
setNpmSettings,
featureToggles: safeFeatureToggles,
setFeatureToggles,
defaults: {
DEFAULT_FLASK_CONFIG,
DEFAULT_NEXTJS_CONFIG,
DEFAULT_NPM_SETTINGS,
DEFAULT_FEATURE_TOGGLES,
DEFAULT_THEME,
DEFAULT_FILES,
}
}
}

View File

@@ -0,0 +1,18 @@
import { useState, useEffect } from 'react'
export function useTabNavigation(defaultTab: string) {
const [activeTab, setActiveTab] = useState(defaultTab)
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const shortcut = params.get('shortcut')
if (shortcut) {
setActiveTab(shortcut)
}
}, [])
return {
activeTab,
setActiveTab,
}
}