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://app.igny8.com",
|
||||||
"https://igny8.com",
|
"https://igny8.com",
|
||||||
"https://www.igny8.com",
|
"https://www.igny8.com",
|
||||||
|
"https://sites.igny8.com",
|
||||||
"http://localhost:5173",
|
"http://localhost:5173",
|
||||||
"http://localhost:5174",
|
"http://localhost:5174",
|
||||||
|
"http://localhost:5176",
|
||||||
|
"http://localhost:8024",
|
||||||
"http://localhost:3000",
|
"http://localhost:3000",
|
||||||
"http://127.0.0.1:5173",
|
"http://127.0.0.1:5173",
|
||||||
"http://127.0.0.1:5174",
|
"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
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ WORKDIR /app
|
|||||||
# Copy package manifests first for better caching
|
# Copy package manifests first for better caching
|
||||||
COPY package*.json ./
|
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 source (still bind-mounted at runtime, but needed for initial run)
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const PreviewCanvas = lazy(() => import('./builder/pages/preview/PreviewCanvas')
|
|||||||
const SiteDashboard = lazy(() => import('./builder/pages/dashboard/SiteDashboard'));
|
const SiteDashboard = lazy(() => import('./builder/pages/dashboard/SiteDashboard'));
|
||||||
|
|
||||||
// Renderer pages (load immediately for public sites)
|
// Renderer pages (load immediately for public sites)
|
||||||
const SiteRenderer = lazy(() => import('./renderer/pages/SiteRenderer'));
|
const SiteRenderer = lazy(() => import('./pages/SiteRenderer'));
|
||||||
|
|
||||||
// Loading component
|
// Loading component
|
||||||
const LoadingFallback = () => (
|
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 axios from 'axios';
|
||||||
import type { SiteDefinition } from '../types';
|
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';
|
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.
|
* Load site definition by site ID.
|
||||||
* First tries to load from filesystem (deployed sites),
|
* 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> {
|
export async function loadSiteDefinition(siteId: string): Promise<SiteDefinition> {
|
||||||
// Try API endpoint for deployed site definition first
|
// Try API endpoint for deployed site definition first
|
||||||
try {
|
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) {
|
if (response.data) {
|
||||||
return response.data as SiteDefinition;
|
return response.data as SiteDefinition;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// API load failed, try blueprint endpoint as fallback
|
// 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)
|
// Fallback to blueprint API (for non-deployed sites)
|
||||||
|
|||||||
@@ -7,7 +7,30 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const SITES_DATA_PATH = import.meta.env.SITES_DATA_PATH || '/sites';
|
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.
|
* Get file URL for a site asset.
|
||||||
|
|||||||
@@ -189,16 +189,22 @@ function renderNavigation(navigation: SiteDefinition['navigation']): React.React
|
|||||||
* Render pages.
|
* Render pages.
|
||||||
*/
|
*/
|
||||||
function renderPages(pages: SiteDefinition['pages']): React.ReactElement[] {
|
function renderPages(pages: SiteDefinition['pages']): React.ReactElement[] {
|
||||||
|
// Filter pages - include ready, generating, and deployed statuses
|
||||||
|
// Only exclude draft status
|
||||||
return pages
|
return pages
|
||||||
.filter((page) => page.status === 'ready')
|
.filter((page) => page.status !== 'draft')
|
||||||
.map((page) => (
|
.map((page) => (
|
||||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||||
<h2>{page.title}</h2>
|
<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}>
|
<div key={index} className="block" data-block-type={block.type}>
|
||||||
{renderTemplate(block)}
|
{renderTemplate(block)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<p>No content available for this page.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,20 @@ import type { Block } from '../types';
|
|||||||
* Render a block using the template engine.
|
* Render a block using the template engine.
|
||||||
* Imports shared components dynamically.
|
* Imports shared components dynamically.
|
||||||
*/
|
*/
|
||||||
export function renderTemplate(block: Block): React.ReactElement {
|
export function renderTemplate(block: Block | any): React.ReactElement {
|
||||||
const { type, data } = block;
|
// 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 {
|
||||||
// Try to import shared component
|
// Try to import shared component
|
||||||
@@ -20,6 +32,10 @@ export function renderTemplate(block: Block): React.ReactElement {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'hero':
|
case 'hero':
|
||||||
return renderHeroBlock(data);
|
return renderHeroBlock(data);
|
||||||
|
case 'features':
|
||||||
|
return renderFeaturesBlock(data);
|
||||||
|
case 'testimonials':
|
||||||
|
return renderTestimonialsBlock(data);
|
||||||
case 'text':
|
case 'text':
|
||||||
return renderTextBlock(data);
|
return renderTextBlock(data);
|
||||||
case 'image':
|
case 'image':
|
||||||
@@ -42,6 +58,14 @@ export function renderTemplate(block: Block): React.ReactElement {
|
|||||||
return renderFormBlock(data);
|
return renderFormBlock(data);
|
||||||
case 'accordion':
|
case 'accordion':
|
||||||
return renderAccordionBlock(data);
|
return renderAccordionBlock(data);
|
||||||
|
case 'faq':
|
||||||
|
return renderFAQBlock(data);
|
||||||
|
case 'cta':
|
||||||
|
return renderCTABlock(data);
|
||||||
|
case 'services':
|
||||||
|
return renderServicesBlock(data);
|
||||||
|
case 'stats':
|
||||||
|
return renderStatsBlock(data);
|
||||||
default:
|
default:
|
||||||
return <div className="block-unknown">Unknown block type: {type}</div>;
|
return <div className="block-unknown">Unknown block type: {type}</div>;
|
||||||
}
|
}
|
||||||
@@ -55,12 +79,18 @@ export function renderTemplate(block: Block): React.ReactElement {
|
|||||||
* Render hero block.
|
* Render hero block.
|
||||||
*/
|
*/
|
||||||
function renderHeroBlock(data: Record<string, any>): React.ReactElement {
|
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 (
|
return (
|
||||||
<section className="block-hero" style={{ padding: '4rem 2rem', textAlign: 'center', background: data.background || '#f0f0f0' }}>
|
<section className="block-hero" style={{ padding: '4rem 2rem', textAlign: 'center', background: data.background || '#f0f0f0' }}>
|
||||||
<h1>{data.title || 'Hero Title'}</h1>
|
<h1>{title}</h1>
|
||||||
{data.subtitle && <p>{data.subtitle}</p>}
|
{subtitle && <p style={{ fontSize: '1.2rem', color: '#666', marginTop: '1rem' }}>{subtitle}</p>}
|
||||||
|
{content && <p style={{ marginTop: '1rem' }}>{content}</p>}
|
||||||
{data.buttonText && (
|
{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}
|
{data.buttonText}
|
||||||
</a>
|
</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