This commit is contained in:
2026-01-06 13:55:04 +01:00
parent 297de69928
commit f991ea6b9b
393 changed files with 41362 additions and 4811 deletions

View File

@@ -139,10 +139,39 @@ function parseWPBakery(html: string): React.ReactNode[] {
// Check for full-width background
const isFullWidth = $row.hasClass('full-width-bg') || $row.hasClass('full-width') || $row.attr('data-full-width');
// Get background image from data attributes or inline styles
const bgImage = $row.attr('data-bg-image') ||
// Get background properties from data attributes
const bgImage = $row.attr('data-bg-image') ||
$row.attr('style')?.match(/background-image:\s*url\(([^)]+)\)/)?.[1] ||
'';
const bgColor = $row.attr('bg_color') || $row.attr('data-bg-color');
const colorOverlay = $row.attr('color_overlay') || $row.attr('data-color-overlay');
const overlayStrength = $row.attr('overlay_strength') || $row.attr('data-overlay-strength');
const topPadding = $row.attr('top_padding');
const bottomPadding = $row.attr('bottom_padding');
const fullScreen = $row.attr('full_screen_row_position');
// Video background attributes - enhanced detection
const videoMp4 = $row.attr('video_mp4') || $row.attr('data-video-mp4') ||
$row.find('[data-video-mp4]').attr('data-video-mp4');
const videoWebm = $row.attr('video_webm') || $row.attr('data-video-webm') ||
$row.find('[data-video-webm]').attr('data-video-webm');
// Check if row has video background indicators
const hasVideoBg = $row.attr('data-video-bg') === 'true' ||
$row.hasClass('nectar-video-wrap') ||
!!(videoMp4?.trim()) || !!(videoWebm?.trim());
// Additional WordPress Salient props
const enableGradient = $row.attr('enable_gradient') === 'true';
const gradientDirection = $row.attr('gradient_direction') || 'left_to_right';
const colorOverlay2 = $row.attr('color_overlay_2');
const parallaxBg = $row.attr('parallax_bg') === 'true';
const parallaxBgSpeed = $row.attr('parallax_bg_speed') || 'medium';
const bgImageAnimation = $row.attr('bg_image_animation') || 'none';
const textAlignment = $row.attr('text_align') || 'left';
const textColor = $row.attr('text_color') || 'dark';
const shapeType = $row.attr('shape_type');
const scenePosition = $row.attr('scene_position') || 'center';
// Get row text for pattern detection
const rowText = $row.text();
@@ -164,14 +193,41 @@ function parseWPBakery(html: string): React.ReactNode[] {
$clone.find('p').first().remove();
const remainingContent = $clone.html()?.trim();
// Calculate overlay opacity
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : undefined;
// Determine height based on full screen position
let heroHeight: 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'screen' = isFullWidth ? 'xl' : 'md';
if (fullScreen === 'middle' || fullScreen === 'top' || fullScreen === 'bottom') {
heroHeight = 'screen';
}
elements.push(
<Hero
key={`hero-${i}`}
title={title}
subtitle={subtitle || undefined}
backgroundImage={heroBg ? replaceUrlInAttribute(heroBg) : undefined}
height={isFullWidth ? 'xl' : 'md'}
backgroundImage={heroBg && !hasVideoBg ? replaceUrlInAttribute(heroBg) : undefined}
height={heroHeight}
overlay={!!heroBg}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
topPadding={topPadding}
bottomPadding={bottomPadding}
textAlignment={textAlignment as any}
textColor={textColor as any}
shapeType={shapeType}
scenePosition={scenePosition as any}
fullScreenRowPosition={fullScreen as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
/>
);
@@ -421,8 +477,33 @@ function parseWPBakery(html: string): React.ReactNode[] {
const title = $h3.text().trim();
const content = $ps.map((pIdx, pEl) => $(pEl).html() || '').get().join('<br/>');
// Calculate overlay opacity
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
elements.push(
<Section key={`content-${i}`} padding="lg">
<Section
key={`content-${i}`}
padding="lg"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
topPadding={topPadding}
bottomPadding={bottomPadding}
textAlignment={textAlignment as any}
textColor={textColor as any}
shapeType={shapeType}
scenePosition={scenePosition as any}
fullScreenRowPosition={fullScreen as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<h3 className="text-2xl font-bold mb-4">{title}</h3>
<ContentRenderer content={content} parsePatterns={false} />
</Section>
@@ -438,7 +519,121 @@ function parseWPBakery(html: string): React.ReactNode[] {
return;
}
// PATTERN 12: Slider/Carousel (nectar_slider, vc_row with slider class)
// PATTERN 12: Generic content row with background (no specific pattern)
// This handles rows with backgrounds that don't match other patterns
if (bgImage || bgColor || colorOverlay || videoMp4 || videoWebm) {
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
const innerHtml = $row.html();
if (innerHtml) {
elements.push(
<Section
key={`bg-section-${i}`}
padding="lg"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
topPadding={topPadding}
bottomPadding={bottomPadding}
textAlignment={textAlignment as any}
textColor={textColor as any}
shapeType={shapeType}
scenePosition={scenePosition as any}
fullScreenRowPosition={fullScreen as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<ContentRenderer content={innerHtml} parsePatterns={true} />
</Section>
);
$row.remove();
return;
}
}
// PATTERN 13: Buttons (vc_btn, .btn classes)
const $buttons = $row.find('a[class*="btn"], a.vc_btn, button.vc_btn');
if ($buttons.length > 0) {
const buttons = $buttons.map((btnIdx, btnEl) => {
const $btn = $(btnEl);
const text = $btn.text().trim();
const href = $btn.attr('href');
const classes = $btn.attr('class') || '';
// Determine variant from classes
let variant: 'primary' | 'secondary' | 'outline' | 'ghost' = 'primary';
if (classes.includes('btn-outline') || classes.includes('vc_btn-outline')) variant = 'outline';
if (classes.includes('btn-secondary') || classes.includes('vc_btn-secondary')) variant = 'secondary';
if (classes.includes('btn-ghost') || classes.includes('vc_btn-ghost')) variant = 'ghost';
// Determine size
let size: 'sm' | 'md' | 'lg' = 'md';
if (classes.includes('btn-large') || classes.includes('vc_btn-lg')) size = 'lg';
if (classes.includes('btn-small') || classes.includes('vc_btn-sm')) size = 'sm';
return (
<Button
key={`btn-${btnIdx}`}
variant={variant}
size={size}
onClick={() => href && (window.location.href = replaceUrlInAttribute(href))}
className={classes.includes('btn-full') ? 'w-full' : ''}
>
{text || 'Click Here'}
</Button>
);
}).get();
if (buttons.length > 0) {
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
elements.push(
<Section
key={`buttons-${i}`}
padding="lg"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
topPadding={topPadding}
bottomPadding={bottomPadding}
textAlignment={textAlignment as any}
textColor={textColor as any}
shapeType={shapeType}
scenePosition={scenePosition as any}
fullScreenRowPosition={fullScreen as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<div className={cn(
'flex flex-wrap gap-3',
buttons.length > 1 ? 'justify-center' : 'justify-start'
)}>
{buttons}
</div>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 14: Slider/Carousel (nectar_slider, vc_row with slider class)
if ($row.hasClass('nectar-slider') || $row.hasClass('vc_row-slider') || $row.find('.nectar-slider').length > 0) {
const slides: Slide[] = [];
@@ -501,56 +696,6 @@ function parseWPBakery(html: string): React.ReactNode[] {
}
}
// PATTERN 13: Buttons (vc_btn, .btn classes)
const $buttons = $row.find('a[class*="btn"], a.vc_btn, button.vc_btn');
if ($buttons.length > 0) {
const buttons = $buttons.map((btnIdx, btnEl) => {
const $btn = $(btnEl);
const text = $btn.text().trim();
const href = $btn.attr('href');
const classes = $btn.attr('class') || '';
// Determine variant from classes
let variant: 'primary' | 'secondary' | 'outline' | 'ghost' = 'primary';
if (classes.includes('btn-outline') || classes.includes('vc_btn-outline')) variant = 'outline';
if (classes.includes('btn-secondary') || classes.includes('vc_btn-secondary')) variant = 'secondary';
if (classes.includes('btn-ghost') || classes.includes('vc_btn-ghost')) variant = 'ghost';
// Determine size
let size: 'sm' | 'md' | 'lg' = 'md';
if (classes.includes('btn-large') || classes.includes('vc_btn-lg')) size = 'lg';
if (classes.includes('btn-small') || classes.includes('vc_btn-sm')) size = 'sm';
return (
<Button
key={`btn-${btnIdx}`}
variant={variant}
size={size}
onClick={() => href && (window.location.href = replaceUrlInAttribute(href))}
className={classes.includes('btn-full') ? 'w-full' : ''}
>
{text || 'Click Here'}
</Button>
);
}).get();
if (buttons.length > 0) {
elements.push(
<Section key={`buttons-${i}`} padding="lg">
<div className={cn(
'flex flex-wrap gap-3',
buttons.length > 1 ? 'justify-center' : 'justify-start'
)}>
{buttons}
</div>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 14: Testimonials (quote blocks, testimonial divs)
const hasTestimonialQuotes = rowText.includes('„') || rowText.includes('“') ||
rowText.includes('"') || rowText.includes('Expertise') ||
@@ -691,6 +836,389 @@ function parseWPBakery(html: string): React.ReactNode[] {
}
}
// PATTERN 16: External Resource Links (vlp-link-container)
const $vlpLinks = $row.find('.vlp-link-container');
if ($vlpLinks.length > 0) {
const linkCards: React.ReactNode[] = [];
$vlpLinks.each((linkIdx, linkEl) => {
const $link = $(linkEl);
const href = $link.find('a').first().attr('href') || '';
const title = $link.find('.vlp-link-title').first().text().trim() || $link.find('a').first().text().trim();
const summary = $link.find('.vlp-link-summary').first().text().trim();
const imgSrc = $link.find('img').first().attr('src');
if (href && title) {
linkCards.push(
<Card key={`vlp-${linkIdx}`} variant="bordered" padding="md" hoverable>
<a
href={replaceUrlInAttribute(href)}
target="_blank"
rel="noopener noreferrer"
className="block hover:no-underline"
>
{imgSrc && (
<div className="mb-3">
<FeaturedImage
src={replaceUrlInAttribute(imgSrc)}
alt={title}
size="sm"
aspectRatio="1:1"
className="rounded-md"
/>
</div>
)}
<h4 className="text-lg font-bold mb-2 text-primary">{title}</h4>
{summary && <p className="text-sm text-gray-600">{summary}</p>}
<div className="mt-2 text-xs text-gray-500 flex items-center gap-1">
<span>🔗</span>
<span className="truncate">{new URL(href).hostname}</span>
</div>
</a>
</Card>
);
}
});
if (linkCards.length > 0) {
elements.push(
<Section key={`vlp-${i}`} padding="lg">
<h3 className="text-2xl font-bold mb-4">Related Resources</h3>
<Grid cols={Math.min(linkCards.length, 3) as 2 | 3 | 4} gap="md">
{linkCards}
</Grid>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 17: Technical Specification Tables
const $tables = $row.find('table');
if ($tables.length > 0) {
const tableElements: React.ReactNode[] = [];
$tables.each((tableIdx, tableEl) => {
const $table = $(tableEl);
const $rows = $table.find('tr');
// Check if this is a spec table (property/value format)
const isSpecTable = $rows.length > 2 && $rows.eq(0).text().includes('Property');
if (isSpecTable) {
const specs: { property: string; value: string }[] = [];
$rows.each((rowIdx, rowEl) => {
if (rowIdx === 0) return; // Skip header
const $row = $(rowEl);
const $cells = $row.find('td');
if ($cells.length >= 2) {
specs.push({
property: $cells.eq(0).text().trim(),
value: $cells.eq(1).text().trim()
});
}
});
if (specs.length > 0) {
tableElements.push(
<div key={`spec-table-${tableIdx}`} className="bg-gray-50 rounded-lg p-6 border border-gray-200">
<h4 className="text-xl font-bold mb-4">Technical Specifications</h4>
<dl className="space-y-3">
{specs.map((spec, idx) => (
<div key={idx} className="flex flex-col sm:flex-row sm:gap-4 border-b border-gray-200 pb-2 last:border-0">
<dt className="font-semibold text-gray-700 sm:w-1/3">{spec.property}</dt>
<dd className="text-gray-600 sm:w-2/3">{spec.value}</dd>
</div>
))}
</dl>
</div>
);
}
} else {
// Regular table - use default HTML rendering
const tableHtml = $table.prop('outerHTML');
if (tableHtml) {
tableElements.push(
<div key={`table-${tableIdx}`} className="my-4 overflow-x-auto">
<ContentRenderer content={tableHtml} parsePatterns={false} />
</div>
);
}
}
});
if (tableElements.length > 0) {
elements.push(
<Section key={`tables-${i}`} padding="lg">
{tableElements}
</Section>
);
$row.remove();
return;
}
}
// PATTERN 18: FAQ Sections
const $questions = $row.find('h3, h4').filter((idx, el) => {
const text = $(el).text().trim();
return text.endsWith('?') || text.toLowerCase().includes('faq') || text.toLowerCase().includes('question');
});
if ($questions.length > 0) {
const faqItems: React.ReactNode[] = [];
$questions.each((qIdx, qEl) => {
const $q = $(qEl);
const question = $q.text().trim();
const $nextP = $q.next('p');
const answer = $nextP.text().trim();
if (question && answer) {
faqItems.push(
<details key={`faq-${qIdx}`} className="bg-white border border-gray-200 rounded-lg p-4 my-2">
<summary className="font-bold cursor-pointer text-primary hover:text-primary-dark">
{question}
</summary>
<p className="mt-2 text-gray-700">{answer}</p>
</details>
);
}
});
if (faqItems.length > 0) {
elements.push(
<Section key={`faq-${i}`} padding="lg">
<h3 className="text-2xl font-bold mb-4">Frequently Asked Questions</h3>
{faqItems}
</Section>
);
$row.remove();
return;
}
}
// PATTERN 19: Call-to-Action (CTA) Sections
const $ctaText = $row.text();
const isCTA = $ctaText.includes('👉') ||
$ctaText.includes('Contact Us') ||
$ctaText.includes('Get in touch') ||
$ctaText.includes('Send your inquiry') ||
($row.find('a[href*="contact"]').length > 0 && $row.find('h2, h3').length > 0);
if (isCTA && colCount <= 2) {
const $title = $row.find('h2, h3').first();
const $desc = $row.find('p').first();
const $button = $row.find('a').first();
const title = $title.text().trim();
const description = $desc.text().trim();
const buttonText = $button.text().trim() || 'Contact Us';
const buttonHref = $button.attr('href') || '/contact';
if (title) {
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
elements.push(
<Section
key={`cta-${i}`}
padding="xl"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor || '#1a1a1a'}
colorOverlay={colorOverlay || '#000000'}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
textAlignment="center"
textColor="light"
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold mb-4">{title}</h2>
{description && <p className="text-xl mb-6 opacity-90">{description}</p>}
<Button
variant="primary"
size="lg"
onClick={() => window.location.href = replaceUrlInAttribute(buttonHref)}
>
{buttonText}
</Button>
</div>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 20: Quote/Blockquote Sections
const $blockquote = $row.find('blockquote').first();
if ($blockquote.length > 0 && colCount === 1) {
const quote = $blockquote.text().trim();
const $cite = $blockquote.find('cite');
const author = $cite.text().trim();
if (quote) {
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
elements.push(
<Section
key={`quote-${i}`}
padding="lg"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
textAlignment="center"
textColor={textColor as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<div className="max-w-2xl mx-auto">
<div className="text-4xl font-serif text-primary mb-4">"</div>
<blockquote className="text-2xl md:text-3xl font-serif italic mb-4">
{quote}
</blockquote>
{author && (
<cite className="text-lg font-semibold not-italic text-gray-300">
— {author}
</cite>
)}
</div>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 21: Numbered List with Icons
const $listItems = $row.find('li');
if ($listItems.length >= 3 && $row.find('i[class*="fa-"]').length > 0) {
const items: React.ReactNode[] = [];
$listItems.each((liIdx, liEl) => {
const $li = $(liEl);
const $icon = $li.find('i[class*="fa-"]').first();
const iconClass = $icon.attr('class') || '';
const text = $li.text().trim().replace(/\s+/g, ' ');
if (text) {
const iconProps = parseWpIcon(iconClass);
items.push(
<div key={`list-item-${liIdx}`} className="flex items-start gap-3">
<div className="flex-shrink-0 mt-1">
<Icon {...iconProps} className="text-primary" />
</div>
<div className="flex-1">
<p className="text-gray-700">{text}</p>
</div>
</div>
);
}
});
if (items.length > 0) {
elements.push(
<Section key={`icon-list-${i}`} padding="lg">
<div className="space-y-3">
{items}
</div>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 22: Video Background Row (nectar-video-wrap or data-video-bg)
if (hasVideoBg) {
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
const innerHtml = $row.html();
if (innerHtml) {
elements.push(
<Section
key={`video-bg-${i}`}
padding="lg"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
topPadding={topPadding}
bottomPadding={bottomPadding}
textAlignment={textAlignment as any}
textColor={textColor as any}
shapeType={shapeType}
scenePosition={scenePosition as any}
fullScreenRowPosition={fullScreen as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<ContentRenderer content={innerHtml} parsePatterns={true} />
</Section>
);
$row.remove();
return;
}
}
// PATTERN 23: Video Embed with Text
const $video = $row.find('video').first();
if ($video.length > 0 && colCount === 1) {
const videoSrc = $video.attr('src') || $video.find('source').first().attr('src');
const $title = $row.find('h2, h3').first();
const $desc = $row.find('p').first();
if (videoSrc) {
elements.push(
<Section key={`video-embed-${i}`} padding="lg">
<div className="grid md:grid-cols-2 gap-6 items-center">
<div>
<video
controls
className="w-full rounded-lg shadow-lg"
style={{ opacity: 1 }}
poster={replaceUrlInAttribute($video.attr('poster') || '')}
>
<source src={replaceUrlInAttribute(videoSrc)} type="video/mp4" />
</video>
</div>
<div>
{$title.length > 0 && <h3 className="text-2xl font-bold mb-3">{$title.text().trim()}</h3>}
{$desc.length > 0 && <ContentRenderer content={$desc.html() || ''} parsePatterns={false} />}
</div>
</div>
</Section>
);
$row.remove();
return;
}
}
// FALLBACK: Generic section with nested content
const innerHtml = $row.html();
if (innerHtml) {
@@ -925,7 +1453,7 @@ function parseHTMLToReact(html: string): React.ReactNode {
// Get sources
const sources: React.ReactNode[] = [];
Array.from(element.childNodes).forEach((child, i) => {
Array.from(node.childNodes).forEach((child, i) => {
if (child.nodeType === Node.ELEMENT_NODE && (child as HTMLElement).tagName.toLowerCase() === 'source') {
const sourceEl = child as HTMLSourceElement;
// Replace asset URLs in source src
@@ -950,6 +1478,10 @@ function parseHTMLToReact(html: string): React.ReactNode {
videoProps.poster = replaceUrlInAttribute(element.getAttribute('poster'));
}
// Ensure video is always fully visible
if (!videoProps.style) videoProps.style = {};
videoProps.style.opacity = 1;
return (
<video {...videoProps}>
{sources}
@@ -1004,6 +1536,7 @@ function parseHTMLToReact(html: string): React.ReactNode {
loop
muted
playsInline
style={{ opacity: 1 }}
>
{mp4 && <source src={replaceUrlInAttribute(mp4)} type="video/mp4" />}
{webm && <source src={replaceUrlInAttribute(webm)} type="video/webm" />}
@@ -1166,19 +1699,20 @@ function replaceWordPressAssets(html: string): string {
/**
* Convert WordPress/Salient classes to Tailwind equivalents
* Note: vc-row and vc-column classes are preserved for pattern parsing
*/
function convertWordPressClasses(html: string): string {
const classMap: Record<string, string> = {
// Salient/Vc_row classes
'vc_row': 'flex flex-wrap -mx-4',
'vc_row-fluid': 'w-full',
'vc_col-sm-12': 'w-full px-4',
'vc_col-md-6': 'w-full md:w-1/2 px-4',
'vc_col-md-4': 'w-full md:w-1/3 px-4',
'vc_col-md-3': 'w-full md:w-1/4 px-4',
'vc_col-lg-6': 'w-full lg:w-1/2 px-4',
'vc_col-lg-4': 'w-full lg:w-1/3 px-4',
'vc_col-lg-3': 'w-full lg:w-1/4 px-4',
// Salient/Vc_row classes - PRESERVED for pattern parsing
// 'vc-row': 'flex flex-wrap -mx-4', // REMOVED - handled by parseWPBakery
// 'vc_row-fluid': 'w-full', // REMOVED - handled by parseWPBakery
// 'vc_col-sm-12': 'w-full px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-md-6': 'w-full md:w-1/2 px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-md-4': 'w-full md:w-1/3 px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-md-3': 'w-full md:w-1/4 px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-lg-6': 'w-full lg:w-1/2 px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-lg-4': 'w-full lg:w-1/3 px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-lg-3': 'w-full lg:w-1/4 px-4', // REMOVED - handled by parseWPBakery
// Typography
'wpb_wrapper': 'space-y-4',

View File

@@ -1,11 +1,13 @@
import React from 'react';
'use client';
import React, { useEffect, useRef } from 'react';
import Image from 'next/image';
import { cn } from '../../lib/utils';
import { Container } from '../ui/Container';
import { Button } from '../ui/Button';
// Hero height options
type HeroHeight = 'sm' | 'md' | 'lg' | 'xl' | 'full';
type HeroHeight = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'screen';
// Hero variant options
type HeroVariant = 'default' | 'dark' | 'primary' | 'gradient';
@@ -24,24 +26,51 @@ interface HeroProps {
overlayOpacity?: number;
children?: React.ReactNode;
className?: string;
// Additional props for background color and overlay
backgroundColor?: string;
colorOverlay?: string;
overlayStrength?: number;
// WordPress Salient-specific props
enableGradient?: boolean;
gradientDirection?: 'left_to_right' | 'right_to_left' | 'top_to_bottom' | 'bottom_to_top';
colorOverlay2?: string;
parallaxBg?: boolean;
parallaxBgSpeed?: 'slow' | 'fast' | 'medium';
bgImageAnimation?: 'none' | 'zoom-out-reveal' | 'fade-in';
topPadding?: string;
bottomPadding?: string;
textAlignment?: 'left' | 'center' | 'right';
textColor?: 'light' | 'dark';
shapeType?: string;
scenePosition?: 'center' | 'top' | 'bottom';
fullScreenRowPosition?: 'middle' | 'top' | 'bottom';
// Video background props
videoBg?: string;
videoMp4?: string;
videoWebm?: string;
}
// Helper function to get height styles
const getHeightStyles = (height: HeroHeight) => {
switch (height) {
case 'sm':
return 'min-h-[300px] md:min-h-[400px]';
case 'md':
return 'min-h-[400px] md:min-h-[500px]';
case 'lg':
return 'min-h-[500px] md:min-h-[600px]';
case 'xl':
return 'min-h-[600px] md:min-h-[700px]';
case 'full':
return 'min-h-screen';
default:
return 'min-h-[500px] md:min-h-[600px]';
const getHeightStyles = (height: HeroHeight, fullScreenRowPosition?: string) => {
const baseHeight = {
sm: 'min-h-[300px] md:min-h-[400px]',
md: 'min-h-[400px] md:min-h-[500px]',
lg: 'min-h-[500px] md:min-h-[600px]',
xl: 'min-h-[600px] md:min-h-[700px]',
full: 'min-h-screen',
screen: 'min-h-screen'
}[height] || 'min-h-[500px] md:min-h-[600px]';
// Handle full screen positioning
if (fullScreenRowPosition === 'middle') {
return `${baseHeight} flex items-center justify-center`;
} else if (fullScreenRowPosition === 'top') {
return `${baseHeight} items-start justify-center pt-12`;
} else if (fullScreenRowPosition === 'bottom') {
return `${baseHeight} items-end justify-center pb-12`;
}
return baseHeight;
};
// Helper function to get variant styles
@@ -80,57 +109,241 @@ export const Hero: React.FC<HeroProps> = ({
overlayOpacity,
children,
className = '',
backgroundColor,
colorOverlay,
overlayStrength,
enableGradient = false,
gradientDirection = 'left_to_right',
colorOverlay2,
parallaxBg = false,
parallaxBgSpeed = 'medium',
bgImageAnimation = 'none',
topPadding,
bottomPadding,
textAlignment = 'center',
textColor = 'light',
shapeType,
scenePosition = 'center',
fullScreenRowPosition,
videoBg,
videoMp4,
videoWebm,
}) => {
const hasBackground = !!backgroundImage;
const hasCTA = !!ctaText && !!ctaLink;
const hasColorOverlay = !!colorOverlay;
const hasGradient = !!enableGradient;
const hasParallax = !!parallaxBg;
const hasVideo = !!(videoMp4?.trim()) || !!(videoWebm?.trim());
const heroRef = useRef<HTMLElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
// Calculate overlay opacity
const overlayOpacityValue = overlayOpacity ?? (overlayStrength !== undefined ? overlayStrength : 0.5);
// Get text alignment
const textAlignClass = {
left: 'text-left',
center: 'text-center',
right: 'text-right',
}[textAlignment];
// Get text color
const textColorClass = textColor === 'light' ? 'text-white' : 'text-gray-900';
const subtitleTextColorClass = textColor === 'light' ? 'text-gray-100' : 'text-gray-600';
// Get gradient direction
const gradientDirectionClass = {
'left_to_right': 'bg-gradient-to-r',
'right_to_left': 'bg-gradient-to-l',
'top_to_bottom': 'bg-gradient-to-b',
'bottom_to_top': 'bg-gradient-to-t',
}[gradientDirection];
// Get parallax speed
const parallaxSpeedClass = {
slow: 'parallax-slow',
medium: 'parallax-medium',
fast: 'parallax-fast',
}[parallaxBgSpeed];
// Get background animation
const bgAnimationClass = {
none: '',
'zoom-out-reveal': 'animate-zoom-out',
'fade-in': 'animate-fade-in',
}[bgImageAnimation];
// Calculate padding from props
const customPaddingStyle = {
paddingTop: topPadding || undefined,
paddingBottom: bottomPadding || undefined,
};
// Parallax effect handler
useEffect(() => {
if (!hasParallax || !heroRef.current) return;
const handleScroll = () => {
if (!heroRef.current) return;
const rect = heroRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
// Calculate offset based on scroll position
const scrollProgress = (viewportHeight - rect.top) / (viewportHeight + rect.height);
const offset = scrollProgress * 50; // Max 50px offset
// Apply to CSS variable
heroRef.current.style.setProperty('--parallax-offset', `${offset}px`);
};
handleScroll(); // Initial call
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [hasParallax]);
return (
<section
ref={heroRef}
className={cn(
'relative w-full overflow-hidden flex items-center justify-center',
getHeightStyles(height),
'relative w-full overflow-hidden',
getHeightStyles(height, fullScreenRowPosition),
textAlignClass,
className
)}
style={{
backgroundColor: backgroundColor || undefined,
...customPaddingStyle,
}}
>
{/* Background Image */}
{hasBackground && (
{/* Video Background */}
{hasVideo && (
<div className="absolute inset-0 z-0">
<video
ref={videoRef}
autoPlay
muted
loop
playsInline
className="absolute inset-0 w-full h-full object-cover"
style={{ opacity: 1 }}
>
{videoWebm && <source src={videoWebm} type="video/webm" />}
{videoMp4 && <source src={videoMp4} type="video/mp4" />}
</video>
</div>
)}
{/* Background Image with Parallax (fallback if no video) */}
{hasBackground && !hasVideo && (
<div className={cn(
'absolute inset-0 z-0',
hasParallax && parallaxSpeedClass,
bgAnimationClass
)}>
<Image
src={backgroundImage}
alt={backgroundAlt || title}
fill
priority
className="object-cover"
className={cn(
'object-cover',
hasParallax && 'transform-gpu'
)}
sizes="100vw"
/>
</div>
)}
{/* Background Variant (if no image) */}
{!hasBackground && (
{!hasBackground && !backgroundColor && (
<div className={cn(
'absolute inset-0 z-0',
getVariantStyles(variant)
)} />
)}
{/* Overlay */}
{overlay && hasBackground && (
{/* Gradient Overlay */}
{hasGradient && (
<div
className={cn(
'absolute inset-0 z-10',
gradientDirectionClass,
'from-transparent via-transparent to-transparent'
)}
style={{
opacity: overlayOpacityValue * 0.3,
}}
/>
)}
{/* Color Overlay (from WordPress color_overlay) */}
{hasColorOverlay && (
<div
className="absolute inset-0 z-10"
style={{
backgroundColor: colorOverlay,
opacity: overlayOpacityValue
}}
/>
)}
{/* Second Color Overlay (for gradients) */}
{colorOverlay2 && (
<div
className="absolute inset-0 z-10"
style={{
backgroundColor: colorOverlay2,
opacity: overlayOpacityValue * 0.5
}}
/>
)}
{/* Standard Overlay */}
{overlay && hasBackground && !hasColorOverlay && (
<div className={cn(
'absolute inset-0 z-10',
getOverlayOpacity(overlayOpacity)
getOverlayOpacity(overlayOpacityValue)
)} />
)}
{/* Shape Divider (bottom) */}
{shapeType && (
<div className="absolute bottom-0 left-0 right-0 z-10">
<svg
className="w-full h-16 md:h-24 lg:h-32"
viewBox="0 0 1200 120"
preserveAspectRatio="none"
>
{shapeType === 'waves_opacity_alt' && (
<path
d="M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.8,104.45-29.34C989.49,25,1113-14.29,1200,52.47V0Z"
opacity=".25"
fill="#ffffff"
/>
)}
{shapeType === 'mountains' && (
<path
d="M0,0V60c100,0,150,20,200,20s100-20,200-20s150,20,200,20s100-20,200-20s150,20,200,20s100-20,200-20V0Z"
opacity=".25"
fill="#ffffff"
/>
)}
</svg>
</div>
)}
{/* Content */}
<div className="relative z-20 w-full">
<Container
maxWidth="6xl"
padding="lg"
padding="none"
className={cn(
'text-center',
'px-4 sm:px-6 md:px-8',
// Add padding for full-height heroes
height === 'full' && 'py-12 md:py-20'
height === 'full' || height === 'screen' ? 'py-12 md:py-20' : 'py-8 md:py-12'
)}
>
{/* Title */}
@@ -139,8 +352,9 @@ export const Hero: React.FC<HeroProps> = ({
'font-bold leading-tight mb-4',
'text-3xl sm:text-4xl md:text-5xl lg:text-6xl',
'tracking-tight',
// Ensure text contrast
hasBackground || variant !== 'default' ? 'text-white' : 'text-gray-900'
textColorClass,
// Enhanced contrast for overlays
(hasBackground || hasColorOverlay || variant !== 'default') && 'drop-shadow-lg'
)}
>
{title}
@@ -153,7 +367,8 @@ export const Hero: React.FC<HeroProps> = ({
'text-lg sm:text-xl md:text-2xl',
'mb-8 max-w-3xl mx-auto',
'leading-relaxed',
hasBackground || variant !== 'default' ? 'text-gray-100' : 'text-gray-600'
subtitleTextColorClass,
(hasBackground || hasColorOverlay || variant !== 'default') && 'drop-shadow-md'
)}
>
{subtitle}

View File

@@ -1,4 +1,7 @@
import React from 'react';
'use client';
import React, { useEffect, useRef } from 'react';
import Image from 'next/image';
import { cn } from '../../lib/utils';
import { Container } from '../ui/Container';
@@ -16,6 +19,33 @@ interface SectionProps {
className?: string;
id?: string;
as?: React.ElementType;
// Additional props for background images and overlays
backgroundImage?: string;
backgroundAlt?: string;
colorOverlay?: string;
overlayOpacity?: number;
backgroundColor?: string;
// WordPress Salient-specific props
enableGradient?: boolean;
gradientDirection?: 'left_to_right' | 'right_to_left' | 'top_to_bottom' | 'bottom_to_top';
colorOverlay2?: string;
parallaxBg?: boolean;
parallaxBgSpeed?: 'slow' | 'fast' | 'medium';
bgImageAnimation?: 'none' | 'zoom-out-reveal' | 'fade-in';
topPadding?: string;
bottomPadding?: string;
textAlignment?: 'left' | 'center' | 'right';
textColor?: 'light' | 'dark';
shapeType?: string;
scenePosition?: 'center' | 'top' | 'bottom';
fullScreenRowPosition?: 'middle' | 'top' | 'bottom';
// Additional styling
borderRadius?: string;
boxShadow?: boolean;
// Video background props
videoBg?: string;
videoMp4?: string;
videoWebm?: string;
}
// Helper function to get background styles
@@ -64,32 +94,362 @@ export const Section: React.FC<SectionProps> = ({
className = '',
id,
as: Component = 'section',
backgroundImage,
backgroundAlt = '',
colorOverlay,
overlayOpacity = 0.5,
backgroundColor,
enableGradient = false,
gradientDirection = 'left_to_right',
colorOverlay2,
parallaxBg = false,
parallaxBgSpeed = 'medium',
bgImageAnimation = 'none',
topPadding,
bottomPadding,
textAlignment = 'left',
textColor = 'dark',
shapeType,
scenePosition = 'center',
fullScreenRowPosition,
borderRadius,
boxShadow = false,
videoBg,
videoMp4,
videoWebm,
}) => {
const sectionClasses = cn(
'w-full',
getBackgroundStyles(background),
const hasBackgroundImage = !!backgroundImage;
const hasColorOverlay = !!colorOverlay;
const hasCustomBg = !!backgroundColor;
const hasGradient = !!enableGradient;
const hasParallax = !!parallaxBg;
const hasVideo = !!(videoMp4?.trim()) || !!(videoWebm?.trim());
const sectionRef = useRef<HTMLDivElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
// Get text alignment
const textAlignClass = {
left: 'text-left',
center: 'text-center',
right: 'text-right',
}[textAlignment];
// Get text color
const textColorClass = textColor === 'light' ? 'text-white' : 'text-gray-900';
// Get gradient direction
const gradientDirectionClass = {
'left_to_right': 'bg-gradient-to-r',
'right_to_left': 'bg-gradient-to-l',
'top_to_bottom': 'bg-gradient-to-b',
'bottom_to_top': 'bg-gradient-to-t',
}[gradientDirection];
// Get parallax speed
const parallaxSpeedClass = {
slow: 'parallax-slow',
medium: 'parallax-medium',
fast: 'parallax-fast',
}[parallaxBgSpeed];
// Get background animation
const bgAnimationClass = {
none: '',
'zoom-out-reveal': 'animate-zoom-out',
'fade-in': 'animate-fade-in',
}[bgImageAnimation];
// Calculate padding from props
const customPaddingStyle = {
paddingTop: topPadding || undefined,
paddingBottom: bottomPadding || undefined,
};
// Base classes
const baseClasses = cn(
'w-full relative overflow-hidden',
getPaddingStyles(padding),
textAlignClass,
textColorClass,
boxShadow && 'shadow-xl',
borderRadius && `rounded-${borderRadius}`,
className
);
const content = fullWidth ? (
<div className={sectionClasses} id={id}>
{children}
</div>
) : (
<section className={sectionClasses} id={id}>
<Container maxWidth="6xl" padding="md">
{children}
</Container>
</section>
// Background style (for solid colors)
const backgroundStyle = hasCustomBg ? { backgroundColor, ...customPaddingStyle } : customPaddingStyle;
// Content wrapper classes
const contentWrapperClasses = cn(
'relative z-20 w-full',
!fullWidth && 'container mx-auto px-4 md:px-6'
);
if (Component !== 'section' && !fullWidth) {
// Parallax effect handler
useEffect(() => {
if (!hasParallax || !sectionRef.current) return;
const handleScroll = () => {
if (!sectionRef.current) return;
const rect = sectionRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
// Calculate offset based on scroll position
const scrollProgress = (viewportHeight - rect.top) / (viewportHeight + rect.height);
const offset = scrollProgress * 50; // Max 50px offset
// Apply to CSS variable
sectionRef.current.style.setProperty('--parallax-offset', `${offset}px`);
};
handleScroll(); // Initial call
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [hasParallax]);
const content = (
<div ref={sectionRef} className={baseClasses} id={id} style={backgroundStyle}>
{/* Video Background */}
{hasVideo && (
<div className="absolute inset-0 z-0">
<video
ref={videoRef}
autoPlay
muted
loop
playsInline
className="absolute inset-0 w-full h-full object-cover"
style={{ opacity: 1 }}
>
{videoWebm && <source src={videoWebm} type="video/webm" />}
{videoMp4 && <source src={videoMp4} type="video/mp4" />}
</video>
</div>
)}
{/* Background Image with Parallax (fallback if no video) */}
{hasBackgroundImage && !hasVideo && (
<div className={cn(
'absolute inset-0 z-0',
hasParallax && parallaxSpeedClass,
bgAnimationClass
)}>
<Image
src={backgroundImage}
alt={backgroundAlt || ''}
fill
className={cn(
'object-cover',
hasParallax && 'transform-gpu'
)}
sizes="100vw"
priority={false}
/>
</div>
)}
{/* Background Variant (if no image) */}
{!hasBackgroundImage && !hasCustomBg && (
<div className={cn(
'absolute inset-0 z-0',
getBackgroundStyles(background)
)} />
)}
{/* Gradient Overlay */}
{hasGradient && (
<div
className={cn(
'absolute inset-0 z-10',
gradientDirectionClass,
'from-transparent via-transparent to-transparent'
)}
style={{
opacity: overlayOpacity * 0.3,
}}
/>
)}
{/* Color Overlay (from WordPress color_overlay) */}
{hasColorOverlay && (
<div
className="absolute inset-0 z-10"
style={{
backgroundColor: colorOverlay,
opacity: overlayOpacity
}}
/>
)}
{/* Second Color Overlay (for gradients) */}
{colorOverlay2 && (
<div
className="absolute inset-0 z-10"
style={{
backgroundColor: colorOverlay2,
opacity: overlayOpacity * 0.5
}}
/>
)}
{/* Shape Divider (bottom) */}
{shapeType && (
<div className="absolute bottom-0 left-0 right-0 z-10">
<svg
className="w-full h-16 md:h-24 lg:h-32"
viewBox="0 0 1200 120"
preserveAspectRatio="none"
>
{shapeType === 'waves_opacity_alt' && (
<path
d="M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.8,104.45-29.34C989.49,25,1113-14.29,1200,52.47V0Z"
opacity=".25"
fill="#ffffff"
/>
)}
{shapeType === 'mountains' && (
<path
d="M0,0V60c100,0,150,20,200,20s100-20,200-20s150,20,200,20s100-20,200-20s150,20,200,20s100-20,200-20V0Z"
opacity=".25"
fill="#ffffff"
/>
)}
</svg>
</div>
)}
{/* Content */}
<div className={contentWrapperClasses}>
{fullWidth ? children : (
<Container maxWidth="6xl" padding="none">
{children}
</Container>
)}
</div>
</div>
);
if (Component !== 'section') {
return (
<Component className={sectionClasses} id={id}>
<Container maxWidth="6xl" padding="md">
{children}
</Container>
<Component className={baseClasses} id={id} style={backgroundStyle}>
{/* Video Background */}
{hasVideo && (
<div className="absolute inset-0 z-0">
<video
ref={videoRef}
autoPlay
muted
loop
playsInline
className="absolute inset-0 w-full h-full object-cover"
style={{ opacity: 1 }}
>
{videoWebm && <source src={videoWebm} type="video/webm" />}
{videoMp4 && <source src={videoMp4} type="video/mp4" />}
</video>
</div>
)}
{/* Background Image with Parallax (fallback if no video) */}
{hasBackgroundImage && !hasVideo && (
<div className={cn(
'absolute inset-0 z-0',
hasParallax && parallaxSpeedClass,
bgAnimationClass
)}>
<Image
src={backgroundImage}
alt={backgroundAlt || ''}
fill
className={cn(
'object-cover',
hasParallax && 'transform-gpu'
)}
sizes="100vw"
priority={false}
/>
</div>
)}
{/* Background Variant (if no image) */}
{!hasBackgroundImage && !hasCustomBg && (
<div className={cn(
'absolute inset-0 z-0',
getBackgroundStyles(background)
)} />
)}
{/* Gradient Overlay */}
{hasGradient && (
<div
className={cn(
'absolute inset-0 z-10',
gradientDirectionClass,
'from-transparent via-transparent to-transparent'
)}
style={{
opacity: overlayOpacity * 0.3,
}}
/>
)}
{/* Color Overlay */}
{hasColorOverlay && (
<div
className="absolute inset-0 z-10"
style={{
backgroundColor: colorOverlay,
opacity: overlayOpacity
}}
/>
)}
{/* Second Color Overlay */}
{colorOverlay2 && (
<div
className="absolute inset-0 z-10"
style={{
backgroundColor: colorOverlay2,
opacity: overlayOpacity * 0.5
}}
/>
)}
{/* Shape Divider */}
{shapeType && (
<div className="absolute bottom-0 left-0 right-0 z-10">
<svg
className="w-full h-16 md:h-24 lg:h-32"
viewBox="0 0 1200 120"
preserveAspectRatio="none"
>
{shapeType === 'waves_opacity_alt' && (
<path
d="M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.8,104.45-29.34C989.49,25,1113-14.29,1200,52.47V0Z"
opacity=".25"
fill="#ffffff"
/>
)}
{shapeType === 'mountains' && (
<path
d="M0,0V60c100,0,150,20,200,20s100-20,200-20s150,20,200,20s100-20,200-20s150,20,200,20s100-20,200-20V0Z"
opacity=".25"
fill="#ffffff"
/>
)}
</svg>
</div>
)}
<div className={contentWrapperClasses}>
{fullWidth ? children : (
<Container maxWidth="6xl" padding="none">
{children}
</Container>
)}
</div>
</Component>
);
}