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`.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-18 19:52:42 +00:00
parent d696d55309
commit 0eb039e1a7
12 changed files with 341 additions and 17 deletions

Binary file not shown.

View File

@@ -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

View File

@@ -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 . .

View File

@@ -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 = () => (

View File

@@ -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 (
<div className="builder-layout" style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<header style={{ padding: '1rem', borderBottom: '1px solid #e5e7eb', backgroundColor: '#fff' }}>
<h1 style={{ margin: 0, fontSize: '1.5rem', fontWeight: 600 }}>IGNY8 Site Builder</h1>
</header>
<main style={{ flex: 1, padding: '2rem' }}>
{children}
</main>
</div>
);
}

View File

@@ -0,0 +1,14 @@
/**
* SiteDashboard - Site Builder Dashboard
* Placeholder component for builder dashboard functionality
*/
export default function SiteDashboard() {
return (
<div>
<h2>Site Builder Dashboard</h2>
<p>This is a placeholder for the Site Builder Dashboard.</p>
<p>The builder functionality can be accessed from the main app at <code>/sites/builder/dashboard</code></p>
</div>
);
}

View File

@@ -0,0 +1,14 @@
/**
* PreviewCanvas - Site Builder Preview
* Placeholder component for builder preview functionality
*/
export default function PreviewCanvas() {
return (
<div>
<h2>Site Builder Preview</h2>
<p>This is a placeholder for the Site Builder Preview.</p>
<p>The builder functionality can be accessed from the main app at <code>/sites/builder/preview</code></p>
</div>
);
}

View File

@@ -0,0 +1,14 @@
/**
* WizardPage - Site Builder Wizard
* Placeholder component for builder wizard functionality
*/
export default function WizardPage() {
return (
<div>
<h2>Site Builder Wizard</h2>
<p>This is a placeholder for the Site Builder Wizard.</p>
<p>The builder functionality can be accessed from the main app at <code>/sites/builder</code></p>
</div>
);
}

View File

@@ -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<SiteDefinition> {
// 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)

View File

@@ -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.

View File

@@ -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) => (
<div key={page.id} className="page" data-page-slug={page.slug}>
<h2>{page.title}</h2>
{page.blocks.map((block, index) => (
{page.blocks && page.blocks.length > 0 ? (
page.blocks.map((block, index) => (
<div key={index} className="block" data-block-type={block.type}>
{renderTemplate(block)}
</div>
))}
))
) : (
<p>No content available for this page.</p>
)}
</div>
));
}

View File

@@ -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<string, any>;
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 <div className="block-unknown">Unknown block type: {type}</div>;
}
@@ -55,12 +79,18 @@ export function renderTemplate(block: Block): React.ReactElement {
* Render hero block.
*/
function renderHeroBlock(data: Record<string, any>): 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 (
<section className="block-hero" style={{ padding: '4rem 2rem', textAlign: 'center', background: data.background || '#f0f0f0' }}>
<h1>{data.title || 'Hero Title'}</h1>
{data.subtitle && <p>{data.subtitle}</p>}
<h1>{title}</h1>
{subtitle && <p style={{ fontSize: '1.2rem', color: '#666', marginTop: '1rem' }}>{subtitle}</p>}
{content && <p style={{ marginTop: '1rem' }}>{content}</p>}
{data.buttonText && (
<a href={data.buttonLink || '#'} className="button">
<a href={data.buttonLink || '#'} className="button" style={{ display: 'inline-block', marginTop: '2rem', padding: '0.75rem 1.5rem', background: '#007bff', color: 'white', textDecoration: 'none', borderRadius: '4px' }}>
{data.buttonText}
</a>
)}
@@ -243,3 +273,154 @@ function renderAccordionBlock(data: Record<string, any>): React.ReactElement {
);
}
/**
* Render features block.
*/
function renderFeaturesBlock(data: Record<string, any>): 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 (
<section className="block-features" style={{ padding: '3rem 2rem', background: data.background || 'transparent' }}>
{heading && <h2 style={{ textAlign: 'center', marginBottom: '1rem' }}>{heading}</h2>}
{subheading && <p style={{ textAlign: 'center', color: '#666', marginBottom: '2rem' }}>{subheading}</p>}
<div style={{
display: 'grid',
gridTemplateColumns: layout === 'two-column' ? 'repeat(2, 1fr)' : 'repeat(3, 1fr)',
gap: '2rem',
maxWidth: '1200px',
margin: '0 auto'
}}>
{content.map((item: string, index: number) => (
<div key={index} style={{ padding: '1.5rem', border: '1px solid #eee', borderRadius: '8px' }}>
<p>{item}</p>
</div>
))}
</div>
</section>
);
}
/**
* Render testimonials block.
*/
function renderTestimonialsBlock(data: Record<string, any>): 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 (
<section className="block-testimonials" style={{ padding: '3rem 2rem', background: data.background || '#f9f9f9' }}>
{heading && <h2 style={{ textAlign: 'center', marginBottom: '1rem' }}>{heading}</h2>}
{subheading && <p style={{ textAlign: 'center', color: '#666', marginBottom: '2rem' }}>{subheading}</p>}
<div style={{
display: 'grid',
gridTemplateColumns: layout === 'cards' ? 'repeat(auto-fit, minmax(300px, 1fr))' : '1fr',
gap: '2rem',
maxWidth: '1200px',
margin: '0 auto'
}}>
{content.map((item: string, index: number) => (
<div key={index} style={{ padding: '2rem', background: 'white', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
<p style={{ fontStyle: 'italic', marginBottom: '1rem' }}>"{item}"</p>
</div>
))}
</div>
</section>
);
}
/**
* Render FAQ block.
*/
function renderFAQBlock(data: Record<string, any>): React.ReactElement {
const heading = data.heading || data.title || 'FAQ';
const subheading = data.subheading || data.subtitle;
const content = Array.isArray(data.content) ? data.content : [];
return (
<section className="block-faq" style={{ padding: '3rem 2rem' }}>
{heading && <h2 style={{ marginBottom: '1rem' }}>{heading}</h2>}
{subheading && <p style={{ color: '#666', marginBottom: '2rem' }}>{subheading}</p>}
<div>
{content.map((item: string, index: number) => (
<div key={index} style={{ marginBottom: '1rem', padding: '1rem', borderBottom: '1px solid #eee' }}>
<p><strong>Q:</strong> {item}</p>
</div>
))}
</div>
</section>
);
}
/**
* Render CTA (Call to Action) block.
*/
function renderCTABlock(data: Record<string, any>): 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 (
<section className="block-cta" style={{ padding: '4rem 2rem', textAlign: 'center', background: data.background || '#007bff', color: 'white' }}>
{heading && <h2 style={{ marginBottom: '1rem' }}>{heading}</h2>}
{subheading && <p style={{ marginBottom: '2rem', fontSize: '1.1rem' }}>{subheading}</p>}
{content && <p style={{ marginBottom: '2rem' }}>{content}</p>}
{data.buttonText && (
<a href={data.buttonLink || '#'} style={{ display: 'inline-block', padding: '1rem 2rem', background: 'white', color: '#007bff', textDecoration: 'none', borderRadius: '4px', fontWeight: 'bold' }}>
{data.buttonText}
</a>
)}
</section>
);
}
/**
* Render services block.
*/
function renderServicesBlock(data: Record<string, any>): React.ReactElement {
const heading = data.heading || data.title || 'Services';
const subheading = data.subheading || data.subtitle;
const content = Array.isArray(data.content) ? data.content : [];
return (
<section className="block-services" style={{ padding: '3rem 2rem' }}>
{heading && <h2 style={{ marginBottom: '1rem' }}>{heading}</h2>}
{subheading && <p style={{ color: '#666', marginBottom: '2rem' }}>{subheading}</p>}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '2rem' }}>
{content.map((item: string, index: number) => (
<div key={index} style={{ padding: '1.5rem', border: '1px solid #ddd', borderRadius: '8px' }}>
<p>{item}</p>
</div>
))}
</div>
</section>
);
}
/**
* Render stats block.
*/
function renderStatsBlock(data: Record<string, any>): React.ReactElement {
const heading = data.heading || data.title || 'Statistics';
const subheading = data.subheading || data.subtitle;
const content = Array.isArray(data.content) ? data.content : [];
return (
<section className="block-stats" style={{ padding: '3rem 2rem', background: data.background || '#f9f9f9' }}>
{heading && <h2 style={{ textAlign: 'center', marginBottom: '1rem' }}>{heading}</h2>}
{subheading && <p style={{ textAlign: 'center', color: '#666', marginBottom: '2rem' }}>{subheading}</p>}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
{content.map((item: string, index: number) => (
<div key={index} style={{ textAlign: 'center', padding: '1.5rem' }}>
<p style={{ fontSize: '1.1rem' }}>{item}</p>
</div>
))}
</div>
</section>
);
}