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:
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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 . .
|
||||
|
||||
@@ -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 = () => (
|
||||
|
||||
23
sites/src/builder/components/layout/BuilderLayout.tsx
Normal file
23
sites/src/builder/components/layout/BuilderLayout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
14
sites/src/builder/pages/dashboard/SiteDashboard.tsx
Normal file
14
sites/src/builder/pages/dashboard/SiteDashboard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
14
sites/src/builder/pages/preview/PreviewCanvas.tsx
Normal file
14
sites/src/builder/pages/preview/PreviewCanvas.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
14
sites/src/builder/pages/wizard/WizardPage.tsx
Normal file
14
sites/src/builder/pages/wizard/WizardPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))}
|
||||
{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>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user