diff --git a/components/ProjectDashboard.js b/components/ProjectDashboard.js index 08fe8ba..73adab6 100644 --- a/components/ProjectDashboard.js +++ b/components/ProjectDashboard.js @@ -50,6 +50,7 @@ export default function ProjectDashboard({ projectData }) { const [projectComponents, setProjectComponents] = useState({ pages: projectData.pages.map((page) => ({ ...page, + instances: [], components: [], // Ensure no components are pre-rendered })), }); @@ -87,24 +88,48 @@ export default function ProjectDashboard({ projectData }) { const [sqlCode, setSqlCode] = useState(''); // New state for SQL code const initialFolderStructure = useGenerateFolderStructure(projectData, projectComponents, serverlessApis, sqlCode); // Initial folder structure - - const [availableComponents, setAvailableComponents] = useState([]); - const [dropAreaHeight, setDropAreaHeight] = useState(800); // Default height - const [isResizing, setIsResizing] = useState(false); - const [initialMouseY, setInitialMouseY] = useState(0); - const [initialHeight, setInitialHeight] = useState(800); // Match initial dropAreaHeight - - useEffect(() => { + + const [availableComponents, setAvailableComponents] = useState([]); + const [dropAreaHeight, setDropAreaHeight] = useState(800); // Default height + const [isResizing, setIsResizing] = useState(false); + const [initialMouseY, setInitialMouseY] = useState(0); + const [initialHeight, setInitialHeight] = useState(800); // Match initial dropAreaHeight + + useEffect(() => { const components = projectComponents.pages.flatMap((page) => - page.components.map((component) => component.name) + page.components.map((component) => component.name) ); setAvailableComponents(components); - }, [projectComponents]); + }, [projectComponents]); + - useEffect(() => { - setFolderStructure(initialFolderStructure); - }, [initialFolderStructure]); + const enhancedProjectData = { + ...projectData, + pages: projectData.pages.map((page) => { + const pageInstances = droppedComponents.filter( + (comp) => comp.page === page.name + ); + return { + ...page, + components: pageInstances.map((instance) => ({ + name: instance.name, + code: instance.instanceCode || instance.code, + position: instance.position, + dimensions: instance.dimensions, + })), + }; + }), + }; + + const newStructure = useGenerateFolderStructure( + enhancedProjectData, + projectComponents, + serverlessApis, + sqlCode + ); + setFolderStructure(newStructure); + }, [projectData, projectComponents, droppedComponents, serverlessApis, sqlCode]); const handleShowFolderStructure = () => { setShowFolderStructure(prev => !prev); @@ -112,110 +137,175 @@ export default function ProjectDashboard({ projectData }) { const handleDropComponent = useCallback( (componentName, offset) => { - console.log('Dropping component:', componentName, 'on page:', selectedPage); - - const dropAreaRect = document - .getElementById('drop-area') - .getBoundingClientRect(); - const x = offset.x - dropAreaRect.left; - const y = offset.y - dropAreaRect.top; - - // Find component data - const componentData = projectComponents.pages - .flatMap((page) => page.components) - .find((comp) => comp.name === componentName); - - if (componentData) { - const newComponent = { - ...componentData, - id: uuidv4(), // Assign a unique ID - position: { x, y }, - dimensions: { width: 500, height: 300 }, - page: selectedPage, - }; - - console.log('New component added:', newComponent); - - setDroppedComponents((prev) => [...prev, newComponent]); - } else { - console.error('Component data not found for:', componentName); - } + const dropAreaRect = document + .getElementById('drop-area') + .getBoundingClientRect(); + const x = offset.x - dropAreaRect.left; + const y = offset.y - dropAreaRect.top; + + // Find component data + const componentData = projectComponents.pages + .flatMap((page) => page.components) + .find((comp) => comp.name === componentName); + + if (componentData) { + const newInstance = { + ...componentData, + id: uuidv4(), + position: { x, y }, + dimensions: { width: 500, height: 300 }, + page: selectedPage, + instanceCode: componentData.code, // Track instance-specific code + }; + + // Update dropped components + setDroppedComponents((prev) => [...prev, newInstance]); + + // Update project components with instance data + setProjectComponents((prev) => ({ + ...prev, + pages: prev.pages.map((page) => + page.name === selectedPage + ? { + ...page, + instances: [...page.instances, newInstance], + } + : page + ), + })); + } }, [selectedPage, projectComponents] - ); - - + ); + + useEffect(() => { const generateComponents = async () => { try { - if (projectData.pages && projectData.pages.length > 0) { - if (selectedPage === '' || selectedPage === null) { - setSelectedPage(projectData.pages[0].name); - } - - const uniqueComponents = new Set(); - projectData.pages.forEach(page => { - page.components.forEach(component => { - uniqueComponents.add(component); - }); + setLoading(true); + // First, handle page selection + if (selectedPage === '' || selectedPage === null) { + setSelectedPage(projectData.pages[0].name); + } + + // Generate components first + const uniqueComponents = new Set(); + projectData.pages.forEach(page => { + page.components.forEach(component => { + uniqueComponents.add(component); }); - - const componentMap = {}; - const componentPromises = Array.from(uniqueComponents).map(async (component) => { + }); + + const componentMap = {}; + const componentResults = await Promise.allSettled( + Array.from(uniqueComponents).map(async (component) => { updateComponentStatus(component, 'generating'); - const generatedComponent = await generateComponent(component, JSON.stringify(projectData.styling)); - componentMap[component] = generatedComponent; - }); - - await Promise.all(componentPromises); - - const generatedPages = projectData.pages.map(page => { - const generatedComponents = page.components.map(component => componentMap[component]); - return { ...page, components: generatedComponents }; - }); - - setProjectComponents({ + try { + const generatedComponent = await generateComponent( + component, + "", // Remove page purpose + JSON.stringify(projectData.styling) + ); + componentMap[component] = generatedComponent; + updateComponentStatus(component, 'success'); + } catch (error) { + console.error(`Failed to generate component ${component}:`, error); + updateComponentStatus(component, 'failed'); + } + }) + ); + + // Only proceed if at least some components generated successfully + if (Object.keys(componentMap).length > 0) { + setProjectComponents(prevState => ({ ...projectData, - pages: generatedPages, - }); - } - - if (projectData.apis && projectData.apis.length > 0) { - const generatedApis = await Promise.all( - projectData.apis.map(async (api, index) => { - updateApiStatus(api.name, 'generating'); - const prompt = constructApiPrompt(api, projectData.databaseSchema); - const apiCode = await retryApiGeneration(prompt, api.name); - return { ...api, code: apiCode }; - }) - ); - setServerlessApis(generatedApis); - } - - if (projectData.databaseSchema) { - setSqlStatus('generating'); - const generatedSQL = await generateSQLDatabaseQuery(JSON.stringify(projectData.databaseSchema)); - setSqlCode(generatedSQL); - setSqlStatus('success'); + pages: projectData.pages.map(page => ({ + ...page, + components: page.components.map(component => componentMap[component] || null) + })) + })); + + // Then handle APIs + if (projectData.apis?.length > 0) { + const apiResults = await Promise.allSettled( + projectData.apis.map(async (api) => { + updateApiStatus(api.name, 'generating'); + const prompt = constructApiPrompt(api, projectData.databaseSchema); + return retryApiGeneration(prompt, api.name); + }) + ); + + const successfulApis = apiResults + .map((result, index) => ({ + ...projectData.apis[index], + code: result.status === 'fulfilled' ? result.value : null + })) + .filter(api => api.code !== null); + + setServerlessApis(successfulApis); + } + + // Finally, handle SQL + if (projectData.databaseSchema) { + setSqlStatus('generating'); + const generatedSQL = await generateSQLDatabaseQuery( + JSON.stringify(projectData.databaseSchema) + ); + setSqlCode(generatedSQL); + setSqlStatus('success'); + } + } else { + throw new Error('No components were generated successfully'); } } catch (error) { - console.error('Error generating components:', error); - setSqlStatus('failed'); + console.error('Error in component generation:', error); + // Don't set loading to false here - let the finally block handle it } finally { setLoading(false); } }; - - generateComponents(); - // eslint-disable-next-line react-hooks/exhaustive-deps + + let mounted = true; + + const initializeGeneration = async () => { + try { + if (mounted) { + await generateComponents(); + } + } catch (error) { + console.error('Fatal error during generation:', error); + if (mounted) { + setLoading(false); + } + } + }; + + initializeGeneration(); + + return () => { + mounted = false; + }; }, []); - + const updateComponentStatus = (componentName, status) => { - setComponentStatus(prevStatus => ({ - ...prevStatus, - [componentName]: status - })); + setComponentStatus(prevStatus => { + const newStatus = { + ...prevStatus, + [componentName]: status + }; + + // Check if all components are either success or failed + const isComplete = Object.values(newStatus).every( + status => status === 'success' || status === 'failed' + ); + + if (isComplete && status === 'failed') { + setLoading(false); + } + + return newStatus; + }); }; const updateApiStatus = (apiName, status) => { @@ -241,36 +331,47 @@ export default function ProjectDashboard({ projectData }) { throw new Error('Failed to generate API after multiple attempts'); }; - const generateComponent = async (componentName, pagePurpose, stylingGuide) => { + const generateComponent = async (componentName, stylingGuide) => { let retryCount = 0; const maxRetries = 3; while (retryCount < maxRetries) { try { - const prompt = `Generate a React component named ${componentName} for a page with the following purpose: ${pagePurpose}. Follow this styling guide: ${stylingGuide}`; + // Simplified prompt for more reliable generation + const prompt = `Generate a React component named ${componentName}. Follow this styling guide: ${stylingGuide}. The component should be a functional component that uses styled-components.`; const result = await generateDynamicallyRenderingReactComponent(prompt); const code = typeof result === 'string' ? result : result?.code; - if (typeof code !== 'string' || !code.trim()) { - throw new Error(`Invalid code generated for component ${componentName}`); + if (!code || typeof code !== 'string') { + throw new Error('Invalid code generated'); } - new Function('React', 'styled', `return ${code}`) - - updateComponentStatus(componentName, 'success'); + // Validate the code structure + if (!code.includes('export default') || !code.includes('return')) { + throw new Error('Generated code missing required React component structure'); + } - return { name: componentName, code }; + // Test component compilation + try { + new Function('React', 'styled', `${code}`); + updateComponentStatus(componentName, 'success'); + return { name: componentName, code }; + } catch (compileError) { + throw new Error(`Component compilation failed: ${compileError.message}`); + } } catch (error) { - console.error(`Error generating component '${componentName}' on attempt ${retryCount + 1}:`, error); - retryCount += 1; + console.error(`Generation attempt ${retryCount + 1} failed for ${componentName}:`, error); + retryCount++; updateComponentStatus(componentName, `retrying (${retryCount})`); } } updateComponentStatus(componentName, 'failed'); - - return { name: componentName, code: `() =>
Error generating ${componentName} after ${maxRetries} attempts
` }; + return { + name: componentName, + code: `const ${componentName} = () =>
Failed to generate component
; export default ${componentName};` + }; }; const handleApiCodeChange = (index, newCode) => { @@ -443,7 +544,7 @@ export default function ProjectDashboard({ projectData }) { }, })); } - + const savedPageData = pageLayouts[pageName]; if (savedPageData) { setLayout(savedPageData.layout || []); @@ -452,12 +553,12 @@ export default function ProjectDashboard({ projectData }) { setLayout([]); setGridHeight(600); } - + setSelectedPage(pageName); setSelectedTab('pages'); setShowPagesDropdown(false); }; - + const handleViewModeChange = (mode) => { setViewMode(mode); }; @@ -476,27 +577,27 @@ export default function ProjectDashboard({ projectData }) { const DynamicComponent = React.memo(({ component }) => { const { code, name } = component; - + try { - // Dynamically create the React component - const GeneratedComponent = new Function('React', 'styled', `return ${code}`)( - React, - styled - ); - - // Render the dynamically created component - return ; + // Dynamically create the React component + const GeneratedComponent = new Function('React', 'styled', `return ${code}`)( + React, + styled + ); + + // Render the dynamically created component + return ; } catch (error) { - console.error(`Error rendering dynamic component "${name}":`, error); - return ( -
- Error rendering component: {name} -
- ); + console.error(`Error rendering dynamic component "${name}":`, error); + return ( +
+ Error rendering component: {name} +
+ ); } - }); - - useEffect(() => { + }); + + useEffect(() => { if (selectedPage) { const savedPageData = pageLayouts[selectedPage]; if (savedPageData) { @@ -518,15 +619,15 @@ export default function ProjectDashboard({ projectData }) { w: 2, h: 2, })) || []; - + if (JSON.stringify(layout) !== JSON.stringify(defaultLayout)) { setLayout(defaultLayout); } } } }, [selectedPage, pageLayouts, layout, gridHeight, projectComponents.pages]); - - + + const handleResizeStart = (event) => { setIsResizing(true); setInitialMouseY(event.clientY); @@ -547,8 +648,8 @@ export default function ProjectDashboard({ projectData }) { const handleRemoveComponentInstance = (id) => { setDroppedComponents((prev) => prev.filter((comp) => comp.id !== id)); - }; - + }; + useEffect(() => { if (isResizing) { @@ -566,7 +667,7 @@ export default function ProjectDashboard({ projectData }) { setLayout(newLayout); const maxHeight = Math.max(...newLayout.map((item) => item.y + item.h)) * 100; setGridHeight(maxHeight + 100); - + if (selectedPage) { setPageLayouts((prevLayouts) => ({ ...prevLayouts, @@ -577,83 +678,99 @@ export default function ProjectDashboard({ projectData }) { })); } }; - - const handleComponentCodeUpdate = (updatedCode, index) => { + + const handleComponentCodeUpdate = (updatedCode, instanceId) => { + // Update dropped components setDroppedComponents((prev) => - prev.map((comp, idx) => - idx === index ? { ...comp, code: updatedCode } : comp + prev.map((comp) => + comp.id === instanceId + ? { ...comp, instanceCode: updatedCode } + : comp ) ); + + // Update project components + setProjectComponents((prev) => ({ + ...prev, + pages: prev.pages.map((page) => ({ + ...page, + instances: page.instances.map((instance) => + instance.id === instanceId + ? { ...instance, instanceCode: updatedCode } + : instance + ), + })), + })); }; - + const handleResize = (id, newDimensions) => { setDroppedComponents((prev) => - prev.map((comp) => - comp.id === id - ? { - ...comp, - dimensions: newDimensions, - } - : comp - ) + prev.map((comp) => + comp.id === id + ? { + ...comp, + dimensions: newDimensions, + } + : comp + ) ); - }; + }; const renderComponentsInDropArea = () => { const activePageComponents = droppedComponents.filter( - (comp) => comp.page === selectedPage + (comp) => comp.page === selectedPage ); - + return activePageComponents.map((component) => { - try { - return ( - handleComponentCodeUpdate(updatedCode, component.id)} - onResize={(newDimensions) => handleResize(component.id, newDimensions)} - onRemove={() => handleRemoveComponentInstance(component.id)} - onPositionChange={(newPosition) => { - const dropArea = document.getElementById('drop-area'); - if (dropArea) { - const dropAreaRect = dropArea.getBoundingClientRect(); - const constrainedPosition = { - x: Math.max( - 0, - Math.min(newPosition.x, dropAreaRect.width - 100) - ), - y: Math.max( - 0, - Math.min(newPosition.y, dropAreaRect.height - 50) - ), - }; - setDroppedComponents((prev) => - prev.map((comp) => - comp.id === component.id - ? { ...comp, position: constrainedPosition } - : comp - ) - ); - } - }} - /> - ); - } catch (error) { - console.error(`Error rendering component "${component.name}":`, error); - return ( -
- Error rendering component: {component.name} -
- ); - } + try { + return ( + handleComponentCodeUpdate(updatedCode, component.id)} + onResize={(newDimensions) => handleResize(component.id, newDimensions)} + onRemove={() => handleRemoveComponentInstance(component.id)} + onPositionChange={(newPosition) => { + const dropArea = document.getElementById('drop-area'); + if (dropArea) { + const dropAreaRect = dropArea.getBoundingClientRect(); + const constrainedPosition = { + x: Math.max( + 0, + Math.min(newPosition.x, dropAreaRect.width - 100) + ), + y: Math.max( + 0, + Math.min(newPosition.y, dropAreaRect.height - 50) + ), + }; + setDroppedComponents((prev) => + prev.map((comp) => + comp.id === component.id + ? { ...comp, position: constrainedPosition } + : comp + ) + ); + } + }} + /> + ); + } catch (error) { + console.error(`Error rendering component "${component.name}":`, error); + return ( +
+ Error rendering component: {component.name} +
+ ); + } }); - }; - + }; + const renderComponents = () => { if (!projectComponents?.pages || !selectedPage) return null; @@ -674,8 +791,8 @@ export default function ProjectDashboard({ projectData }) { {/* Drop Area */}
- -
+
+ >
@@ -734,7 +851,7 @@ export default function ProjectDashboard({ projectData }) { ); } }; - + const toggleColorPicker = () => { setColorPickerVisible(!colorPickerVisible); }; @@ -813,13 +930,12 @@ export default function ProjectDashboard({ projectData }) { ) : ( <> -
-
+
+
@@ -829,9 +945,8 @@ export default function ProjectDashboard({ projectData }) { @@ -840,76 +955,74 @@ export default function ProjectDashboard({ projectData }) { )}
- - - - - -
- - {/* Render selected tab content */} - {selectedTab === 'pages' && ( -
-
- {renderPageTabs()} + + + + +
-
- - - - {colorPickerVisible && ( -
- -
- )} + + {/* Render selected tab content */} + {selectedTab === 'pages' && ( +
+
+ {renderPageTabs()} +
+
+ + + + {colorPickerVisible && ( +
+ +
+ )} +
+ {!showFolderStructure && ( +
+ {renderComponents()} +
+ )}
- {!showFolderStructure && ( -
- {renderComponents()} -
)} -
- )} {selectedTab === 'apis' && (
@@ -928,9 +1041,9 @@ export default function ProjectDashboard({ projectData }) { API {index + 1}: {api.name} ({api.method} {api.endpoint}) @@ -1013,30 +1126,30 @@ export default function ProjectDashboard({ projectData }) {
)} -{showFolderStructure && ( -
- {folderStructure ? ( - - ) : ( -
Unable to generate folder structure. Please check the project data.
- )} -
-)} + {showFolderStructure && ( +
+ {folderStructure ? ( + + ) : ( +
Unable to generate folder structure. Please check the project data.
+ )} +
+ )} {showPromptDialog && ( diff --git a/utils/api.js b/utils/api.js index 1488d9b..913662f 100644 --- a/utils/api.js +++ b/utils/api.js @@ -1,7 +1,12 @@ -// utils/api.js import axios from 'axios'; import sparkConfig from '../spark.config.json'; + +// Check if running locally or on Vercel +const isLocalEnvironment = typeof window !== 'undefined' + ? window.location.hostname === 'localhost' + : process.env.VERCEL_ENV !== 'production'; + // Utility function to evaluate and replace expressions within strings const evaluateExpressions = (str, context) => { return str.replace(/'([^']*)'\s*\+\s*([\w.]+)/g, (match, p1, p2) => { @@ -15,7 +20,6 @@ const safeParse = (data, context) => { try { console.log('Raw Data:', data); - // Evaluate expressions and replace single quotes with double quotes, but ignore escaped quotes let fixedData = evaluateExpressions(data, context) .replace(/\\'/g, '\\u0027') // Temporarily replace escaped apostrophes with a Unicode character .replace(/'/g, '"') // Replace non-escaped apostrophes with double quotes @@ -30,20 +34,49 @@ const safeParse = (data, context) => { } }; -export const callSpark = async (projectId, prompt) => { +// Direct Spark API call for local environment +const callSparkDirectly = async (projectId, prompt) => { const payload = { - projectId, - prompt + api_key: process.env.SPARK_API_KEY, + project_id: projectId, + prompt: prompt, }; try { - const response = await axios.post('/api/callSpark', payload); + const response = await axios.post('https://sparkengine.ai/api/engine/completion', payload, { + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Process and sanitize the response + const data = response.data.data.map((item) => { + if (item.output && typeof item.output === 'string') { + // Remove backticks if they exist at the start or end + item.output = item.output.replace(/^```|```$/g, '').trim(); + } + return item; + }); + + return data; + } catch (error) { + console.error('Error calling Spark API directly:', error.message); + throw error; + } +}; - console.log(response.data) +// Function to decide between local direct call or serverless function +export const callSpark = async (projectId, prompt) => { + if (isLocalEnvironment) { + return await callSparkDirectly(projectId, prompt); + } + // Use the serverless function in production + try { + const response = await axios.post('/api/callSpark', { projectId, prompt }); return response.data; } catch (error) { - console.error('Error calling Spark API:', error.message); + console.error('Error calling Spark API through serverless function:', error.message); throw error; } }; diff --git a/utils/folderStructure.js b/utils/folderStructure.js index 5df9a3f..144e206 100644 --- a/utils/folderStructure.js +++ b/utils/folderStructure.js @@ -1,5 +1,5 @@ import { useEffect, useState, useCallback, useRef, useMemo } from 'react'; -import { generateNormalReactPage } from './api'; // Import the generateNormalReactPage function +import { generateNormalReactPage } from './api'; const generateFolderStructureJson = async (projectData, projectComponents, jsconfig, serverlessApis) => { if (!projectData || !projectComponents || !jsconfig || !serverlessApis) return null; @@ -16,16 +16,43 @@ const generateFolderStructureJson = async (projectData, projectComponents, jscon } }; - // Function to modify the component code - function modifyComponentCode(code) { + // Function to generate layout configuration for a page + const generateLayoutConfig = (pageComponents) => { + return pageComponents.map(component => ({ + id: component.name, + position: component.position || { x: 0, y: 0 }, + dimensions: component.dimensions || { width: 0, height: 0 } + })); + }; + + // Function to modify component code with layout information + function modifyComponentCode(code, position, dimensions) { const reactImport = `import React from 'react';\n`; const styledImport = `import styled from 'styled-components';\n`; - // Insert import statements at the beginning - let modifiedCode = `${reactImport}${styledImport}${code}`; + // Create a wrapper with positioning + const wrapperComponent = ` +const PositionedWrapper = styled.div\` + position: absolute; + left: ${position?.x || 0}px; + top: ${position?.y || 0}px; + width: ${dimensions?.width || 'auto'}; + height: ${dimensions?.height || 'auto'}; +\`;\n`; - // Add 'export default' to the start of the function definition - modifiedCode = modifiedCode.replace(/function\s+(\w+)/, 'export default function $1'); + // Modify the component to use the wrapper + let modifiedCode = `${reactImport}${styledImport}${wrapperComponent}${code}`; + + // Wrap the component's return statement with PositionedWrapper + modifiedCode = modifiedCode.replace( + /return\s*\(([\s\S]*?)\);/, + 'return ($1);' + ); + + // Add export default if it doesn't exist + if (!modifiedCode.includes('export default')) { + modifiedCode = modifiedCode.replace(/function\s+(\w+)/, 'export default function $1'); + } return modifiedCode; } @@ -37,18 +64,34 @@ const generateFolderStructureJson = async (projectData, projectComponents, jscon children: [] }); - // Generate the content for each page using generateNormalReactPage + // Generate layout configuration file + structure.children.push({ + name: 'layout.config.js', + content: `export default ${JSON.stringify({ + pages: projectData.pages.reduce((acc, page) => ({ + ...acc, + [page.name]: generateLayoutConfig(page.components) + }), {}) + }, null, 2)}` + }); + + // Generate the content for each page for (const page of projectData.pages) { const strippedName = (page.name || 'unnamed_page').replace(/\s+/g, ''); let pageContent = ''; try { - const prompt = `Generate a Next.js page for ${page.name}. Include the following components: ${page.components?.join(', ') || 'none'}. - Here is the jsconfig.json file for reference: ${JSON.stringify(jsconfig)}.`; - pageContent = await generateNormalReactPage(prompt); // Fetch page content + // Include layout information in the page generation prompt + const layoutInfo = generateLayoutConfig(page.components); + const prompt = `Generate a Next.js page for ${page.name} with the following layout: ${JSON.stringify(layoutInfo)}. + Include these components: ${page.components.map(c => c.name).join(', ')}. + Reference jsconfig.json: ${JSON.stringify(jsconfig)}.`; + + pageContent = await generateNormalReactPage(prompt); + pageContent = pageContent.replace(/```[\s\S]*?```/g, match => match.replace(/```/g, '').trim()); - // Ensuring that the code block returned by the AI is valid and without comments - pageContent = pageContent.replace(/```[\s\S]*?```/g, (match) => match.replace(/```/g, '').trim()); + // Add layout imports and configuration + pageContent = `import layoutConfig from '../layout.config.js';\n${pageContent}`; } catch (error) { console.error(`Error generating page content for ${page.name}:`, error); pageContent = `// Error generating page content for ${page.name}`; @@ -56,53 +99,47 @@ const generateFolderStructureJson = async (projectData, projectComponents, jscon structure.children[0].children.push({ name: strippedName, - originalName: page.name || 'unnamed_page', // Keeping a reference to the original page name - children: [{ name: strippedName, content: pageContent || '' }] + originalName: page.name, + children: [{ name: 'index.js', content: pageContent }] }); } } - // APIs - // if (projectData.apis) { - // structure.children.push({ - // name: 'api', - // children: [] - // }); - // addChildren(structure.children[structure.children.length - 1].children, projectData.apis, api => ({ - // name: `${api.name || 'unnamed_api'}.js`, - // content: api.content || '', - // })); - // } - - // Serverless APIs (newly added section) + // Serverless APIs if (serverlessApis && serverlessApis.length > 0) { structure.children.push({ - name: 'api', // Serverless APIs usually reside in the `/api` folder in Next.js + name: 'api', children: [] }); addChildren(structure.children[structure.children.length - 1].children, serverlessApis, api => ({ name: `${api.name || 'unnamed_serverless_api'}.js`, - content: api.code || '', // Use the code from serverlessApis + content: api.code || '' })); } - // Components - const components = new Map(); // Use a Map to ensure unique components by name + // Components with instance handling + const components = new Map(); if (projectComponents.pages) { projectComponents.pages.forEach(page => { if (page.components) { page.components.forEach(component => { - // Use the component name as the key to ensure uniqueness const componentName = component.name || 'unnamed_component'; + const componentCode = modifyComponentCode( + component.code || component.instanceCode, + component.position, + component.dimensions + ); + if (!components.has(componentName)) { - // Modify the component code as per your requirements - const modifiedCode = modifyComponentCode(component.code); - components.set(componentName, { - ...component, - code: modifiedCode - }); + components.set(componentName, componentCode); + } else { + // If component exists but has different instance code, create a variant + const existingCode = components.get(componentName); + if (existingCode !== componentCode) { + components.set(`${componentName}_${components.size}`, componentCode); + } } }); } @@ -115,49 +152,41 @@ const generateFolderStructureJson = async (projectData, projectComponents, jscon children: [] }); - addChildren(structure.children[structure.children.length - 1].children, Array.from(components.values()), component => ({ - name: `${component.name || 'unnamed_component'}.js`, - content: component.code || '', - })); - } - - // Middleware - if (projectData.middleware) { - structure.children.push({ - name: 'middleware', - children: [] - }); - addChildren(structure.children[structure.children.length - 1].children, projectData.middleware, middleware => ({ - name: `${middleware.name || 'unnamed_middleware'}.js`, - content: middleware.code || '', - })); + addChildren( + structure.children[structure.children.length - 1].children, + Array.from(components.entries()), + ([name, code]) => ({ + name: `${name}.js`, + content: code + }) + ); } - // Add other static folders/files + // Add static configuration files structure.children.push( { name: 'package.json', content: JSON.stringify({ - "name": "nextjs-builder", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" + name: "nextjs-builder", + version: "0.1.0", + private: true, + scripts: { + dev: "next dev", + build: "next build", + start: "next start", + lint: "next lint" }, - "dependencies": { - "next": "14.2.5", - "react": "^18", + dependencies: { + next: "14.2.5", + react: "^18", "react-dom": "^18", "styled-components": "^6.1.12" }, - "devDependencies": { - "postcss": "^8", - "tailwindcss": "^3.4.1" + devDependencies: { + postcss: "^8", + tailwindcss: "^3.4.1" } - }) + }, null, 2) }, { name: 'next.config.js', @@ -174,45 +203,42 @@ const generateFolderStructureJson = async (projectData, projectComponents, jscon { name: 'postcss.config.js', content: `/** @type {import('postcss-load-config').Config} */ - const config = { - plugins: { +const config = { + plugins: { tailwindcss: {}, - }, - }; - - export default config;` + }, +}; + +export default config;` }, { name: 'tailwind.config.js', content: `/** @type {import('tailwindcss').Config} */ - module.exports = { - content: [ +module.exports = { + content: [ "./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { + ], + theme: { extend: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, }, - }, - plugins: [], - };` + }, + plugins: [], +};` } ); - /* styles/globals.css */ - return structure; }; const useGenerateFolderStructure = (projectData, projectComponents, serverlessApis) => { const [folderStructure, setFolderStructure] = useState(null); - const isGeneratedRef = useRef(false); // Use ref to avoid unnecessary re-renders + const isGeneratedRef = useRef(false); const jsconfig = useMemo(() => ({ compilerOptions: { @@ -222,14 +248,19 @@ const useGenerateFolderStructure = (projectData, projectComponents, serverlessAp "@pages/*": ["pages/*"] } } - }), []); // Memoize to avoid unnecessary recalculations + }), []); const generateStructure = useCallback(async () => { if (projectData && projectComponents && jsconfig && !isGeneratedRef.current) { try { - const structure = await generateFolderStructureJson(projectData, projectComponents, jsconfig, serverlessApis); + const structure = await generateFolderStructureJson( + projectData, + projectComponents, + jsconfig, + serverlessApis + ); setFolderStructure(structure); - isGeneratedRef.current = true; // Mark as generated + isGeneratedRef.current = true; } catch (error) { console.error('Error generating folder structure:', error); setFolderStructure(null); @@ -244,4 +275,4 @@ const useGenerateFolderStructure = (projectData, projectComponents, serverlessAp return folderStructure; }; -export default useGenerateFolderStructure; +export default useGenerateFolderStructure; \ No newline at end of file