From adc681af8ca8536d4bfef9f1c8e9e01e1ff2362c Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Tue, 18 Nov 2025 20:33:31 +0000 Subject: [PATCH] Enhance site layout rendering and integrate shared components - Added read-only access to shared frontend components in `docker-compose.app.yml`. - Updated TypeScript configuration in `tsconfig.app.json` and `tsconfig.json` to support path mapping for shared components. - Modified CSS imports in `index.css` to accommodate shared styles. - Refactored layout rendering logic in `layoutRenderer.tsx` to utilize shared layout components for various site layouts. - Improved template rendering in `templateEngine.tsx` by integrating shared block components for better consistency and maintainability. --- backend/celerybeat-schedule | Bin 16384 -> 16384 bytes docker-compose.app.yml | 1 + sites/src/index.css | 4 + sites/src/main.tsx | 3 + sites/src/utils/layoutRenderer.tsx | 364 +++++++++++++++++------------ sites/src/utils/templateEngine.tsx | 208 +++++++++-------- sites/tsconfig.app.json | 6 + sites/tsconfig.json | 2 +- 8 files changed, 342 insertions(+), 246 deletions(-) diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index 8dd13fa1f9b6a0218bad32ab98aaa8ac41115342..52dda699d3678aa93ea423fc19f4bbd6d797c88c 100644 GIT binary patch delta 30 mcmZo@U~Fh$+@NT}FCxjnz_MjZhG^TApef$Ln+;4Za038~p$VM; delta 30 lcmZo@U~Fh$+@NT}FRaVJzz{bjL$qy5&=ha)%?2hHxB-Wc2~Ge2 diff --git a/docker-compose.app.yml b/docker-compose.app.yml index a782c587..6498a98d 100644 --- a/docker-compose.app.yml +++ b/docker-compose.app.yml @@ -116,6 +116,7 @@ services: volumes: - /data/app/igny8/sites:/app:rw - /data/app/sites-data:/sites:ro # Read-only access to deployed sites + - /data/app/igny8/frontend:/frontend:ro # Read-only access to shared components networks: [igny8_net] labels: - "com.docker.compose.project=igny8-app" diff --git a/sites/src/index.css b/sites/src/index.css index e4368115..c892358e 100644 --- a/sites/src/index.css +++ b/sites/src/index.css @@ -1,3 +1,7 @@ +/* Import shared component styles */ +/* Note: Using relative path since @shared alias may not work in CSS */ +/* These will be imported via JavaScript instead */ + :root { font-family: 'Inter', 'Inter var', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #0f172a; diff --git a/sites/src/main.tsx b/sites/src/main.tsx index dcf08c3d..bf351639 100644 --- a/sites/src/main.tsx +++ b/sites/src/main.tsx @@ -2,6 +2,9 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App.tsx'; import './index.css'; +// Import shared component styles - Vite alias @shared resolves to frontend/src/components/shared +import '@shared/blocks/blocks.css'; +import '@shared/layouts/layouts.css'; createRoot(document.getElementById('root')!).render( diff --git a/sites/src/utils/layoutRenderer.tsx b/sites/src/utils/layoutRenderer.tsx index 69bd823f..71a07135 100644 --- a/sites/src/utils/layoutRenderer.tsx +++ b/sites/src/utils/layoutRenderer.tsx @@ -2,20 +2,29 @@ * Layout Renderer * Phase 5: Sites Renderer & Publishing * - * Renders different layout types for sites. + * Renders different layout types for sites using shared components. */ import React from 'react'; import type { SiteDefinition } from '../types'; import { renderTemplate } from './templateEngine'; +import { + DefaultLayout, + MinimalLayout, + MagazineLayout, + EcommerceLayout, + PortfolioLayout, + BlogLayout, + CorporateLayout, +} from '@shared/layouts'; export type LayoutType = | 'default' - | 'centered' - | 'sidebar' - | 'grid' - | 'magazine' | 'minimal' - | 'fullwidth'; + | 'magazine' + | 'ecommerce' + | 'portfolio' + | 'blog' + | 'corporate'; /** * Render site layout based on site definition. @@ -24,18 +33,18 @@ export function renderLayout(siteDefinition: SiteDefinition): React.ReactElement const layoutType = siteDefinition.layout as LayoutType; switch (layoutType) { - case 'centered': - return renderCenteredLayout(siteDefinition); - case 'sidebar': - return renderSidebarLayout(siteDefinition); - case 'grid': - return renderGridLayout(siteDefinition); - case 'magazine': - return renderMagazineLayout(siteDefinition); case 'minimal': return renderMinimalLayout(siteDefinition); - case 'fullwidth': - return renderFullwidthLayout(siteDefinition); + case 'magazine': + return renderMagazineLayout(siteDefinition); + case 'ecommerce': + return renderEcommerceLayout(siteDefinition); + case 'portfolio': + return renderPortfolioLayout(siteDefinition); + case 'blog': + return renderBlogLayout(siteDefinition); + case 'corporate': + return renderCorporateLayout(siteDefinition); case 'default': default: return renderDefaultLayout(siteDefinition); @@ -44,168 +53,227 @@ export function renderLayout(siteDefinition: SiteDefinition): React.ReactElement /** * Default layout: Standard header, content, footer. + * Uses shared DefaultLayout component with fully styled modern design. */ function renderDefaultLayout(siteDefinition: SiteDefinition): React.ReactElement { - return ( -
-
- -
-
- {renderPages(siteDefinition.pages)} -
-
-

© {new Date().getFullYear()} {siteDefinition.name}

-
-
- ); -} + // Find home page for hero + const homePage = siteDefinition.pages.find(p => p.slug === 'home'); + const heroBlock = homePage?.blocks?.find(b => b.type === 'hero'); + const hero: React.ReactNode = heroBlock ? (renderTemplate(heroBlock) as React.ReactNode) : undefined; -/** - * Centered layout: Content centered with max-width. - */ -function renderCenteredLayout(siteDefinition: SiteDefinition): React.ReactElement { - return ( -
-
- -
-
- {renderPages(siteDefinition.pages)} -
-
-

© {new Date().getFullYear()} {siteDefinition.name}

-
-
- ); -} + // Render all pages as sections (excluding hero from home page if it exists) + const sections = siteDefinition.pages + .filter((page) => page.status !== 'draft') + .map((page) => { + // Filter out hero block if it's the home page (already rendered as hero) + const blocksToRender = page.slug === 'home' && heroBlock + ? page.blocks?.filter(b => b.type !== 'hero') || [] + : page.blocks || []; -/** - * Sidebar layout: Sidebar navigation with main content. - */ -function renderSidebarLayout(siteDefinition: SiteDefinition): React.ReactElement { - return ( -
- -
- {renderPages(siteDefinition.pages)} -
-
- ); -} + return ( +
+ {page.slug !== 'home' &&

{page.title}

} + {blocksToRender.length > 0 ? ( + blocksToRender.map((block, index) => ( +
+ {renderTemplate(block)} +
+ )) + ) : page.slug !== 'home' ? ( +

No content available for this page.

+ ) : null} +
+ ); + }); -/** - * Grid layout: Grid-based page layout. - */ -function renderGridLayout(siteDefinition: SiteDefinition): React.ReactElement { return ( -
-
- -
-
- {renderPages(siteDefinition.pages)} -
-
- ); -} - -/** - * Magazine layout: Multi-column magazine style. - */ -function renderMagazineLayout(siteDefinition: SiteDefinition): React.ReactElement { - return ( -
-
-

{siteDefinition.name}

- -
-
- {renderPages(siteDefinition.pages)} -
-
+ ); } /** * Minimal layout: Clean, minimal design. + * Uses shared MinimalLayout component. */ function renderMinimalLayout(siteDefinition: SiteDefinition): React.ReactElement { + const mainContent = ( + <> + {siteDefinition.pages + .filter((page) => page.status !== 'draft') + .map((page) => ( +
+

{page.title}

+ {page.blocks && page.blocks.length > 0 ? ( + page.blocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ )) + ) : null} +
+ ))} + + ); + return ( -
-
- {renderPages(siteDefinition.pages)} -
-
+ + {mainContent} + ); } /** - * Fullwidth layout: Full-width content. + * Magazine layout: Editorial, content-focused. + * Uses shared MagazineLayout component. */ -function renderFullwidthLayout(siteDefinition: SiteDefinition): React.ReactElement { +function renderMagazineLayout(siteDefinition: SiteDefinition): React.ReactElement { + const mainContent = ( + <> + {siteDefinition.pages + .filter((page) => page.status !== 'draft') + .map((page) => ( +
+ {page.blocks && page.blocks.length > 0 ? ( + page.blocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ )) + ) : null} +
+ ))} + + ); + return ( -
-
- -
-
- {renderPages(siteDefinition.pages)} -
-
+ ); } /** - * Render navigation items. + * Ecommerce layout: Product-focused. + * Uses shared EcommerceLayout component. */ -function renderNavigation(navigation: SiteDefinition['navigation']): React.ReactElement { +function renderEcommerceLayout(siteDefinition: SiteDefinition): React.ReactElement { + const mainContent = ( + <> + {siteDefinition.pages + .filter((page) => page.status !== 'draft') + .map((page) => ( +
+ {page.blocks && page.blocks.length > 0 ? ( + page.blocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ )) + ) : null} +
+ ))} + + ); + return ( - + ); } /** - * Render pages. + * Portfolio layout: Showcase. + * Uses shared PortfolioLayout component. */ -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 !== 'draft') - .map((page) => ( -
-

{page.title}

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

No content available for this page.

- )} -
- )); +function renderPortfolioLayout(siteDefinition: SiteDefinition): React.ReactElement { + const mainContent = ( + <> + {siteDefinition.pages + .filter((page) => page.status !== 'draft') + .map((page) => ( +
+ {page.blocks && page.blocks.length > 0 ? ( + page.blocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ )) + ) : null} +
+ ))} + + ); + + return ( + + ); } +/** + * Blog layout: Content-first. + * Uses shared BlogLayout component. + */ +function renderBlogLayout(siteDefinition: SiteDefinition): React.ReactElement { + const mainContent = ( + <> + {siteDefinition.pages + .filter((page) => page.status !== 'draft') + .map((page) => ( +
+ {page.blocks && page.blocks.length > 0 ? ( + page.blocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ )) + ) : null} +
+ ))} + + ); + + return ( + + ); +} + +/** + * Corporate layout: Business. + * Uses shared CorporateLayout component. + */ +function renderCorporateLayout(siteDefinition: SiteDefinition): React.ReactElement { + const mainContent = ( + <> + {siteDefinition.pages + .filter((page) => page.status !== 'draft') + .map((page) => ( +
+ {page.blocks && page.blocks.length > 0 ? ( + page.blocks.map((block, index) => ( +
+ {renderTemplate(block)} +
+ )) + ) : null} +
+ ))} + + ); + + return ( + + ); +} + +// Note: Navigation and page rendering are now handled within each layout component + diff --git a/sites/src/utils/templateEngine.tsx b/sites/src/utils/templateEngine.tsx index 808822a7..58d1593a 100644 --- a/sites/src/utils/templateEngine.tsx +++ b/sites/src/utils/templateEngine.tsx @@ -3,9 +3,24 @@ * Phase 5: Sites Renderer & Publishing * * Renders blocks using shared components from the component library. + * Uses fully styled modern components from @shared. */ import React from 'react'; import type { Block } from '../types'; +import { + HeroBlock, + FeatureGridBlock, + TestimonialsBlock, + CTABlock, + ServicesBlock, + StatsPanel, + TextBlock, + QuoteBlock, + ContactFormBlock, + ImageGalleryBlock, + VideoBlock, + ProductsBlock, +} from '@shared/blocks'; /** * Render a block using the template engine. @@ -76,36 +91,36 @@ export function renderTemplate(block: Block | any): React.ReactElement { } /** - * Render hero block. + * Render hero block using shared HeroBlock component. */ 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 ( -
-

{title}

- {subtitle &&

{subtitle}

} - {content &&

{content}

} - {data.buttonText && ( - - {data.buttonText} - - )} -
+ window.location.href = (data.buttonLink || data.ctaLink) : undefined} + supportingContent={content ?

{content}

: undefined} + /> ); } /** - * Render text block. + * Render text block using shared TextBlock component. */ function renderTextBlock(data: Record): React.ReactElement { + const content = Array.isArray(data.content) ? data.content.join(' ') : data.content; + return ( -
- {data.content &&
} -
+ ); } @@ -274,62 +289,52 @@ function renderAccordionBlock(data: Record): React.ReactElement { } /** - * Render features block. + * Render features block using shared FeatureGridBlock component. */ function renderFeaturesBlock(data: Record): React.ReactElement { - const heading = data.heading || data.title || 'Features'; - const subheading = data.subheading || data.subtitle; + const heading = data.heading || data.title; const content = Array.isArray(data.content) ? data.content : []; const layout = data.layout || 'two-column'; + const columns = layout === 'two-column' ? 2 : layout === 'three-column' ? 3 : 3; + + // Convert content array to features array + const features = content.map((item: string) => ({ + title: item.split(':')[0] || item, + description: item.includes(':') ? item.split(':').slice(1).join(':').trim() : undefined, + })); return ( -
- {heading &&

{heading}

} - {subheading &&

{subheading}

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

{item}

-
- ))} -
-
+ ); } /** - * Render testimonials block. + * Render testimonials block using shared TestimonialsBlock component. */ function renderTestimonialsBlock(data: Record): React.ReactElement { - const heading = data.heading || data.title || 'Testimonials'; - const subheading = data.subheading || data.subtitle; + const title = data.heading || data.title; + const subtitle = data.subheading || data.subtitle; const content = Array.isArray(data.content) ? data.content : []; const layout = data.layout || 'cards'; + // Convert content array to testimonials array + const testimonials = content.map((item: string) => ({ + quote: item, + author: 'Customer', // Default author if not provided + })); + return ( -
- {heading &&

{heading}

} - {subheading &&

{subheading}

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

"{item}"

-
- ))} -
-
+ ); } @@ -357,70 +362,79 @@ function renderFAQBlock(data: Record): React.ReactElement { } /** - * Render CTA (Call to Action) block. + * Render CTA (Call to Action) block using shared CTABlock component. */ function renderCTABlock(data: Record): React.ReactElement { - const heading = data.heading || data.title || 'Call to Action'; - const subheading = data.subheading || data.subtitle; + const title = data.heading || data.title || 'Call to Action'; + const subtitle = 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} - - )} -
+ + {content &&

{content}

} +
); } /** - * Render services block. + * Render services block using shared ServicesBlock component. */ function renderServicesBlock(data: Record): React.ReactElement { - const heading = data.heading || data.title || 'Services'; - const subheading = data.subheading || data.subtitle; + const title = data.heading || data.title; + const subtitle = data.subheading || data.subtitle; const content = Array.isArray(data.content) ? data.content : []; + // Convert content array to services array + const services = content.map((item: string) => ({ + title: item.split(':')[0] || item, + description: item.includes(':') ? item.split(':').slice(1).join(':').trim() : undefined, + })); + return ( -
- {heading &&

{heading}

} - {subheading &&

{subheading}

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

{item}

-
- ))} -
-
+ ); } /** - * Render stats block. + * Render stats block using shared StatsPanel component. */ 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 : []; + // Convert content array to stats array + const stats = content.map((item: string) => { + // Try to parse "Label: Value" format + const parts = item.split(':'); + if (parts.length >= 2) { + return { + label: parts[0].trim(), + value: parts.slice(1).join(':').trim(), + }; + } + return { + label: item, + value: '', + }; + }); + return ( -
- {heading &&

{heading}

} - {subheading &&

{subheading}

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

{item}

-
- ))} -
-
+ ); } diff --git a/sites/tsconfig.app.json b/sites/tsconfig.app.json index 21b6fc4b..630adaf9 100644 --- a/sites/tsconfig.app.json +++ b/sites/tsconfig.app.json @@ -14,6 +14,12 @@ "noEmit": true, "jsx": "react-jsx", + /* Path mapping */ + "baseUrl": ".", + "paths": { + "@shared/*": ["../frontend/src/components/shared/*"] + }, + /* Linting */ "strict": true, "noUnusedLocals": true, diff --git a/sites/tsconfig.json b/sites/tsconfig.json index c36f9b47..e19fe006 100644 --- a/sites/tsconfig.json +++ b/sites/tsconfig.json @@ -7,7 +7,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@shared/*": ["../frontend/src/components/shared/*"] + "@shared/*": ["../frontend/src/components/shared/*", "/frontend/src/components/shared/*"] } } }