From 0eb039e1a7e31dab80dce95c4837b1ac087ae58b Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Tue, 18 Nov 2025 19:52:42 +0000 Subject: [PATCH] Update CORS settings, enhance API URL detection, and improve template rendering - Added new CORS origins for local development and specific IP addresses in `settings.py`. - Refactored API URL retrieval logic in `loadSiteDefinition.ts` and `fileAccess.ts` to auto-detect based on the current origin. - Enhanced error handling in API calls and improved logging for better debugging. - Updated `renderTemplate` function to support additional block types and improved rendering logic for various components in `templateEngine.tsx`. --- backend/celerybeat-schedule | Bin 16384 -> 16384 bytes backend/igny8_core/settings.py | 6 + sites/Dockerfile.dev | 2 +- sites/src/App.tsx | 2 +- .../components/layout/BuilderLayout.tsx | 23 +++ .../builder/pages/dashboard/SiteDashboard.tsx | 14 ++ .../builder/pages/preview/PreviewCanvas.tsx | 14 ++ sites/src/builder/pages/wizard/WizardPage.tsx | 14 ++ sites/src/loaders/loadSiteDefinition.ts | 49 ++++- sites/src/utils/fileAccess.ts | 25 ++- sites/src/utils/layoutRenderer.tsx | 18 +- sites/src/utils/templateEngine.tsx | 191 +++++++++++++++++- 12 files changed, 341 insertions(+), 17 deletions(-) create mode 100644 sites/src/builder/components/layout/BuilderLayout.tsx create mode 100644 sites/src/builder/pages/dashboard/SiteDashboard.tsx create mode 100644 sites/src/builder/pages/preview/PreviewCanvas.tsx create mode 100644 sites/src/builder/pages/wizard/WizardPage.tsx diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index a0d31a14e48ece3e6d436f3e658c9cc4f516d081..8dd13fa1f9b6a0218bad32ab98aaa8ac41115342 100644 GIT binary patch delta 29 kcmZo@U~Fh$+@NH_r^~>=5H}@5v~5bz6mRd%1|}D{0fPPsI{*Lx delta 29 kcmZo@U~Fh$+@NH_C&|FT5HTe~v~5bz6mR#<1|}D{0f7(+9RL6T diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index 4dae88e4..cf5983cd 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -454,11 +454,17 @@ CORS_ALLOWED_ORIGINS = [ "https://app.igny8.com", "https://igny8.com", "https://www.igny8.com", + "https://sites.igny8.com", "http://localhost:5173", "http://localhost:5174", + "http://localhost:5176", + "http://localhost:8024", "http://localhost:3000", "http://127.0.0.1:5173", "http://127.0.0.1:5174", + "http://127.0.0.1:5176", + "http://127.0.0.1:8024", + "http://31.97.144.105:8024", ] CORS_ALLOW_CREDENTIALS = True diff --git a/sites/Dockerfile.dev b/sites/Dockerfile.dev index 64399848..ef25028d 100644 --- a/sites/Dockerfile.dev +++ b/sites/Dockerfile.dev @@ -6,7 +6,7 @@ WORKDIR /app # Copy package manifests first for better caching COPY package*.json ./ -RUN npm install +RUN npm install --legacy-peer-deps # Copy source (still bind-mounted at runtime, but needed for initial run) COPY . . diff --git a/sites/src/App.tsx b/sites/src/App.tsx index 8788c23f..5c948155 100644 --- a/sites/src/App.tsx +++ b/sites/src/App.tsx @@ -9,7 +9,7 @@ const PreviewCanvas = lazy(() => import('./builder/pages/preview/PreviewCanvas') const SiteDashboard = lazy(() => import('./builder/pages/dashboard/SiteDashboard')); // Renderer pages (load immediately for public sites) -const SiteRenderer = lazy(() => import('./renderer/pages/SiteRenderer')); +const SiteRenderer = lazy(() => import('./pages/SiteRenderer')); // Loading component const LoadingFallback = () => ( diff --git a/sites/src/builder/components/layout/BuilderLayout.tsx b/sites/src/builder/components/layout/BuilderLayout.tsx new file mode 100644 index 00000000..d70e8054 --- /dev/null +++ b/sites/src/builder/components/layout/BuilderLayout.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from 'react'; + +interface BuilderLayoutProps { + children: ReactNode; +} + +/** + * BuilderLayout - Layout wrapper for Site Builder pages + * Provides consistent layout for builder routes + */ +export default function BuilderLayout({ children }: BuilderLayoutProps) { + return ( +
+
+

IGNY8 Site Builder

+
+
+ {children} +
+
+ ); +} + diff --git a/sites/src/builder/pages/dashboard/SiteDashboard.tsx b/sites/src/builder/pages/dashboard/SiteDashboard.tsx new file mode 100644 index 00000000..57f90bd3 --- /dev/null +++ b/sites/src/builder/pages/dashboard/SiteDashboard.tsx @@ -0,0 +1,14 @@ +/** + * SiteDashboard - Site Builder Dashboard + * Placeholder component for builder dashboard functionality + */ +export default function SiteDashboard() { + return ( +
+

Site Builder Dashboard

+

This is a placeholder for the Site Builder Dashboard.

+

The builder functionality can be accessed from the main app at /sites/builder/dashboard

+
+ ); +} + diff --git a/sites/src/builder/pages/preview/PreviewCanvas.tsx b/sites/src/builder/pages/preview/PreviewCanvas.tsx new file mode 100644 index 00000000..f232277c --- /dev/null +++ b/sites/src/builder/pages/preview/PreviewCanvas.tsx @@ -0,0 +1,14 @@ +/** + * PreviewCanvas - Site Builder Preview + * Placeholder component for builder preview functionality + */ +export default function PreviewCanvas() { + return ( +
+

Site Builder Preview

+

This is a placeholder for the Site Builder Preview.

+

The builder functionality can be accessed from the main app at /sites/builder/preview

+
+ ); +} + diff --git a/sites/src/builder/pages/wizard/WizardPage.tsx b/sites/src/builder/pages/wizard/WizardPage.tsx new file mode 100644 index 00000000..2ecedffb --- /dev/null +++ b/sites/src/builder/pages/wizard/WizardPage.tsx @@ -0,0 +1,14 @@ +/** + * WizardPage - Site Builder Wizard + * Placeholder component for builder wizard functionality + */ +export default function WizardPage() { + return ( +
+

Site Builder Wizard

+

This is a placeholder for the Site Builder Wizard.

+

The builder functionality can be accessed from the main app at /sites/builder

+
+ ); +} + diff --git a/sites/src/loaders/loadSiteDefinition.ts b/sites/src/loaders/loadSiteDefinition.ts index 0966d280..b5b51f1f 100644 --- a/sites/src/loaders/loadSiteDefinition.ts +++ b/sites/src/loaders/loadSiteDefinition.ts @@ -7,9 +7,39 @@ import axios from 'axios'; import type { SiteDefinition } from '../types'; -const API_URL = import.meta.env.VITE_API_URL || 'https://api.igny8.com/api'; const SITES_DATA_PATH = import.meta.env.SITES_DATA_PATH || '/sites'; +/** + * Get API base URL - auto-detect based on current origin + */ +function getApiBaseUrl(): string { + // First check environment variables + const envUrl = import.meta.env.VITE_API_URL; + if (envUrl) { + return envUrl.endsWith('/api') ? envUrl : `${envUrl}/api`; + } + + // Auto-detect based on current origin (browser only) + if (typeof window !== 'undefined') { + const origin = window.location.origin; + + // If accessing via IP address, use direct backend port + if (/^\d+\.\d+\.\d+\.\d+/.test(origin) || origin.includes('localhost') || origin.includes('127.0.0.1')) { + // Backend is on port 8011 (external) when accessed via IP + if (origin.includes(':8024')) { + return origin.replace(':8024', ':8011') + '/api'; + } + // Default: try port 8011 + return origin.split(':')[0] + ':8011/api'; + } + } + + // Production: use subdomain + return 'https://api.igny8.com/api'; +} + +const API_URL = getApiBaseUrl(); + /** * Load site definition by site ID. * First tries to load from filesystem (deployed sites), @@ -18,13 +48,26 @@ const SITES_DATA_PATH = import.meta.env.SITES_DATA_PATH || '/sites'; export async function loadSiteDefinition(siteId: string): Promise { // Try API endpoint for deployed site definition first try { - const response = await axios.get(`${API_URL}/v1/publisher/sites/${siteId}/definition/`); + const response = await axios.get(`${API_URL}/v1/publisher/sites/${siteId}/definition/`, { + timeout: 10000, // 10 second timeout + headers: { + 'Accept': 'application/json', + }, + }); if (response.data) { return response.data as SiteDefinition; } } catch (error) { // API load failed, try blueprint endpoint as fallback - console.warn('Failed to load deployed site definition, trying blueprint:', error); + console.error('Failed to load deployed site definition:', error); + if (axios.isAxiosError(error)) { + console.error('Error details:', { + message: error.message, + code: error.code, + response: error.response?.status, + url: error.config?.url, + }); + } } // Fallback to blueprint API (for non-deployed sites) diff --git a/sites/src/utils/fileAccess.ts b/sites/src/utils/fileAccess.ts index 016dee8d..981949cb 100644 --- a/sites/src/utils/fileAccess.ts +++ b/sites/src/utils/fileAccess.ts @@ -7,7 +7,30 @@ */ const SITES_DATA_PATH = import.meta.env.SITES_DATA_PATH || '/sites'; -const API_URL = import.meta.env.VITE_API_URL || 'https://api.igny8.com/api'; + +/** + * Get API base URL - auto-detect based on current origin + */ +function getApiBaseUrl(): string { + const envUrl = import.meta.env.VITE_API_URL; + if (envUrl) { + return envUrl.endsWith('/api') ? envUrl : `${envUrl}/api`; + } + + if (typeof window !== 'undefined') { + const origin = window.location.origin; + if (/^\d+\.\d+\.\d+\.\d+/.test(origin) || origin.includes('localhost') || origin.includes('127.0.0.1')) { + if (origin.includes(':8024')) { + return origin.replace(':8024', ':8011') + '/api'; + } + return origin.split(':')[0] + ':8011/api'; + } + } + + return 'https://api.igny8.com/api'; +} + +const API_URL = getApiBaseUrl(); /** * Get file URL for a site asset. diff --git a/sites/src/utils/layoutRenderer.tsx b/sites/src/utils/layoutRenderer.tsx index deaad19d..69bd823f 100644 --- a/sites/src/utils/layoutRenderer.tsx +++ b/sites/src/utils/layoutRenderer.tsx @@ -189,16 +189,22 @@ function renderNavigation(navigation: SiteDefinition['navigation']): React.React * Render pages. */ function renderPages(pages: SiteDefinition['pages']): React.ReactElement[] { + // Filter pages - include ready, generating, and deployed statuses + // Only exclude draft status return pages - .filter((page) => page.status === 'ready') + .filter((page) => page.status !== 'draft') .map((page) => (

{page.title}

- {page.blocks.map((block, index) => ( -
- {renderTemplate(block)} -
- ))} + {page.blocks && page.blocks.length > 0 ? ( + page.blocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ )) + ) : ( +

No content available for this page.

+ )}
)); } diff --git a/sites/src/utils/templateEngine.tsx b/sites/src/utils/templateEngine.tsx index 0d702ded..808822a7 100644 --- a/sites/src/utils/templateEngine.tsx +++ b/sites/src/utils/templateEngine.tsx @@ -11,8 +11,20 @@ import type { Block } from '../types'; * Render a block using the template engine. * Imports shared components dynamically. */ -export function renderTemplate(block: Block): React.ReactElement { - const { type, data } = block; +export function renderTemplate(block: Block | any): React.ReactElement { + // Handle both formats: { type, data } or { type, ...properties } + let type: string; + let data: Record; + + if (block.type && block.data) { + // Standard format: { type, data } + type = block.type; + data = block.data; + } else { + // API format: { type, heading, subheading, content, ... } + type = block.type; + data = block; + } try { // Try to import shared component @@ -20,6 +32,10 @@ export function renderTemplate(block: Block): React.ReactElement { switch (type) { case 'hero': return renderHeroBlock(data); + case 'features': + return renderFeaturesBlock(data); + case 'testimonials': + return renderTestimonialsBlock(data); case 'text': return renderTextBlock(data); case 'image': @@ -42,6 +58,14 @@ export function renderTemplate(block: Block): React.ReactElement { return renderFormBlock(data); case 'accordion': return renderAccordionBlock(data); + case 'faq': + return renderFAQBlock(data); + case 'cta': + return renderCTABlock(data); + case 'services': + return renderServicesBlock(data); + case 'stats': + return renderStatsBlock(data); default: return
Unknown block type: {type}
; } @@ -55,12 +79,18 @@ export function renderTemplate(block: Block): React.ReactElement { * Render hero block. */ function renderHeroBlock(data: Record): React.ReactElement { + // Handle both API format (heading/subheading) and template format (title/subtitle) + const title = data.heading || data.title || 'Hero Title'; + const subtitle = data.subheading || data.subtitle; + const content = Array.isArray(data.content) ? data.content.join(' ') : data.content; + return (
-

{data.title || 'Hero Title'}

- {data.subtitle &&

{data.subtitle}

} +

{title}

+ {subtitle &&

{subtitle}

} + {content &&

{content}

} {data.buttonText && ( - + {data.buttonText} )} @@ -243,3 +273,154 @@ function renderAccordionBlock(data: Record): React.ReactElement { ); } +/** + * Render features block. + */ +function renderFeaturesBlock(data: Record): React.ReactElement { + const heading = data.heading || data.title || 'Features'; + const subheading = data.subheading || data.subtitle; + const content = Array.isArray(data.content) ? data.content : []; + const layout = data.layout || 'two-column'; + + return ( +
+ {heading &&

{heading}

} + {subheading &&

{subheading}

} +
+ {content.map((item: string, index: number) => ( +
+

{item}

+
+ ))} +
+
+ ); +} + +/** + * Render testimonials block. + */ +function renderTestimonialsBlock(data: Record): React.ReactElement { + const heading = data.heading || data.title || 'Testimonials'; + const subheading = data.subheading || data.subtitle; + const content = Array.isArray(data.content) ? data.content : []; + const layout = data.layout || 'cards'; + + return ( +
+ {heading &&

{heading}

} + {subheading &&

{subheading}

} +
+ {content.map((item: string, index: number) => ( +
+

"{item}"

+
+ ))} +
+
+ ); +} + +/** + * Render FAQ block. + */ +function renderFAQBlock(data: Record): React.ReactElement { + const heading = data.heading || data.title || 'FAQ'; + const subheading = data.subheading || data.subtitle; + const content = Array.isArray(data.content) ? data.content : []; + + return ( +
+ {heading &&

{heading}

} + {subheading &&

{subheading}

} +
+ {content.map((item: string, index: number) => ( +
+

Q: {item}

+
+ ))} +
+
+ ); +} + +/** + * Render CTA (Call to Action) block. + */ +function renderCTABlock(data: Record): React.ReactElement { + const heading = data.heading || data.title || 'Call to Action'; + const subheading = data.subheading || data.subtitle; + const content = Array.isArray(data.content) ? data.content[0] : data.content; + + return ( +
+ {heading &&

{heading}

} + {subheading &&

{subheading}

} + {content &&

{content}

} + {data.buttonText && ( + + {data.buttonText} + + )} +
+ ); +} + +/** + * Render services block. + */ +function renderServicesBlock(data: Record): React.ReactElement { + const heading = data.heading || data.title || 'Services'; + const subheading = data.subheading || data.subtitle; + const content = Array.isArray(data.content) ? data.content : []; + + return ( +
+ {heading &&

{heading}

} + {subheading &&

{subheading}

} +
+ {content.map((item: string, index: number) => ( +
+

{item}

+
+ ))} +
+
+ ); +} + +/** + * Render stats block. + */ +function renderStatsBlock(data: Record): React.ReactElement { + const heading = data.heading || data.title || 'Statistics'; + const subheading = data.subheading || data.subtitle; + const content = Array.isArray(data.content) ? data.content : []; + + return ( +
+ {heading &&

{heading}

} + {subheading &&

{subheading}

} +
+ {content.map((item: string, index: number) => ( +
+

{item}

+
+ ))} +
+
+ ); +} +