mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Generated by Spark: Save / load project. Maybe it could save it in database.
This commit is contained in:
11
PRD.md
11
PRD.md
@@ -1,6 +1,6 @@
|
||||
# Planning Guide
|
||||
|
||||
A visual low-code platform for generating Next.js applications with Material UI styling, integrated Monaco code editor, and Prisma schema designer.
|
||||
A visual low-code platform for generating Next.js applications with Material UI styling, integrated Monaco code editor, Prisma schema designer, and persistent project management.
|
||||
|
||||
**Experience Qualities**:
|
||||
1. **Empowering** - Users feel in control with both visual and code-level editing capabilities
|
||||
@@ -8,10 +8,17 @@ A visual low-code platform for generating Next.js applications with Material UI
|
||||
3. **Professional** - Output-ready code that follows modern best practices and conventions
|
||||
|
||||
**Complexity Level**: Complex Application (advanced functionality, likely with multiple views)
|
||||
This is a full-featured low-code IDE with multiple integrated tools (code editor, visual designers, schema builder), state management across views, and code generation capabilities that require sophisticated UI organization.
|
||||
This is a full-featured low-code IDE with multiple integrated tools (code editor, visual designers, schema builder), state management across views, persistent project storage, and code generation capabilities that require sophisticated UI organization.
|
||||
|
||||
## Essential Features
|
||||
|
||||
### Project Save/Load Management
|
||||
- **Functionality**: Complete project persistence system using Spark KV database with save, load, duplicate, export, import, and delete operations
|
||||
- **Purpose**: Allow users to work on multiple projects over time without losing progress, share projects via JSON export, and maintain a library of saved work
|
||||
- **Trigger**: Save/Load/New Project buttons in the header toolbar
|
||||
- **Progression**: Click Save → Enter project name and description → Project saved to database → View saved projects list → Load any project → All state restored including files, models, components, trees, workflows, lambdas, themes, tests, and settings
|
||||
- **Success criteria**: Projects persist between sessions; all application state is saved and restored correctly; can duplicate, export (JSON), import, and delete projects; project list shows metadata (name, description, dates); smooth loading experience with no data loss
|
||||
|
||||
### Monaco Code Editor Integration
|
||||
- **Functionality**: Full-featured code editor with syntax highlighting, autocomplete, multi-file editing, and AI-powered code improvement and explanation
|
||||
- **Purpose**: Allows direct code manipulation for users who want precise control, with AI assistance for learning and optimization
|
||||
|
||||
58
README.md
58
README.md
@@ -1,24 +1,30 @@
|
||||
# 🔨 CodeForge - Low-Code Next.js App Builder
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
A comprehensive visual low-code platform for generating production-ready Next.js applications with Material UI, Prisma, Flask backends, and comprehensive testing suites. Built with AI-powered code generation at its core.
|
||||
A comprehensive visual low-code platform for generating production-ready Next.js applications with Material UI, Prisma, Flask backends, comprehensive testing suites, and persistent project management. Built with AI-powered code generation at its core.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🎯 Core Capabilities
|
||||
- **Project Management** - Save, load, duplicate, export, and import complete projects with full state persistence
|
||||
- **Project Dashboard** - At-a-glance overview of project status, completion metrics, and quick tips
|
||||
- **Monaco Code Editor** - Full-featured IDE with syntax highlighting, autocomplete, and multi-file editing
|
||||
- **Prisma Schema Designer** - Visual database model builder with relations and field configuration
|
||||
- **Component Tree Builder** - Hierarchical React component designer with Material UI integration
|
||||
- **Component Tree Manager** - Manage multiple named component trees for different app sections
|
||||
- **Workflow Designer** - n8n-style visual workflow builder with triggers, actions, conditions, and lambdas
|
||||
- **Lambda Designer** - Serverless function editor with multi-runtime support and trigger configuration
|
||||
- **Theme Designer** - Advanced theming with multiple variants (light/dark/custom) and unlimited custom colors
|
||||
- **Sass Styling System** - Custom Material UI components with Sass, including utilities, mixins, and animations
|
||||
- **Flask Backend Designer** - Python REST API designer with blueprints, endpoints, and CORS configuration
|
||||
- **Project Settings** - Configure Next.js options, npm packages, scripts, and build settings
|
||||
- **CI/CD Integration** - Generate workflow files for GitHub Actions, GitLab CI, Jenkins, and CircleCI
|
||||
- **Feature Toggles** - Customize your workspace by enabling/disabling designer features
|
||||
- **Keyboard Shortcuts** - Power-user shortcuts for rapid navigation and actions
|
||||
|
||||
### 🤖 AI-Powered Generation
|
||||
@@ -41,19 +47,33 @@ A comprehensive visual low-code platform for generating production-ready Next.js
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Quick Start
|
||||
1. Open the **Documentation** tab in the app for comprehensive guides
|
||||
2. Use **AI Generate** to scaffold a complete application from a description
|
||||
3. Navigate between tabs to design models, components, themes, and backend APIs
|
||||
4. Click **Export Project** to download your complete Next.js application
|
||||
1. **Save Your Work** - Use **Save Project** button to persist your work to the database
|
||||
2. **Load Projects** - Click **Load Project** to view and switch between saved projects
|
||||
3. Open the **Documentation** tab in the app for comprehensive guides
|
||||
4. Use **AI Generate** to scaffold a complete application from a description
|
||||
5. Navigate between tabs to design models, components, themes, and backend APIs
|
||||
6. Click **Export Project** to download your complete Next.js application
|
||||
|
||||
### Project Management
|
||||
- **Save Project** - Save current work with name and description to database
|
||||
- **Load Project** - Browse and load any saved project
|
||||
- **New Project** - Start fresh with a blank workspace
|
||||
- **Duplicate** - Create a copy of any saved project
|
||||
- **Export** - Download project as JSON file for backup or sharing
|
||||
- **Import** - Load a project from an exported JSON file
|
||||
- **Delete** - Remove projects from database
|
||||
|
||||
### Manual Building
|
||||
1. **Models Tab** - Create your database schema with Prisma models
|
||||
2. **Components Tab** - Build your UI component hierarchy
|
||||
3. **Styling Tab** - Design your theme with custom colors and typography
|
||||
4. **Flask API Tab** - Configure your backend REST API
|
||||
5. **Settings Tab** - Configure Next.js and npm packages
|
||||
6. **Code Editor Tab** - Fine-tune generated code directly
|
||||
7. **Export** - Download your complete, production-ready application
|
||||
3. **Component Trees Tab** - Organize components into named trees
|
||||
4. **Workflows Tab** - Design automation workflows visually
|
||||
5. **Lambdas Tab** - Create serverless functions
|
||||
6. **Styling Tab** - Design your theme with custom colors and typography
|
||||
7. **Flask API Tab** - Configure your backend REST API
|
||||
8. **Settings Tab** - Configure Next.js and npm packages
|
||||
9. **Code Editor Tab** - Fine-tune generated code directly
|
||||
10. **Export** - Download your complete, production-ready application
|
||||
|
||||
## 📋 Technology Stack
|
||||
|
||||
@@ -92,10 +112,14 @@ Access documentation by clicking the **Documentation** tab in the application.
|
||||
|
||||
## 🗺️ Roadmap
|
||||
|
||||
### ✅ Completed (v1.0 - v4.1)
|
||||
### ✅ Completed (v1.0 - v5.2)
|
||||
- Project persistence with save/load functionality
|
||||
- Project dashboard with completion metrics
|
||||
- Monaco code editor integration
|
||||
- Visual designers for models, components, and themes
|
||||
- Multiple component trees management
|
||||
- n8n-style workflow designer
|
||||
- Lambda function designer with multi-runtime support
|
||||
- AI-powered generation across all features
|
||||
- Multi-theme variant support
|
||||
- Testing suite designers (Playwright, Storybook, Unit Tests)
|
||||
@@ -105,15 +129,11 @@ Access documentation by clicking the **Documentation** tab in the application.
|
||||
- Custom Sass styling system with utilities and mixins
|
||||
- ZIP file export with README generation
|
||||
- Keyboard shortcuts for power users
|
||||
|
||||
### ✅ Recently Added (v4.2)
|
||||
- Complete CI/CD configurations (GitHub Actions, GitLab CI, Jenkins, CircleCI)
|
||||
- Docker containerization with multi-stage builds
|
||||
- Nginx configuration for production deployment
|
||||
- Automated release workflow with versioning
|
||||
- Security scanning integration (Trivy, npm audit)
|
||||
- Slack notification integration
|
||||
- Health check endpoints
|
||||
- Feature toggle system for customizable workspace
|
||||
- Project export/import as JSON
|
||||
- Project duplication and deletion
|
||||
|
||||
### 🔮 Planned
|
||||
- Real-time preview with hot reload
|
||||
|
||||
48
ROADMAP.md
48
ROADMAP.md
@@ -111,9 +111,53 @@ Improved export and comprehensive documentation:
|
||||
- ✅ Keyboard shortcuts for power users
|
||||
- ✅ Search functionality in documentation
|
||||
|
||||
### v5.0 - Workflows, Lambdas & Feature Toggles (Completed)
|
||||
**Release Date:** Week 10
|
||||
|
||||
Advanced automation and customization:
|
||||
- ✅ n8n-style workflow designer with visual node editor
|
||||
- ✅ Workflow nodes: triggers, actions, conditions, transforms, lambdas, API calls, database queries
|
||||
- ✅ Visual workflow connections and data flow
|
||||
- ✅ Lambda function designer with Monaco editor
|
||||
- ✅ Multi-runtime lambda support (JavaScript, TypeScript, Python)
|
||||
- ✅ Lambda triggers (HTTP, schedule, event, queue)
|
||||
- ✅ Environment variable management for lambdas
|
||||
- ✅ Multiple Component Trees management system
|
||||
- ✅ Feature toggle system to enable/disable designers
|
||||
- ✅ Customizable workspace based on user needs
|
||||
|
||||
### v5.1 - CI/CD Integration (Completed)
|
||||
**Release Date:** Week 11
|
||||
|
||||
Comprehensive DevOps pipeline configuration:
|
||||
- ✅ GitHub Actions workflow generator
|
||||
- ✅ GitLab CI/CD pipeline configuration
|
||||
- ✅ Jenkins pipeline (Jenkinsfile) generation
|
||||
- ✅ CircleCI configuration
|
||||
- ✅ Multi-stage builds and deployments
|
||||
- ✅ Environment-specific configurations
|
||||
- ✅ Automated testing in pipelines
|
||||
- ✅ Docker integration in CI/CD
|
||||
- ✅ Deployment strategies configuration
|
||||
|
||||
### v5.2 - Project Persistence (Completed)
|
||||
**Release Date:** Week 12
|
||||
|
||||
Complete project management system:
|
||||
- ✅ Save projects to Spark KV database
|
||||
- ✅ Load projects from database
|
||||
- ✅ Project listing with metadata (name, description, timestamps)
|
||||
- ✅ Duplicate existing projects
|
||||
- ✅ Delete projects from database
|
||||
- ✅ Export projects as JSON files
|
||||
- ✅ Import projects from JSON
|
||||
- ✅ New project creation with state reset
|
||||
- ✅ Current project indicator
|
||||
- ✅ Complete state persistence (files, models, components, trees, workflows, lambdas, themes, tests, settings)
|
||||
|
||||
## Upcoming Releases
|
||||
|
||||
### v4.2 - Real-Time Preview (In Planning)
|
||||
### v5.3 - Real-Time Preview (In Planning)
|
||||
**Estimated:** Q2 2024
|
||||
|
||||
Live application preview:
|
||||
@@ -164,7 +208,7 @@ Visual form design:
|
||||
- Zod schema validation
|
||||
- Material UI form components
|
||||
|
||||
### v5.0 - Authentication & Security (In Planning)
|
||||
### v4.5 - Authentication & Security (In Planning)
|
||||
**Estimated:** Q3 2024
|
||||
|
||||
Complete authentication system:
|
||||
|
||||
44
src/App.tsx
44
src/App.tsx
@@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
||||
import { Code, Database, Tree, PaintBrush, Download, Sparkle, Flask, BookOpen, Play, Wrench, Gear, Cube, FileText, ChartBar, Keyboard, FlowArrow, Faders } from '@phosphor-icons/react'
|
||||
import { ProjectFile, PrismaModel, ComponentNode, ComponentTree, ThemeConfig, PlaywrightTest, StorybookStory, UnitTest, FlaskConfig, NextJsConfig, NpmSettings, Workflow, Lambda, FeatureToggles } from '@/types/project'
|
||||
import { ProjectFile, PrismaModel, ComponentNode, ComponentTree, ThemeConfig, PlaywrightTest, StorybookStory, UnitTest, FlaskConfig, NextJsConfig, NpmSettings, Workflow, Lambda, FeatureToggles, Project } from '@/types/project'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { ModelDesigner } from '@/components/ModelDesigner'
|
||||
import { ComponentTreeBuilder } from '@/components/ComponentTreeBuilder'
|
||||
@@ -27,6 +27,7 @@ import { SassStylesShowcase } from '@/components/SassStylesShowcase'
|
||||
import { ProjectDashboard } from '@/components/ProjectDashboard'
|
||||
import { KeyboardShortcutsDialog } from '@/components/KeyboardShortcutsDialog'
|
||||
import { FeatureToggleSettings } from '@/components/FeatureToggleSettings'
|
||||
import { ProjectManager } from '@/components/ProjectManager'
|
||||
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts'
|
||||
import { generateNextJSProject, generatePrismaSchema, generateMUITheme, generatePlaywrightTests, generateStorybookStories, generateUnitTests, generateFlaskApp } from '@/lib/generators'
|
||||
import { AIService } from '@/lib/ai-service'
|
||||
@@ -454,6 +455,43 @@ Navigate to the backend directory and follow the setup instructions.
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoadProject = (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)
|
||||
}
|
||||
|
||||
const getCurrentProject = (): Project => {
|
||||
return {
|
||||
name: safeNextjsConfig.appName,
|
||||
files: safeFiles,
|
||||
models: safeModels,
|
||||
components: safeComponents,
|
||||
componentTrees: safeComponentTrees,
|
||||
workflows: safeWorkflows,
|
||||
lambdas: safeLambdas,
|
||||
theme: safeTheme,
|
||||
playwrightTests: safePlaywrightTests,
|
||||
storybookStories: safeStorybookStories,
|
||||
unitTests: safeUnitTests,
|
||||
flaskConfig: safeFlaskConfig,
|
||||
nextjsConfig: safeNextjsConfig,
|
||||
npmSettings: safeNpmSettings,
|
||||
featureToggles: safeFeatureToggles,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-background text-foreground">
|
||||
<header className="border-b border-border bg-card px-6 py-4">
|
||||
@@ -470,6 +508,10 @@ Navigate to the backend directory and follow the setup instructions.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<ProjectManager
|
||||
currentProject={getCurrentProject()}
|
||||
onProjectLoad={handleLoadProject}
|
||||
/>
|
||||
{safeFeatureToggles.errorRepair && autoDetectedErrors.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
417
src/components/ProjectManager.tsx
Normal file
417
src/components/ProjectManager.tsx
Normal file
@@ -0,0 +1,417 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
|
||||
import { FloppyDisk, FolderOpen, Trash, Copy, DownloadSimple, UploadSimple, Plus, FolderPlus } from '@phosphor-icons/react'
|
||||
import { ProjectService, SavedProject } from '@/lib/project-service'
|
||||
import { Project } from '@/types/project'
|
||||
import { toast } from 'sonner'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface ProjectManagerProps {
|
||||
currentProject: Project
|
||||
onProjectLoad: (project: Project) => void
|
||||
}
|
||||
|
||||
export function ProjectManager({ currentProject, onProjectLoad }: ProjectManagerProps) {
|
||||
const [projects, setProjects] = useState<SavedProject[]>([])
|
||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false)
|
||||
const [loadDialogOpen, setLoadDialogOpen] = useState(false)
|
||||
const [newProjectDialogOpen, setNewProjectDialogOpen] = useState(false)
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [importDialogOpen, setImportDialogOpen] = useState(false)
|
||||
const [projectToDelete, setProjectToDelete] = useState<string | null>(null)
|
||||
const [projectName, setProjectName] = useState('')
|
||||
const [projectDescription, setProjectDescription] = useState('')
|
||||
const [currentProjectId, setCurrentProjectId] = useState<string | null>(null)
|
||||
const [importJson, setImportJson] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadProjectsList()
|
||||
}, [])
|
||||
|
||||
const loadProjectsList = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const list = await ProjectService.listProjects()
|
||||
setProjects(list)
|
||||
} catch (error) {
|
||||
console.error('Failed to load projects:', error)
|
||||
toast.error('Failed to load projects list')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveProject = async () => {
|
||||
if (!projectName.trim()) {
|
||||
toast.error('Please enter a project name')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const id = currentProjectId || ProjectService.generateProjectId()
|
||||
|
||||
await ProjectService.saveProject(
|
||||
id,
|
||||
projectName,
|
||||
currentProject,
|
||||
projectDescription
|
||||
)
|
||||
|
||||
setCurrentProjectId(id)
|
||||
toast.success('Project saved successfully!')
|
||||
setSaveDialogOpen(false)
|
||||
await loadProjectsList()
|
||||
} catch (error) {
|
||||
console.error('Failed to save project:', error)
|
||||
toast.error('Failed to save project')
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoadProject = async (project: SavedProject) => {
|
||||
try {
|
||||
onProjectLoad(project.data)
|
||||
setCurrentProjectId(project.id)
|
||||
setProjectName(project.name)
|
||||
setProjectDescription(project.description || '')
|
||||
setLoadDialogOpen(false)
|
||||
toast.success(`Loaded project: ${project.name}`)
|
||||
} catch (error) {
|
||||
console.error('Failed to load project:', error)
|
||||
toast.error('Failed to load project')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteProject = async () => {
|
||||
if (!projectToDelete) return
|
||||
|
||||
try {
|
||||
await ProjectService.deleteProject(projectToDelete)
|
||||
toast.success('Project deleted successfully')
|
||||
setDeleteDialogOpen(false)
|
||||
setProjectToDelete(null)
|
||||
|
||||
if (currentProjectId === projectToDelete) {
|
||||
setCurrentProjectId(null)
|
||||
setProjectName('')
|
||||
setProjectDescription('')
|
||||
}
|
||||
|
||||
await loadProjectsList()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete project:', error)
|
||||
toast.error('Failed to delete project')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDuplicateProject = async (id: string, name: string) => {
|
||||
try {
|
||||
const duplicated = await ProjectService.duplicateProject(id, `${name} (Copy)`)
|
||||
if (duplicated) {
|
||||
toast.success('Project duplicated successfully')
|
||||
await loadProjectsList()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to duplicate project:', error)
|
||||
toast.error('Failed to duplicate project')
|
||||
}
|
||||
}
|
||||
|
||||
const handleExportProject = async (id: string, name: string) => {
|
||||
try {
|
||||
const json = await ProjectService.exportProjectAsJSON(id)
|
||||
if (json) {
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
toast.success('Project exported successfully')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to export project:', error)
|
||||
toast.error('Failed to export project')
|
||||
}
|
||||
}
|
||||
|
||||
const handleImportProject = async () => {
|
||||
if (!importJson.trim()) {
|
||||
toast.error('Please paste project JSON')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const imported = await ProjectService.importProjectFromJSON(importJson)
|
||||
if (imported) {
|
||||
toast.success('Project imported successfully')
|
||||
setImportDialogOpen(false)
|
||||
setImportJson('')
|
||||
await loadProjectsList()
|
||||
} else {
|
||||
toast.error('Invalid project JSON')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to import project:', error)
|
||||
toast.error('Failed to import project')
|
||||
}
|
||||
}
|
||||
|
||||
const handleNewProject = () => {
|
||||
setCurrentProjectId(null)
|
||||
setProjectName('')
|
||||
setProjectDescription('')
|
||||
setNewProjectDialogOpen(false)
|
||||
toast.success('New project started')
|
||||
}
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleString()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => setSaveDialogOpen(true)} variant="outline">
|
||||
<FloppyDisk size={16} className="mr-2" />
|
||||
Save Project
|
||||
</Button>
|
||||
<Button onClick={() => setLoadDialogOpen(true)} variant="outline">
|
||||
<FolderOpen size={16} className="mr-2" />
|
||||
Load Project
|
||||
</Button>
|
||||
<Button onClick={() => setNewProjectDialogOpen(true)} variant="outline">
|
||||
<FolderPlus size={16} className="mr-2" />
|
||||
New Project
|
||||
</Button>
|
||||
<Button onClick={() => setImportDialogOpen(true)} variant="outline">
|
||||
<UploadSimple size={16} className="mr-2" />
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Save Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Save your current project to the database
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="project-name">Project Name</Label>
|
||||
<Input
|
||||
id="project-name"
|
||||
value={projectName}
|
||||
onChange={(e) => setProjectName(e.target.value)}
|
||||
placeholder="My Awesome Project"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="project-description">Description (Optional)</Label>
|
||||
<Textarea
|
||||
id="project-description"
|
||||
value={projectDescription}
|
||||
onChange={(e) => setProjectDescription(e.target.value)}
|
||||
placeholder="Brief description of your project..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
{currentProjectId && (
|
||||
<Badge variant="secondary">
|
||||
This will update the existing project
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setSaveDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveProject}>
|
||||
<FloppyDisk size={16} className="mr-2" />
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={loadDialogOpen} onOpenChange={setLoadDialogOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Load Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select a project to load from the database
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-96">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<p className="text-muted-foreground">Loading projects...</p>
|
||||
</div>
|
||||
) : projects.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<FolderOpen size={48} className="text-muted-foreground mb-4" />
|
||||
<p className="text-muted-foreground">No saved projects</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{projects.map((project) => (
|
||||
<Card
|
||||
key={project.id}
|
||||
className={cn(
|
||||
'cursor-pointer hover:bg-accent transition-colors',
|
||||
currentProjectId === project.id && 'border-primary'
|
||||
)}
|
||||
>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-base">{project.name}</CardTitle>
|
||||
{project.description && (
|
||||
<CardDescription className="mt-1">
|
||||
{project.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
</div>
|
||||
{currentProjectId === project.id && (
|
||||
<Badge variant="default">Current</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-3">
|
||||
<div className="flex gap-2 text-xs text-muted-foreground">
|
||||
<span>Updated: {formatDate(project.updatedAt)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="pt-0 flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleLoadProject(project)}
|
||||
>
|
||||
<FolderOpen size={14} className="mr-1" />
|
||||
Load
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDuplicateProject(project.id, project.name)
|
||||
}}
|
||||
>
|
||||
<Copy size={14} className="mr-1" />
|
||||
Duplicate
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleExportProject(project.id, project.name)
|
||||
}}
|
||||
>
|
||||
<DownloadSimple size={14} className="mr-1" />
|
||||
Export
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setProjectToDelete(project.id)
|
||||
setDeleteDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Trash size={14} className="mr-1" />
|
||||
Delete
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog open={newProjectDialogOpen} onOpenChange={setNewProjectDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Start New Project?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will clear your current workspace. Make sure you've saved your current project if you want to keep it.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleNewProject}>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Start New Project
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Project?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the project from the database.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => setProjectToDelete(null)}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteProject} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
<Trash size={16} className="mr-2" />
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<Dialog open={importDialogOpen} onOpenChange={setImportDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Paste the JSON content of an exported project
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<Textarea
|
||||
value={importJson}
|
||||
onChange={(e) => setImportJson(e.target.value)}
|
||||
placeholder="Paste project JSON here..."
|
||||
rows={12}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setImportDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleImportProject}>
|
||||
<UploadSimple size={16} className="mr-2" />
|
||||
Import
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
139
src/lib/project-service.ts
Normal file
139
src/lib/project-service.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Project } from '@/types/project'
|
||||
|
||||
export interface SavedProject {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
data: Project
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
version: string
|
||||
}
|
||||
|
||||
const PROJECT_VERSION = '1.0.0'
|
||||
|
||||
export class ProjectService {
|
||||
private static readonly PROJECTS_LIST_KEY = 'codeforge-projects-list'
|
||||
private static readonly PROJECT_PREFIX = 'codeforge-project-'
|
||||
|
||||
static async listProjects(): Promise<SavedProject[]> {
|
||||
try {
|
||||
const projectIds = await window.spark.kv.get<string[]>(this.PROJECTS_LIST_KEY)
|
||||
if (!projectIds || projectIds.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const projects: SavedProject[] = []
|
||||
for (const id of projectIds) {
|
||||
const project = await this.loadProject(id)
|
||||
if (project) {
|
||||
projects.push(project)
|
||||
}
|
||||
}
|
||||
|
||||
return projects.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
} catch (error) {
|
||||
console.error('Failed to list projects:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
static async saveProject(
|
||||
id: string,
|
||||
name: string,
|
||||
projectData: Project,
|
||||
description?: string
|
||||
): Promise<SavedProject> {
|
||||
const now = Date.now()
|
||||
|
||||
const existingProject = await this.loadProject(id)
|
||||
|
||||
const savedProject: SavedProject = {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
data: projectData,
|
||||
createdAt: existingProject?.createdAt || now,
|
||||
updatedAt: now,
|
||||
version: PROJECT_VERSION,
|
||||
}
|
||||
|
||||
await window.spark.kv.set(`${this.PROJECT_PREFIX}${id}`, savedProject)
|
||||
|
||||
const projectIds = (await window.spark.kv.get<string[]>(this.PROJECTS_LIST_KEY)) || []
|
||||
if (!projectIds.includes(id)) {
|
||||
projectIds.push(id)
|
||||
await window.spark.kv.set(this.PROJECTS_LIST_KEY, projectIds)
|
||||
}
|
||||
|
||||
return savedProject
|
||||
}
|
||||
|
||||
static async loadProject(id: string): Promise<SavedProject | null> {
|
||||
try {
|
||||
const project = await window.spark.kv.get<SavedProject>(`${this.PROJECT_PREFIX}${id}`)
|
||||
return project || null
|
||||
} catch (error) {
|
||||
console.error(`Failed to load project ${id}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteProject(id: string): Promise<void> {
|
||||
await window.spark.kv.delete(`${this.PROJECT_PREFIX}${id}`)
|
||||
|
||||
const projectIds = (await window.spark.kv.get<string[]>(this.PROJECTS_LIST_KEY)) || []
|
||||
const updatedIds = projectIds.filter((pid) => pid !== id)
|
||||
await window.spark.kv.set(this.PROJECTS_LIST_KEY, updatedIds)
|
||||
}
|
||||
|
||||
static async duplicateProject(id: string, newName: string): Promise<SavedProject | null> {
|
||||
const project = await this.loadProject(id)
|
||||
if (!project) {
|
||||
return null
|
||||
}
|
||||
|
||||
const newId = `project-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
const duplicated = await this.saveProject(
|
||||
newId,
|
||||
newName,
|
||||
project.data,
|
||||
project.description
|
||||
)
|
||||
|
||||
return duplicated
|
||||
}
|
||||
|
||||
static async exportProjectAsJSON(id: string): Promise<string | null> {
|
||||
const project = await this.loadProject(id)
|
||||
if (!project) {
|
||||
return null
|
||||
}
|
||||
|
||||
return JSON.stringify(project, null, 2)
|
||||
}
|
||||
|
||||
static async importProjectFromJSON(jsonString: string): Promise<SavedProject | null> {
|
||||
try {
|
||||
const imported = JSON.parse(jsonString) as SavedProject
|
||||
|
||||
const newId = `project-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
const project = await this.saveProject(
|
||||
newId,
|
||||
`${imported.name} (Imported)`,
|
||||
imported.data,
|
||||
imported.description
|
||||
)
|
||||
|
||||
return project
|
||||
} catch (error) {
|
||||
console.error('Failed to import project:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
static generateProjectId(): string {
|
||||
return `project-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user