diff --git a/README.md b/README.md index 6745922..a362226 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A comprehensive visual low-code platform for generating production-ready Next.js ## ✨ Features ### 🎯 Core Capabilities +- **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 @@ -18,6 +19,7 @@ A comprehensive visual low-code platform for generating production-ready Next.js - **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 +- **Keyboard Shortcuts** - Power-user shortcuts for rapid navigation and actions ### 🤖 AI-Powered Generation - **Complete App Generation** - Describe your app and get a full project structure @@ -89,7 +91,8 @@ Access documentation by clicking the **Documentation** tab in the application. ## 🗺️ Roadmap -### ✅ Completed (v1.0 - v4.0) +### ✅ Completed (v1.0 - v4.1) +- Project dashboard with completion metrics - Monaco code editor integration - Visual designers for models, components, and themes - AI-powered generation across all features @@ -99,6 +102,8 @@ Access documentation by clicking the **Documentation** tab in the application. - Flask backend designer - Project settings and npm management - Custom Sass styling system with utilities and mixins +- ZIP file export with README generation +- Keyboard shortcuts for power users ### 🔮 Planned - Real-time preview with hot reload diff --git a/ROADMAP.md b/ROADMAP.md index b515039..c94c9ad 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -96,9 +96,24 @@ Backend and configuration tools: - ✅ Package manager selection (npm/yarn/pnpm) - ✅ Complete project settings control +### v4.1 - Enhanced Export & Documentation (Completed) +**Release Date:** Week 9 + +Improved export and comprehensive documentation: +- ✅ ZIP file download for complete project export +- ✅ Auto-generated README in exported projects +- ✅ Copy all files to clipboard functionality +- ✅ Comprehensive in-app documentation system +- ✅ Sass styles guide with live examples +- ✅ Complete roadmap documentation +- ✅ AI agents architecture documentation +- ✅ Project dashboard with completion metrics +- ✅ Keyboard shortcuts for power users +- ✅ Search functionality in documentation + ## Upcoming Releases -### v4.1 - Real-Time Preview (In Planning) +### v4.2 - Real-Time Preview (In Planning) **Estimated:** Q2 2024 Live application preview: @@ -115,7 +130,7 @@ Live application preview: - State preservation across reloads - Error boundary implementation -### v4.2 - Data Management (In Planning) +### v4.3 - Data Management (In Planning) **Estimated:** Q2 2024 Database and API integration: @@ -132,7 +147,7 @@ Database and API integration: - TypeScript API client with fetch/axios - Automatic type inference from endpoints -### v4.3 - Form Builder (In Planning) +### v4.4 - Form Builder (In Planning) **Estimated:** Q2-Q3 2024 Visual form design: diff --git a/package-lock.json b/package-lock.json index 4d7c58d..4be0384 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "embla-carousel-react": "^8.5.2", "framer-motion": "^12.6.2", "input-otp": "^1.4.2", + "jszip": "^3.10.1", "lucide-react": "^0.484.0", "marked": "^15.0.7", "next-themes": "^0.4.6", @@ -5692,6 +5693,12 @@ "node": ">=6.6.0" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7550,6 +7557,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immutable": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", @@ -7732,6 +7745,12 @@ "@types/estree": "*" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7829,6 +7848,18 @@ "dev": true, "license": "MIT" }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7853,6 +7884,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", @@ -8543,6 +8583,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8752,6 +8798,12 @@ "license": "MIT", "peer": true }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -9058,6 +9110,27 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -9447,6 +9520,12 @@ "node": ">= 18" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -9644,6 +9723,21 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -10153,6 +10247,12 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/package.json b/package.json index c4fea5c..1a7aeb7 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "embla-carousel-react": "^8.5.2", "framer-motion": "^12.6.2", "input-otp": "^1.4.2", + "jszip": "^3.10.1", "lucide-react": "^0.484.0", "marked": "^15.0.7", "next-themes": "^0.4.6", diff --git a/src/App.tsx b/src/App.tsx index f1b4b07..55ccb63 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button' 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 } from '@phosphor-icons/react' +import { Code, Database, Tree, PaintBrush, Download, Sparkle, Flask, BookOpen, Play, Wrench, Gear, Cube, FileText, ChartBar, Keyboard } from '@phosphor-icons/react' import { ProjectFile, PrismaModel, ComponentNode, ThemeConfig, PlaywrightTest, StorybookStory, UnitTest, FlaskConfig, NextJsConfig, NpmSettings } from '@/types/project' import { CodeEditor } from '@/components/CodeEditor' import { ModelDesigner } from '@/components/ModelDesigner' @@ -21,9 +21,13 @@ import { ProjectSettingsDesigner } from '@/components/ProjectSettingsDesigner' import { ErrorPanel } from '@/components/ErrorPanel' import { DocumentationView } from '@/components/DocumentationView' import { SassStylesShowcase } from '@/components/SassStylesShowcase' +import { ProjectDashboard } from '@/components/ProjectDashboard' +import { KeyboardShortcutsDialog } from '@/components/KeyboardShortcutsDialog' +import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts' import { generateNextJSProject, generatePrismaSchema, generateMUITheme, generatePlaywrightTests, generateStorybookStories, generateUnitTests, generateFlaskApp } from '@/lib/generators' import { AIService } from '@/lib/ai-service' import { toast } from 'sonner' +import JSZip from 'jszip' import { Dialog, DialogContent, @@ -144,8 +148,9 @@ function App() { const [nextjsConfig, setNextjsConfig] = useKV('project-nextjs-config', DEFAULT_NEXTJS_CONFIG) const [npmSettings, setNpmSettings] = useKV('project-npm-settings', DEFAULT_NPM_SETTINGS) const [activeFileId, setActiveFileId] = useState((files || [])[0]?.id || null) - const [activeTab, setActiveTab] = useState('code') + const [activeTab, setActiveTab] = useState('dashboard') const [exportDialogOpen, setExportDialogOpen] = useState(false) + const [shortcutsDialogOpen, setShortcutsDialogOpen] = useState(false) const [generatedCode, setGeneratedCode] = useState>({}) const safeFiles = files || [] @@ -161,6 +166,57 @@ function App() { const { errors: autoDetectedErrors } = useAutoRepair(safeFiles, false) + useKeyboardShortcuts([ + { + key: '1', + ctrl: true, + description: 'Go to Dashboard', + action: () => setActiveTab('dashboard'), + }, + { + key: '2', + ctrl: true, + description: 'Go to Code Editor', + action: () => setActiveTab('code'), + }, + { + key: '3', + ctrl: true, + description: 'Go to Models', + action: () => setActiveTab('models'), + }, + { + key: '4', + ctrl: true, + description: 'Go to Components', + action: () => setActiveTab('components'), + }, + { + key: '5', + ctrl: true, + description: 'Go to Styling', + action: () => setActiveTab('styling'), + }, + { + key: 'e', + ctrl: true, + description: 'Export Project', + action: () => handleExportProject(), + }, + { + key: 'k', + ctrl: true, + description: 'AI Generate', + action: () => handleGenerateWithAI(), + }, + { + key: '/', + ctrl: true, + description: 'Show Keyboard Shortcuts', + action: () => setShortcutsDialogOpen(true), + }, + ]) + const handleFileChange = (fileId: string, content: string) => { setFiles((currentFiles) => (currentFiles || []).map((f) => (f.id === fileId ? { ...f, content } : f)) @@ -232,6 +288,75 @@ function App() { 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', `# ${safeNextjsConfig.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 = `${safeNextjsConfig.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') + } + } + const handleGenerateWithAI = async () => { const description = prompt('Describe the application you want to generate:') if (!description) return @@ -287,6 +412,14 @@ function App() { {autoDetectedErrors.length} {autoDetectedErrors.length === 1 ? 'Error' : 'Errors'} )} + + +
{Object.entries(generatedCode).map(([path, content]) => ( @@ -478,6 +646,11 @@ function App() { + +
) } diff --git a/src/components/DocumentationView.tsx b/src/components/DocumentationView.tsx index 14cd82f..81adcc8 100644 --- a/src/components/DocumentationView.tsx +++ b/src/components/DocumentationView.tsx @@ -4,6 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { ScrollArea } from '@/components/ui/scroll-area' import { Badge } from '@/components/ui/badge' import { Separator } from '@/components/ui/separator' +import { Input } from '@/components/ui/input' import { BookOpen, MapPin, @@ -23,34 +24,47 @@ import { Package, Rocket, Target, - Lightbulb + Lightbulb, + MagnifyingGlass } from '@phosphor-icons/react' export function DocumentationView() { const [activeTab, setActiveTab] = useState('readme') + const [searchQuery, setSearchQuery] = useState('') return (
-
- - - - README - - - - Roadmap - - - - Agents Files - - - - Sass Styles Guide - - +
+
+ + + + README + + + + Roadmap + + + + Agents Files + + + + Sass Styles Guide + + +
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
diff --git a/src/components/KeyboardShortcutsDialog.tsx b/src/components/KeyboardShortcutsDialog.tsx new file mode 100644 index 0000000..3e6c5d8 --- /dev/null +++ b/src/components/KeyboardShortcutsDialog.tsx @@ -0,0 +1,130 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Card, CardContent } from '@/components/ui/card' +import { Separator } from '@/components/ui/separator' +import { Keyboard } from '@phosphor-icons/react' + +interface KeyboardShortcutsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function KeyboardShortcutsDialog({ open, onOpenChange }: KeyboardShortcutsDialogProps) { + const isMac = navigator.platform.includes('Mac') + const ctrlKey = isMac ? '⌘' : 'Ctrl' + + return ( + + + + + + Keyboard Shortcuts + + + Speed up your workflow with these shortcuts + + + +
+
+

Navigation

+
+ + + + + +
+
+ + + +
+

Actions

+
+ + + + +
+
+ + + +
+

Code Editor

+
+ + + + +
+
+
+
+
+ ) +} + +function ShortcutRow({ keys, description }: { keys: string[]; description: string }) { + return ( +
+ {description} +
+ {keys.map((key, index) => ( + + {key} + + ))} +
+
+ ) +} diff --git a/src/components/ProjectDashboard.tsx b/src/components/ProjectDashboard.tsx new file mode 100644 index 0000000..d7813f7 --- /dev/null +++ b/src/components/ProjectDashboard.tsx @@ -0,0 +1,273 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Progress } from '@/components/ui/progress' +import { + Code, + Database, + Tree, + PaintBrush, + Flask, + Play, + Cube, + FileText, + CheckCircle, + Warning +} from '@phosphor-icons/react' +import { ProjectFile, PrismaModel, ComponentNode, ThemeConfig, PlaywrightTest, StorybookStory, UnitTest, FlaskConfig } from '@/types/project' + +interface ProjectDashboardProps { + files: ProjectFile[] + models: PrismaModel[] + components: ComponentNode[] + theme: ThemeConfig + playwrightTests: PlaywrightTest[] + storybookStories: StorybookStory[] + unitTests: UnitTest[] + flaskConfig: FlaskConfig +} + +export function ProjectDashboard({ + files, + models, + components, + theme, + playwrightTests, + storybookStories, + unitTests, + flaskConfig, +}: ProjectDashboardProps) { + const totalFiles = files.length + const totalModels = models.length + const totalComponents = components.length + const totalThemeVariants = theme.variants.length + const totalEndpoints = flaskConfig.blueprints.reduce((acc, bp) => acc + bp.endpoints.length, 0) + const totalTests = playwrightTests.length + storybookStories.length + unitTests.length + + const completionScore = calculateCompletionScore({ + files: totalFiles, + models: totalModels, + components: totalComponents, + tests: totalTests, + }) + + return ( +
+
+

Project Dashboard

+

+ Overview of your CodeForge project +

+
+ + + + + + Project Completeness + + Overall progress of your application + + +
+ {completionScore}% + = 70 ? 'default' : 'secondary'} className="text-sm"> + {completionScore >= 70 ? 'Ready to Export' : 'In Progress'} + +
+ +

+ {getCompletionMessage(completionScore)} +

+
+
+ +
+ } + title="Code Files" + value={totalFiles} + description={`${totalFiles} file${totalFiles !== 1 ? 's' : ''} in your project`} + color="text-blue-500" + /> + + } + title="Database Models" + value={totalModels} + description={`${totalModels} Prisma model${totalModels !== 1 ? 's' : ''} defined`} + color="text-purple-500" + /> + + } + title="Components" + value={totalComponents} + description={`${totalComponents} component${totalComponents !== 1 ? 's' : ''} in tree`} + color="text-green-500" + /> + + } + title="Theme Variants" + value={totalThemeVariants} + description={`${totalThemeVariants} theme${totalThemeVariants !== 1 ? 's' : ''} configured`} + color="text-pink-500" + /> + + } + title="API Endpoints" + value={totalEndpoints} + description={`${totalEndpoints} Flask endpoint${totalEndpoints !== 1 ? 's' : ''}`} + color="text-orange-500" + /> + + } + title="Tests" + value={totalTests} + description={`${totalTests} test${totalTests !== 1 ? 's' : ''} written`} + color="text-cyan-500" + /> +
+ + + + Project Details + + + } + label="Playwright Tests" + value={playwrightTests.length} + /> + } + label="Storybook Stories" + value={storybookStories.length} + /> + } + label="Unit Tests" + value={unitTests.length} + /> + } + label="Flask Blueprints" + value={flaskConfig.blueprints.length} + /> + + + + {(totalModels === 0 || totalFiles === 0) && ( + + + + + Quick Tips + + + + {totalFiles === 0 && ( +

• Start by creating some code files in the Code Editor tab

+ )} + {totalModels === 0 && ( +

• Define your data models in the Models tab to set up your database

+ )} + {totalComponents === 0 && ( +

• Build your UI structure in the Components tab

+ )} + {totalThemeVariants <= 1 && ( +

• Create additional theme variants (dark mode) in the Styling tab

+ )} + {totalTests === 0 && ( +

• Add tests for better code quality and reliability

+ )} +
+
+ )} +
+ ) +} + +function StatCard({ + icon, + title, + value, + description, + color +}: { + icon: React.ReactNode + title: string + value: number + description: string + color: string +}) { + return ( + + +
+
+

{title}

+

{value}

+

{description}

+
+
{icon}
+
+
+
+ ) +} + +function DetailRow({ + icon, + label, + value +}: { + icon: React.ReactNode + label: string + value: number +}) { + return ( +
+
+ {icon} + {label} +
+ {value} +
+ ) +} + +function calculateCompletionScore(data: { + files: number + models: number + components: number + tests: number +}): number { + const weights = { + files: 25, + models: 25, + components: 25, + tests: 25, + } + + const scores = { + files: Math.min(data.files / 5, 1) * weights.files, + models: Math.min(data.models / 3, 1) * weights.models, + components: Math.min(data.components / 5, 1) * weights.components, + tests: Math.min(data.tests / 5, 1) * weights.tests, + } + + return Math.round( + scores.files + scores.models + scores.components + scores.tests + ) +} + +function getCompletionMessage(score: number): string { + if (score >= 90) return 'Excellent! Your project is comprehensive and ready to deploy.' + if (score >= 70) return 'Great progress! Your project has most essential features.' + if (score >= 50) return 'Good start! Keep adding more features and tests.' + if (score >= 30) return 'Getting there! Add more components and models.' + return 'Just starting! Begin by creating models and components.' +} diff --git a/src/hooks/use-keyboard-shortcuts.ts b/src/hooks/use-keyboard-shortcuts.ts new file mode 100644 index 0000000..e1f5c59 --- /dev/null +++ b/src/hooks/use-keyboard-shortcuts.ts @@ -0,0 +1,49 @@ +import { useEffect } from 'react' + +interface KeyboardShortcut { + key: string + ctrl?: boolean + shift?: boolean + alt?: boolean + action: () => void + description: string +} + +export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) { + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + for (const shortcut of shortcuts) { + const ctrlMatch = shortcut.ctrl ? (event.ctrlKey || event.metaKey) : !event.ctrlKey && !event.metaKey + const shiftMatch = shortcut.shift ? event.shiftKey : !event.shiftKey + const altMatch = shortcut.alt ? event.altKey : !event.altKey + const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase() + + if (ctrlMatch && shiftMatch && altMatch && keyMatch) { + event.preventDefault() + shortcut.action() + break + } + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [shortcuts]) +} + +export function getShortcutDisplay(shortcut: Omit): string { + const parts: string[] = [] + + if (shortcut.ctrl) { + parts.push(navigator.platform.includes('Mac') ? '⌘' : 'Ctrl') + } + if (shortcut.shift) { + parts.push('Shift') + } + if (shortcut.alt) { + parts.push('Alt') + } + parts.push(shortcut.key.toUpperCase()) + + return parts.join(' + ') +}