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.
This commit is contained in:
@@ -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 (
|
||||
<div className="layout-default">
|
||||
<header className="site-header">
|
||||
<nav className="site-navigation">
|
||||
{renderNavigation(siteDefinition.navigation)}
|
||||
</nav>
|
||||
</header>
|
||||
<main className="site-main">
|
||||
{renderPages(siteDefinition.pages)}
|
||||
</main>
|
||||
<footer className="site-footer">
|
||||
<p>© {new Date().getFullYear()} {siteDefinition.name}</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 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 (
|
||||
<div className="layout-centered">
|
||||
<header className="site-header">
|
||||
<nav className="site-navigation">
|
||||
{renderNavigation(siteDefinition.navigation)}
|
||||
</nav>
|
||||
</header>
|
||||
<main className="site-main" style={{ maxWidth: '1200px', margin: '0 auto', padding: '2rem' }}>
|
||||
{renderPages(siteDefinition.pages)}
|
||||
</main>
|
||||
<footer className="site-footer">
|
||||
<p>© {new Date().getFullYear()} {siteDefinition.name}</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 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 (
|
||||
<div className="layout-sidebar">
|
||||
<aside className="site-sidebar">
|
||||
<nav className="site-navigation">
|
||||
{renderNavigation(siteDefinition.navigation)}
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="site-main">
|
||||
{renderPages(siteDefinition.pages)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
{page.slug !== 'home' && <h2>{page.title}</h2>}
|
||||
{blocksToRender.length > 0 ? (
|
||||
blocksToRender.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : page.slug !== 'home' ? (
|
||||
<p>No content available for this page.</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Grid layout: Grid-based page layout.
|
||||
*/
|
||||
function renderGridLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
||||
return (
|
||||
<div className="layout-grid">
|
||||
<header className="site-header">
|
||||
<nav className="site-navigation">
|
||||
{renderNavigation(siteDefinition.navigation)}
|
||||
</nav>
|
||||
</header>
|
||||
<main className="site-main" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '2rem', padding: '2rem' }}>
|
||||
{renderPages(siteDefinition.pages)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magazine layout: Multi-column magazine style.
|
||||
*/
|
||||
function renderMagazineLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
||||
return (
|
||||
<div className="layout-magazine">
|
||||
<header className="site-header">
|
||||
<h1>{siteDefinition.name}</h1>
|
||||
<nav className="site-navigation">
|
||||
{renderNavigation(siteDefinition.navigation)}
|
||||
</nav>
|
||||
</header>
|
||||
<main className="site-main" style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '2rem', padding: '2rem' }}>
|
||||
{renderPages(siteDefinition.pages)}
|
||||
</main>
|
||||
</div>
|
||||
<DefaultLayout
|
||||
hero={hero as any}
|
||||
sections={sections}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
<h2>{page.title}</h2>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="layout-minimal">
|
||||
<main className="site-main" style={{ padding: '4rem 2rem', maxWidth: '800px', margin: '0 auto' }}>
|
||||
{renderPages(siteDefinition.pages)}
|
||||
</main>
|
||||
</div>
|
||||
<MinimalLayout>
|
||||
{mainContent}
|
||||
</MinimalLayout>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="layout-fullwidth">
|
||||
<header className="site-header">
|
||||
<nav className="site-navigation">
|
||||
{renderNavigation(siteDefinition.navigation)}
|
||||
</nav>
|
||||
</header>
|
||||
<main className="site-main" style={{ width: '100%' }}>
|
||||
{renderPages(siteDefinition.pages)}
|
||||
</main>
|
||||
</div>
|
||||
<MagazineLayout
|
||||
mainContent={mainContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ul style={{ listStyle: 'none', display: 'flex', gap: '1rem', padding: 0 }}>
|
||||
{navigation.map((item) => (
|
||||
<li key={item.slug}>
|
||||
<a href={`/${item.slug}`}>{item.label}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<EcommerceLayout
|
||||
mainContent={mainContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
<h2>{page.title}</h2>
|
||||
{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>
|
||||
));
|
||||
function renderPortfolioLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
||||
const mainContent = (
|
||||
<>
|
||||
{siteDefinition.pages
|
||||
.filter((page) => page.status !== 'draft')
|
||||
.map((page) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<PortfolioLayout
|
||||
mainContent={mainContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<BlogLayout
|
||||
mainContent={mainContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Corporate layout: Business.
|
||||
* Uses shared CorporateLayout component.
|
||||
*/
|
||||
function renderCorporateLayout(siteDefinition: SiteDefinition): React.ReactElement {
|
||||
const mainContent = (
|
||||
<>
|
||||
{siteDefinition.pages
|
||||
.filter((page) => page.status !== 'draft')
|
||||
.map((page) => (
|
||||
<div key={page.id} className="page" data-page-slug={page.slug}>
|
||||
{page.blocks && page.blocks.length > 0 ? (
|
||||
page.blocks.map((block, index) => (
|
||||
<div key={index} className="block" data-block-type={block.type}>
|
||||
{renderTemplate(block)}
|
||||
</div>
|
||||
))
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<CorporateLayout
|
||||
mainContent={mainContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Note: Navigation and page rendering are now handled within each layout component
|
||||
|
||||
|
||||
Reference in New Issue
Block a user