if (!function_exists('renderNode')) {
function renderNode($nodeId, $nodes, $websiteId = null, $currencySymbol = 'RM', $websiteSubdomain = null) {
if (!isset($nodes[$nodeId])) {
return '';
}
$node = $nodes[$nodeId];
$type = $node['type']['resolvedName'] ?? 'div';
\Log::info('renderNode called', ['nodeId' => $nodeId, 'type' => $type]);
$props = $node['props'] ?? [];
$childNodes = $node['nodes'] ?? [];
// Debug log
if ($type === 'Posts') {
\Log::info('POSTS NODE FOUND!', ['nodeId' => $nodeId, 'type' => $type, 'props' => $props]);
}
// Render based on component type
switch ($type) {
case 'Container':
return renderContainer($props, $childNodes, $nodes, $websiteId, $currencySymbol, $websiteSubdomain);
case 'Heading':
return renderHeading($props);
case 'Paragraph':
return renderParagraph($props);
case 'Button':
return renderButton($props);
case 'Image':
return renderImage($props);
case 'Row':
return renderRow($props, $childNodes, $nodes, $websiteId, $currencySymbol, $websiteSubdomain);
case 'Column':
return renderColumn($props, $childNodes, $nodes, $websiteId, $currencySymbol, $websiteSubdomain);
case 'Card':
return renderCard($props);
case 'Hero':
return renderHero($props);
case 'IconBox':
return renderIconBox($props);
case 'Slider':
return renderSlider($props);
case 'Accordion':
return renderAccordion($props);
case 'Tabs':
return renderTabs($props);
case 'Gallery':
return renderGallery($props);
case 'Video':
return renderVideo($props);
case 'Form':
return renderForm($props);
case 'Testimonials':
return renderTestimonials($props);
case 'PricingTable':
return renderPricingTable($props);
case 'TeamMembers':
return renderTeamMembers($props);
case 'Stats':
return renderStats($props);
case 'Timeline':
return renderTimeline($props);
case 'Map':
return renderMap($props);
case 'CTABanner':
return renderCTABanner($props);
case 'Divider':
return renderDivider($props);
case 'Spacer':
return renderSpacer($props);
case 'Menu':
return renderMenu($props, $websiteId);
case 'ProductGrid':
return renderProductGrid($props, $websiteId, $currencySymbol, $websiteSubdomain);
case 'FeaturedProduct':
return renderFeaturedProduct($props, $websiteId, $currencySymbol);
case 'AddToCart':
return renderAddToCart($props, $websiteId, $currencySymbol);
case 'BookingForm':
return renderBookingForm($props, $websiteId, $currencySymbol);
case 'ResourcesList':
return renderResourcesList($props, $websiteId, $currencySymbol);
case 'PaymentForm':
return renderPaymentForm($props);
case 'Posts':
\Log::info('Posts case matched!', ['props' => $props, 'websiteId' => $websiteId, 'subdomain' => $websiteSubdomain]);
$result = renderPosts($props, $websiteId, $websiteSubdomain);
\Log::info('renderPosts returned', ['length' => strlen($result), 'preview' => substr($result, 0, 100)]);
return $result;
default:
// Unknown component type - render children in a div
$html = '
';
foreach ($childNodes as $childId) {
$html .= renderNode($childId, $nodes, $websiteId, $currencySymbol, $websiteSubdomain);
}
$html .= '
';
return $html;
}
}
/**
* Helper function to extract margin and padding styles from props
*/
function getSpacingStyles($props) {
$styles = [];
if (isset($props['margin'])) {
$styles[] = 'margin: ' . htmlspecialchars($props['margin']);
}
if (isset($props['padding'])) {
$styles[] = 'padding: ' . htmlspecialchars($props['padding']);
}
return $styles;
}
}
/**
* Render Container component
*/
if (!function_exists('renderContainer')) {
function renderContainer($props, $childNodes, $nodes, $websiteId = null, $currencySymbol = 'RM', $websiteSubdomain = null) {
$width = $props['width'] ?? 'fluid';
$customWidth = $props['customWidth'] ?? '1140px';
$padding = $props['padding'] ?? '20px';
$margin = $props['margin'] ?? '0px';
$backgroundColor = $props['backgroundColor'] ?? 'transparent';
$containerClass = $width === 'fluid' ? 'container-fluid' : 'container';
// Build inline styles
$styles = [];
$styles[] = 'padding: ' . htmlspecialchars($padding);
// Add margin
if ($width === 'fixed') {
// For fixed width, handle margin specially to preserve centering
$margins = explode(' ', trim($margin));
if (count($margins) === 1) {
$styles[] = 'margin: ' . htmlspecialchars($margins[0]) . ' auto';
} else if (count($margins) === 2) {
$styles[] = 'margin: ' . htmlspecialchars($margins[0]) . ' auto';
} else if (count($margins) === 4) {
$styles[] = 'margin: ' . htmlspecialchars($margins[0]) . ' auto ' . htmlspecialchars($margins[2]) . ' auto';
} else {
$styles[] = 'margin-left: auto';
$styles[] = 'margin-right: auto';
}
$styles[] = 'max-width: ' . htmlspecialchars($customWidth);
} else {
// Fluid width, use margin as-is
$styles[] = 'margin: ' . htmlspecialchars($margin);
}
// Only add background color if not transparent
if ($backgroundColor && $backgroundColor !== 'transparent') {
$styles[] = 'background-color: ' . htmlspecialchars($backgroundColor);
}
$styleAttr = implode('; ', $styles);
$html = sprintf('', htmlspecialchars($containerClass), $styleAttr);
foreach ($childNodes as $childId) {
$html .= renderNode($childId, $nodes, $websiteId, $currencySymbol, $websiteSubdomain);
}
$html .= '
';
return $html;
}
}
/**
* Render Heading component
*/
if (!function_exists('renderHeading')) {
function renderHeading($props) {
$text = $props['text'] ?? 'Heading';
$level = $props['level'] ?? 'h2';
$color = $props['color'] ?? '#000000';
$align = $props['align'] ?? 'left';
$styles = getSpacingStyles($props);
$styles[] = sprintf('color: %s', $color);
$styles[] = sprintf('text-align: %s', $align);
$style = implode('; ', $styles);
return sprintf(
'<%s style="%s">%s%s>',
$level,
$style,
htmlspecialchars($text),
$level
);
}
}
/**
* Render Paragraph component
*/
if (!function_exists('renderParagraph')) {
function renderParagraph($props) {
$text = $props['text'] ?? '';
$color = $props['color'] ?? '#666666';
$align = $props['align'] ?? 'left';
$styles = getSpacingStyles($props);
$styles[] = sprintf('color: %s', $color);
$styles[] = sprintf('text-align: %s', $align);
$style = implode('; ', $styles);
return sprintf(
'%s
',
$style,
nl2br(htmlspecialchars($text))
);
}
}
/**
* Render Button component
*/
if (!function_exists('renderButton')) {
function renderButton($props) {
$text = $props['text'] ?? 'Click Me';
$url = $props['url'] ?? '#';
$variant = $props['variant'] ?? 'primary';
$size = $props['size'] ?? 'md';
$spacingStyles = getSpacingStyles($props);
$wrapperStyle = !empty($spacingStyles) ? sprintf(' style="%s"', implode('; ', $spacingStyles)) : '';
return sprintf(
'',
$wrapperStyle,
htmlspecialchars($url),
$variant,
$size,
htmlspecialchars($text)
);
}
}
/**
* Render Image component
*/
if (!function_exists('renderImage')) {
function renderImage($props) {
$src = $props['src'] ?? 'https://via.placeholder.com/800x400';
$alt = $props['alt'] ?? 'Image';
$width = $props['width'] ?? '100%';
$rounded = $props['rounded'] ?? false;
$styles = getSpacingStyles($props);
$styles[] = sprintf('width: %s', $width);
if ($rounded) {
$styles[] = 'border-radius: 8px';
}
$style = implode('; ', $styles);
return sprintf(
'
',
htmlspecialchars($src),
htmlspecialchars($alt),
$style,
$rounded ? 'rounded' : ''
);
}
}
/**
* Render Row component
*/
if (!function_exists('renderRow')) {
function renderRow($props, $childNodes, $nodes, $websiteId = null, $currencySymbol = 'RM', $websiteSubdomain = null) {
$gap = $props['gap'] ?? '20px';
$styles = getSpacingStyles($props);
$styles[] = sprintf('gap: %s', $gap);
$styles[] = sprintf('margin-left: -%spx', intval($gap) / 2);
$styles[] = sprintf('margin-right: -%spx', intval($gap) / 2);
$style = implode('; ', $styles);
$html = sprintf('', $style);
foreach ($childNodes as $childId) {
$html .= renderNode($childId, $nodes, $websiteId, $currencySymbol, $websiteSubdomain);
}
$html .= '
';
return $html;
}
}
/**
* Render Column component
*/
if (!function_exists('renderColumn')) {
function renderColumn($props, $childNodes, $nodes, $websiteId = null, $currencySymbol = 'RM', $websiteSubdomain = null) {
$width = $props['width'] ?? 'auto';
$padding = $props['padding'] ?? '15px';
$backgroundColor = $props['backgroundColor'] ?? 'transparent';
// Convert width to Bootstrap column class
$columnClass = 'col';
if ($width === '100%') $columnClass = 'col-12';
elseif ($width === '50%') $columnClass = 'col-md-6';
elseif ($width === '33.33%') $columnClass = 'col-md-4';
elseif ($width === '25%') $columnClass = 'col-md-3';
elseif ($width === '66.66%') $columnClass = 'col-md-8';
elseif ($width === '75%') $columnClass = 'col-md-9';
$style = sprintf(
'padding: %s; background-color: %s; min-height: 50px;',
$padding,
$backgroundColor
);
$html = sprintf('', $columnClass, $style);
foreach ($childNodes as $childId) {
$html .= renderNode($childId, $nodes, $websiteId, $currencySymbol, $websiteSubdomain);
}
$html .= '
';
return $html;
}
}
/**
* Render Card component
*/
if (!function_exists('renderCard')) {
function renderCard($props) {
$image = $props['image'] ?? 'https://via.placeholder.com/400x250/667eea/ffffff?text=Card+Image';
$title = $props['title'] ?? 'Card Title';
$description = $props['description'] ?? 'Card description goes here.';
$buttonText = $props['buttonText'] ?? 'Learn More';
$buttonUrl = $props['buttonUrl'] ?? '#';
$imagePosition = $props['imagePosition'] ?? 'top';
$alignment = $props['alignment'] ?? 'left';
$showButton = $props['showButton'] ?? true;
$padding = $props['padding'] ?? '20px';
$margin = $props['margin'] ?? '0px 0px 20px 0px';
$cardClass = ($imagePosition === 'left' || $imagePosition === 'right')
? 'card d-flex flex-row'
: 'card';
$html = sprintf('',
$cardClass,
htmlspecialchars($padding),
htmlspecialchars($margin)
);
// Image
if ($imagePosition === 'top' || $imagePosition === 'left') {
$imgClass = $imagePosition === 'top' ? 'card-img-top' : 'img-fluid rounded';
$imgStyle = $imagePosition === 'left' ? 'max-width: 200px; margin-right: 20px;' : '';
$html .= sprintf('

',
htmlspecialchars($image),
htmlspecialchars($title),
$imgClass,
$imgStyle
);
}
// Card body
$html .= sprintf('
', $alignment);
$html .= sprintf('
%s
', htmlspecialchars($title));
$html .= sprintf('
%s
', htmlspecialchars($description));
if ($showButton) {
$html .= sprintf('
%s',
htmlspecialchars($buttonUrl),
htmlspecialchars($buttonText)
);
}
$html .= '
';
// Right/Bottom image
if ($imagePosition === 'right') {
$html .= sprintf('

',
htmlspecialchars($image),
htmlspecialchars($title)
);
} elseif ($imagePosition === 'bottom') {
$html .= sprintf('

',
htmlspecialchars($image),
htmlspecialchars($title)
);
}
$html .= '
';
return $html;
}
}
/**
* Render Hero component
*/
if (!function_exists('renderHero')) {
function renderHero($props) {
$title = $props['title'] ?? 'Welcome to Our Website';
$subtitle = $props['subtitle'] ?? 'Build your perfect website';
$backgroundType = $props['backgroundType'] ?? 'gradient';
$backgroundColor = $props['backgroundColor'] ?? '#667eea';
$backgroundImage = $props['backgroundImage'] ?? '';
$gradientStart = $props['gradientStart'] ?? '#667eea';
$gradientEnd = $props['gradientEnd'] ?? '#764ba2';
$overlay = $props['overlay'] ?? true;
$overlayOpacity = $props['overlayOpacity'] ?? 0.5;
$minHeight = $props['minHeight'] ?? '500px';
$padding = $props['padding'] ?? '60px 20px';
$margin = $props['margin'] ?? '0px';
$textColor = $props['textColor'] ?? '#ffffff';
$alignment = $props['alignment'] ?? 'center';
$showButton = $props['showButton'] ?? true;
$buttonText = $props['buttonText'] ?? 'Get Started';
$buttonUrl = $props['buttonUrl'] ?? '#';
$buttonVariant = $props['buttonVariant'] ?? 'light';
// Background style
$backgroundStyle = '';
if ($backgroundType === 'image') {
$backgroundStyle = sprintf('background-image: url(%s); background-size: cover; background-position: center;',
htmlspecialchars($backgroundImage));
} elseif ($backgroundType === 'gradient') {
$backgroundStyle = sprintf('background: linear-gradient(135deg, %s 0%%, %s 100%%);',
$gradientStart, $gradientEnd);
} else {
$backgroundStyle = sprintf('background-color: %s;', $backgroundColor);
}
$html = sprintf(
'',
$backgroundStyle,
$minHeight,
$alignment,
htmlspecialchars($padding),
htmlspecialchars($margin)
);
// Overlay
if ($overlay && $backgroundType === 'image') {
$html .= sprintf('
',
$overlayOpacity);
}
// Content
$html .= '
';
$textShadow = $backgroundType === 'image' ? 'text-shadow: 2px 2px 4px rgba(0,0,0,0.3);' : '';
$html .= sprintf('
%s
',
$textColor, $textShadow, htmlspecialchars($title));
$html .= sprintf('
%s
',
$textColor, $textShadow, htmlspecialchars($subtitle));
if ($showButton) {
$html .= sprintf('
%s',
htmlspecialchars($buttonUrl),
$buttonVariant,
htmlspecialchars($buttonText)
);
}
$html .= '
';
return $html;
}
}
/**
* Render Icon Box component
*/
if (!function_exists('renderIconBox')) {
function renderIconBox($props) {
$icon = $props['icon'] ?? 'bi-star-fill';
$title = $props['title'] ?? 'Feature Title';
$description = $props['description'] ?? 'Feature description goes here.';
$iconColor = $props['iconColor'] ?? '#667eea';
$iconSize = $props['iconSize'] ?? '48px';
$iconStyle = $props['iconStyle'] ?? 'default';
$alignment = $props['alignment'] ?? 'center';
$titleColor = $props['titleColor'] ?? '#333333';
$descriptionColor = $props['descriptionColor'] ?? '#666666';
// Icon container style
$iconContainerStyle = 'display: inline-flex; align-items: center; justify-content: center; margin-bottom: 15px;';
if ($iconStyle === 'circle') {
$iconContainerStyle .= sprintf(' width: calc(%s + 30px); height: calc(%s + 30px); border-radius: 50%%; background-color: %s20; border: 2px solid %s;',
$iconSize, $iconSize, $iconColor, $iconColor);
} elseif ($iconStyle === 'square') {
$iconContainerStyle .= sprintf(' width: calc(%s + 30px); height: calc(%s + 30px); border-radius: 8px; background-color: %s20; border: 2px solid %s;',
$iconSize, $iconSize, $iconColor, $iconColor);
} elseif ($iconStyle === 'filled-circle') {
$iconContainerStyle .= sprintf(' width: calc(%s + 30px); height: calc(%s + 30px); border-radius: 50%%; background-color: %s;',
$iconSize, $iconSize, $iconColor);
} elseif ($iconStyle === 'filled-square') {
$iconContainerStyle .= sprintf(' width: calc(%s + 30px); height: calc(%s + 30px); border-radius: 8px; background-color: %s;',
$iconSize, $iconSize, $iconColor);
}
$iconColorFinal = (str_contains($iconStyle, 'filled')) ? '#ffffff' : $iconColor;
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = sprintf('text-align: %s', $alignment);
$styleAttr = implode('; ', $spacingStyles);
$html = sprintf('', $styleAttr);
$html .= sprintf('
', $iconContainerStyle);
$html .= sprintf('', $icon, $iconSize, $iconColorFinal);
$html .= '
';
$html .= sprintf('
%s
',
$titleColor, htmlspecialchars($title));
$html .= sprintf('
%s
',
$descriptionColor, htmlspecialchars($description));
$html .= '
';
return $html;
}
}
/**
* Render Slider component using Bootstrap 5 Carousel
*/
if (!function_exists('renderSlider')) {
function renderSlider($props) {
$slides = $props['slides'] ?? [];
$autoplay = $props['autoplay'] ?? true;
$interval = $props['interval'] ?? 5000;
$showArrows = $props['showArrows'] ?? true;
$showDots = $props['showDots'] ?? true;
$showCaptions = $props['showCaptions'] ?? true;
$height = $props['height'] ?? '500px';
$overlayOpacity = $props['overlayOpacity'] ?? 0.3;
// Generate unique ID for this carousel
$carouselId = 'carousel-' . uniqid();
$spacingStyles = getSpacingStyles($props);
$styleAttr = !empty($spacingStyles) ? sprintf(' style="%s"', implode('; ', $spacingStyles)) : '';
$html = sprintf(
'',
$carouselId,
$autoplay ? 'carousel' : 'false',
$interval,
$styleAttr
);
// Carousel indicators (dots)
if ($showDots && count($slides) > 1) {
$html .= '
';
foreach ($slides as $index => $slide) {
$html .= sprintf(
'',
$carouselId,
$index,
$index === 0 ? 'class="active" aria-current="true"' : '',
$index + 1
);
}
$html .= '
';
}
// Carousel inner (slides)
$html .= '
';
foreach ($slides as $index => $slide) {
$image = $slide['image'] ?? '';
$title = $slide['title'] ?? '';
$description = $slide['description'] ?? '';
$html .= sprintf(
'
',
$index === 0 ? 'active' : '',
$height
);
// Background image
$html .= sprintf(
'
',
htmlspecialchars($image)
);
// Overlay
$html .= sprintf(
'
',
$overlayOpacity
);
// Caption
if ($showCaptions) {
$html .= '
';
$html .= '
';
$html .= sprintf(
'
%s
',
htmlspecialchars($title)
);
$html .= sprintf(
'
%s
',
htmlspecialchars($description)
);
$html .= '
';
$html .= '
';
}
$html .= '
';
}
$html .= '
';
// Carousel controls (arrows)
if ($showArrows && count($slides) > 1) {
$html .= sprintf(
'
',
$carouselId
);
$html .= sprintf(
'
',
$carouselId
);
}
$html .= '
';
return $html;
}
}
/**
* Render Accordion Component
*/
if (!function_exists('renderAccordion')) {
function renderAccordion($props) {
$items = $props['items'] ?? [];
$allowMultipleOpen = $props['allowMultipleOpen'] ?? false;
$defaultOpenIndex = $props['defaultOpenIndex'] ?? 0;
$borderColor = $props['borderColor'] ?? '#dee2e6';
$headerBg = $props['headerBg'] ?? '#f8f9fa';
$headerTextColor = $props['headerTextColor'] ?? '#212529';
$contentBg = $props['contentBg'] ?? '#ffffff';
$contentTextColor = $props['contentTextColor'] ?? '#212529';
$accordionId = 'accordion-' . uniqid();
$spacingStyles = getSpacingStyles($props);
$styleAttr = !empty($spacingStyles) ? sprintf(' style="%s"', implode('; ', $spacingStyles)) : '';
$html = sprintf('', $accordionId, $styleAttr);
foreach ($items as $index => $item) {
$itemId = $accordionId . '-item-' . $index;
$isOpen = $index == $defaultOpenIndex;
$html .= '
';
// Header
$html .= sprintf(
'',
$isOpen ? '' : ' collapsed',
$itemId,
$headerBg,
$headerTextColor,
htmlspecialchars($item['title'] ?? '')
);
// Content
$html .= sprintf(
'
',
$itemId,
$isOpen ? ' show' : '',
$allowMultipleOpen ? '' : '#' . $accordionId,
$contentBg,
$contentTextColor,
nl2br(htmlspecialchars($item['content'] ?? ''))
);
$html .= '
';
}
$html .= '
';
return $html;
}
}
/**
* Render Tabs Component
*/
if (!function_exists('renderTabs')) {
function renderTabs($props) {
$tabs = $props['tabs'] ?? [];
$defaultActiveTab = $props['defaultActiveTab'] ?? 0;
$tabPosition = $props['tabPosition'] ?? 'top';
$activeColor = $props['activeColor'] ?? '#667eea';
$inactiveColor = $props['inactiveColor'] ?? '#6c757d';
$borderColor = $props['borderColor'] ?? '#dee2e6';
$contentBg = $props['contentBg'] ?? '#ffffff';
$contentPadding = $props['contentPadding'] ?? '20px';
$tabsId = 'tabs-' . uniqid();
$isHorizontal = $tabPosition === 'top';
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = 'display: ' . ($isHorizontal ? 'block' : 'flex');
$spacingStyles[] = 'gap: ' . ($isHorizontal ? '0' : '20px');
$html = '';
// Tab nav
$html .= '
';
foreach ($tabs as $index => $tab) {
$isActive = $index == $defaultActiveTab;
$html .= sprintf(
'-
',
$isActive ? ' active' : '',
$tabsId,
$index,
$isActive ? $activeColor : $inactiveColor,
htmlspecialchars($tab['title'] ?? '')
);
}
$html .= '
';
// Tab content
$html .= '
';
foreach ($tabs as $index => $tab) {
$isActive = $index == $defaultActiveTab;
$html .= sprintf(
'
%s
',
$isActive ? ' show active' : '',
$tabsId,
$index,
nl2br(htmlspecialchars($tab['content'] ?? ''))
);
}
$html .= '
';
$html .= '
';
return $html;
}
}
/**
* Render Gallery Component
*/
if (!function_exists('renderGallery')) {
function renderGallery($props) {
$images = $props['images'] ?? [];
$columns = $props['columns'] ?? 3;
$gap = $props['gap'] ?? '15px';
$borderRadius = $props['borderRadius'] ?? '8px';
$showCaptions = $props['showCaptions'] ?? true;
$galleryId = 'gallery-' . uniqid();
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = 'display: grid';
$spacingStyles[] = 'grid-template-columns: repeat(' . $columns . ', 1fr)';
$spacingStyles[] = 'gap: ' . $gap;
$html = '';
foreach ($images as $index => $image) {
$html .= '
';
$html .= sprintf(
'

',
htmlspecialchars($image['url'] ?? ''),
htmlspecialchars($image['caption'] ?? '')
);
if ($showCaptions && !empty($image['caption'])) {
$html .= sprintf(
'
%s
',
htmlspecialchars($image['caption'])
);
}
$html .= '
';
// Modal for lightbox
$html .= sprintf(
'

%s
',
$galleryId,
$index,
htmlspecialchars($image['url'] ?? ''),
htmlspecialchars($image['caption'] ?? ''),
!empty($image['caption']) ? '
' . htmlspecialchars($image['caption']) . '
' : ''
);
}
$html .= '
';
return $html;
}
}
/**
* Render Video Component
*/
if (!function_exists('renderVideo')) {
function renderVideo($props) {
$videoUrl = $props['videoUrl'] ?? '';
$aspectRatio = $props['aspectRatio'] ?? '16:9';
$autoplay = $props['autoplay'] ?? false;
$controls = $props['controls'] ?? true;
$muted = $props['muted'] ?? false;
$loop = $props['loop'] ?? false;
$borderRadius = $props['borderRadius'] ?? '8px';
// Calculate padding for aspect ratio
$ratioMap = [
'16:9' => '56.25%',
'4:3' => '75%',
'1:1' => '100%',
'21:9' => '42.86%'
];
$paddingTop = $ratioMap[$aspectRatio] ?? '56.25%';
$embedUrl = '';
// YouTube
if (preg_match('/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i', $videoUrl, $matches)) {
$videoId = $matches[1];
$params = http_build_query([
'autoplay' => $autoplay ? '1' : '0',
'controls' => $controls ? '1' : '0',
'mute' => $muted ? '1' : '0',
'loop' => $loop ? '1' : '0',
'playlist' => $loop ? $videoId : ''
]);
$embedUrl = "https://www.youtube.com/embed/{$videoId}?{$params}";
}
// Vimeo
elseif (preg_match('/vimeo\.com\/(?:video\/)?(\d+)/i', $videoUrl, $matches)) {
$videoId = $matches[1];
$params = http_build_query([
'autoplay' => $autoplay ? '1' : '0',
'controls' => $controls ? '1' : '0',
'muted' => $muted ? '1' : '0',
'loop' => $loop ? '1' : '0'
]);
$embedUrl = "https://player.vimeo.com/video/{$videoId}?{$params}";
}
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = 'position: relative';
$spacingStyles[] = 'width: 100%';
$spacingStyles[] = 'padding-top: ' . $paddingTop;
$spacingStyles[] = 'border-radius: ' . $borderRadius;
$spacingStyles[] = 'overflow: hidden';
$spacingStyles[] = 'background-color: #000';
$html = '';
if ($embedUrl) {
$html .= sprintf(
'
',
$embedUrl
);
} else {
$html .= '
';
}
$html .= '
';
return $html;
}
}
/**
* Render Form Component
*/
if (!function_exists('renderForm')) {
function renderForm($props) {
$formTitle = $props['formTitle'] ?? 'Contact Us';
$formDescription = $props['formDescription'] ?? '';
$fields = $props['fields'] ?? [];
$submitButtonText = $props['submitButtonText'] ?? 'Send Message';
$submitButtonColor = $props['submitButtonColor'] ?? '#667eea';
$backgroundColor = $props['backgroundColor'] ?? '#f8f9fa';
$padding = $props['padding'] ?? '30px';
$borderRadius = $props['borderRadius'] ?? '8px';
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = 'background-color: ' . $backgroundColor;
$spacingStyles[] = 'border-radius: ' . $borderRadius;
// Note: padding from props overrides the $padding variable if set
if (!isset($props['padding'])) {
$spacingStyles[] = 'padding: ' . $padding;
}
$html = '';
return $html;
}
}
/**
* Render Testimonials Component
*/
if (!function_exists('renderTestimonials')) {
function renderTestimonials($props) {
$testimonials = $props['testimonials'] ?? [];
$layout = $props['layout'] ?? 'card';
$backgroundColor = $props['backgroundColor'] ?? '#f8f9fa';
$textColor = $props['textColor'] ?? '#212529';
$accentColor = $props['accentColor'] ?? '#667eea';
if (empty($testimonials)) {
return '';
}
$carouselId = 'testimonials-' . uniqid();
$spacingStyles = getSpacingStyles($props);
$html = '';
$html .= '
';
// Carousel inner
$html .= '
';
foreach ($testimonials as $index => $testimonial) {
$isActive = $index === 0;
$html .= '
';
if ($layout === 'card') {
$html .= '
';
$html .= '
![' . htmlspecialchars($testimonial['name'] ?? '') . '](' . htmlspecialchars($testimonial['image'] ?? '') . ')
';
// Stars
$rating = $testimonial['rating'] ?? 5;
$html .= '
';
for ($i = 0; $i < 5; $i++) {
$html .= '';
}
$html .= '
';
$html .= '
"' . htmlspecialchars($testimonial['text'] ?? '') . '"
';
$html .= '
' . htmlspecialchars($testimonial['name'] ?? '') . '
';
$html .= '
' . htmlspecialchars($testimonial['role'] ?? '') . '
';
$html .= '
';
} else {
$html .= '
';
$html .= '
';
// Stars
$rating = $testimonial['rating'] ?? 5;
$html .= '
';
for ($i = 0; $i < 5; $i++) {
$html .= '';
}
$html .= '
';
$html .= '
"' . htmlspecialchars($testimonial['text'] ?? '') . '"
';
$html .= '
';
$html .= '
![' . htmlspecialchars($testimonial['name'] ?? '') . '](' . htmlspecialchars($testimonial['image'] ?? '') . ')
';
$html .= '
';
$html .= '
' . htmlspecialchars($testimonial['name'] ?? '') . '
';
$html .= '
' . htmlspecialchars($testimonial['role'] ?? '') . '
';
$html .= '
';
$html .= '
';
}
$html .= '
';
}
$html .= '
';
// Carousel controls
if (count($testimonials) > 1) {
$html .= '
';
$html .= '
';
}
$html .= '
';
return $html;
}
}
/**
* Render Pricing Table Component
*/
if (!function_exists('renderPricingTable')) {
function renderPricingTable($props) {
$plans = $props['plans'] ?? [];
$accentColor = $props['accentColor'] ?? '#667eea';
$cardBg = $props['cardBg'] ?? '#ffffff';
$highlightedBg = $props['highlightedBg'] ?? '#667eea';
$highlightedTextColor = $props['highlightedTextColor'] ?? '#ffffff';
if (empty($plans)) {
return '';
}
$columns = min(count($plans), 3);
$spacingStyles = getSpacingStyles($props);
$html = '';
$html .= '
';
foreach ($plans as $plan) {
$isHighlighted = $plan['highlighted'] ?? false;
$bgColor = $isHighlighted ? $highlightedBg : $cardBg;
$textColor = $isHighlighted ? $highlightedTextColor : '#212529';
$html .= '
';
if ($isHighlighted) {
$html .= '
Popular
';
}
$html .= '
' . htmlspecialchars($plan['name'] ?? '') . '
';
$html .= '
' . htmlspecialchars($plan['description'] ?? '') . '
';
$html .= '
';
$html .= '$';
$html .= '' . htmlspecialchars($plan['price'] ?? '0') . '';
$html .= '/' . htmlspecialchars($plan['period'] ?? 'month') . '';
$html .= '
';
$html .= '
';
foreach ($plan['features'] ?? [] as $feature) {
$html .= '- ';
$html .= '';
$html .= '' . htmlspecialchars($feature) . '';
$html .= '
';
}
$html .= '
';
$buttonBg = $isHighlighted ? 'white' : $accentColor;
$buttonColor = $isHighlighted ? $accentColor : 'white';
$html .= '
';
$html .= '
';
}
$html .= '
';
return $html;
}
}
/**
* Render Team Members Component
*/
if (!function_exists('renderTeamMembers')) {
function renderTeamMembers($props) {
$members = $props['members'] ?? [];
$columns = $props['columns'] ?? 4;
$cardStyle = $props['cardStyle'] ?? 'modern';
$showBio = $props['showBio'] ?? true;
$showSocial = $props['showSocial'] ?? true;
$accentColor = $props['accentColor'] ?? '#667eea';
$cardBg = $props['cardBg'] ?? '#ffffff';
if (empty($members)) {
return '';
}
$spacingStyles = getSpacingStyles($props);
$html = '';
$html .= '
';
foreach ($members as $member) {
$textAlign = $cardStyle === 'modern' ? 'center' : 'left';
$html .= '
';
// Image
$paddingTop = $cardStyle === 'modern' ? '100%' : '120%';
$html .= '
';
$html .= '
![' . htmlspecialchars($member['name'] ?? '') . '](' . htmlspecialchars($member['image'] ?? '') . ')
';
$html .= '
';
// Content
$html .= '
';
$html .= '
' . htmlspecialchars($member['name'] ?? '') . '
';
$html .= '
' . htmlspecialchars($member['role'] ?? '') . '
';
if ($showBio && !empty($member['bio'])) {
$html .= '
' . htmlspecialchars($member['bio']) . '
';
}
if ($showSocial && !empty($member['social'])) {
$html .= '
';
foreach ($member['social'] as $platform => $url) {
if ($url) {
$iconMap = [
'linkedin' => 'bi-linkedin',
'twitter' => 'bi-twitter',
'facebook' => 'bi-facebook',
'github' => 'bi-github',
'email' => 'bi-envelope-fill',
'dribbble' => 'bi-dribbble',
'behance' => 'bi-behance',
'instagram' => 'bi-instagram'
];
$icon = $iconMap[$platform] ?? 'bi-link-45deg';
$html .= '
';
}
}
$html .= '
';
}
$html .= '
';
$html .= '
';
}
$html .= '
';
return $html;
}
}
/**
* Render Stats Component
*/
if (!function_exists('renderStats')) {
function renderStats($props) {
$stats = $props['stats'] ?? [];
$layout = $props['layout'] ?? 'horizontal';
$backgroundColor = $props['backgroundColor'] ?? 'transparent';
$cardStyle = $props['cardStyle'] ?? 'modern';
$textAlign = $props['textAlign'] ?? 'center';
if (empty($stats)) {
return '';
}
$columns = $layout === 'grid' ? min(count($stats), 4) : ($layout === 'horizontal' ? count($stats) : 1);
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = 'background-color: ' . $backgroundColor;
$html = '';
$html .= '
';
foreach ($stats as $stat) {
$padding = $cardStyle === 'modern' ? '30px 20px' : '20px';
$bgColor = $cardStyle === 'modern' ? 'rgba(255, 255, 255, 0.9)' : 'transparent';
$borderRadius = $cardStyle === 'modern' ? '12px' : '0';
$border = $cardStyle === 'bordered' ? '2px solid #e0e0e0' : 'none';
$boxShadow = $cardStyle === 'modern' ? '0 4px 6px rgba(0,0,0,0.1)' : 'none';
$html .= '
';
if (!empty($stat['icon'])) {
$html .= '
';
}
$html .= '
' . htmlspecialchars($stat['number']) . '
';
$html .= '
' . htmlspecialchars($stat['label']) . '
';
$html .= '
';
}
$html .= '
';
return $html;
}
}
/**
* Render Timeline Component
*/
if (!function_exists('renderTimeline')) {
function renderTimeline($props) {
$events = $props['events'] ?? [];
$orientation = $props['orientation'] ?? 'vertical';
$accentColor = $props['accentColor'] ?? '#667eea';
$lineColor = $props['lineColor'] ?? '#dee2e6';
$iconBg = $props['iconBg'] ?? '#ffffff';
$showIcons = $props['showIcons'] ?? true;
if (empty($events)) {
return '';
}
$spacingStyles = getSpacingStyles($props);
$html = '';
if ($orientation === 'vertical') {
$html .= '
';
$html .= '
';
foreach ($events as $index => $event) {
$html .= '
';
// Icon
$html .= '
';
if ($showIcons && !empty($event['icon'])) {
$html .= '';
}
$html .= '
';
// Content
$html .= '
';
$html .= '
' . htmlspecialchars($event['date']) . '
';
$html .= '
' . htmlspecialchars($event['title']) . '
';
$html .= '
' . htmlspecialchars($event['description']) . '
';
$html .= '
';
$html .= '
';
}
$html .= '
';
} else {
// Horizontal
$html .= '
';
$html .= '
';
$html .= '
';
foreach ($events as $event) {
$html .= '
';
// Icon
$html .= '
';
if ($showIcons && !empty($event['icon'])) {
$html .= '';
}
$html .= '
';
$html .= '
' . htmlspecialchars($event['date']) . '
';
$html .= '
' . htmlspecialchars($event['title']) . '
';
$html .= '
' . htmlspecialchars($event['description']) . '
';
$html .= '
';
}
$html .= '
';
}
$html .= '
';
return $html;
}
}
/**
* Render Map Component
*/
if (!function_exists('renderMap')) {
function renderMap($props) {
$location = $props['location'] ?? 'New York, NY, USA';
$mapUrl = $props['mapUrl'] ?? '';
$height = $props['height'] ?? '450px';
$zoom = $props['zoom'] ?? '14';
$borderRadius = $props['borderRadius'] ?? '8px';
$showDirections = $props['showDirections'] ?? true;
$embedUrl = $mapUrl ?: 'https://www.google.com/maps?q=' . urlencode($location) . '&output=embed&z=' . $zoom;
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = 'position: relative';
$spacingStyles[] = 'width: 100%';
$spacingStyles[] = 'height: ' . $height;
$spacingStyles[] = 'border-radius: ' . $borderRadius;
$spacingStyles[] = 'overflow: hidden';
$spacingStyles[] = 'box-shadow: 0 2px 8px rgba(0,0,0,0.1)';
$html = '';
$html .= '
';
if ($showDirections && $location) {
$directionsUrl = 'https://www.google.com/maps/dir/?api=1&destination=' . urlencode($location);
$html .= '
Get Directions';
}
$html .= '
';
return $html;
}
}
/**
* Render CTA Banner Component
*/
if (!function_exists('renderCTABanner')) {
function renderCTABanner($props) {
$heading = $props['heading'] ?? 'Ready to Get Started?';
$subheading = $props['subheading'] ?? '';
$buttonText = $props['buttonText'] ?? 'Get Started Now';
$buttonLink = $props['buttonLink'] ?? '#';
$secondaryButtonText = $props['secondaryButtonText'] ?? '';
$secondaryButtonLink = $props['secondaryButtonLink'] ?? '#';
$layout = $props['layout'] ?? 'center';
$backgroundColor = $props['backgroundColor'] ?? '#667eea';
$backgroundImage = $props['backgroundImage'] ?? '';
$textColor = $props['textColor'] ?? '#ffffff';
$buttonStyle = $props['buttonStyle'] ?? 'solid';
$buttonColor = $props['buttonColor'] ?? '#ffffff';
$buttonTextColor = $props['buttonTextColor'] ?? '#667eea';
$padding = $props['padding'] ?? '60px 20px';
$borderRadius = $props['borderRadius'] ?? '12px';
$overlayOpacity = $props['overlayOpacity'] ?? 0.8;
$hasBackgroundImage = !empty($backgroundImage);
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = 'position: relative';
$spacingStyles[] = 'border-radius: ' . $borderRadius;
$spacingStyles[] = 'overflow: hidden';
if ($hasBackgroundImage) {
$spacingStyles[] = 'background-image: url(' . $backgroundImage . ')';
$spacingStyles[] = 'background-size: cover';
$spacingStyles[] = 'background-position: center';
} else {
$spacingStyles[] = 'background-color: ' . $backgroundColor;
}
$html = '';
if ($hasBackgroundImage) {
$html .= '
';
}
$textAlign = $layout === 'center' ? 'center' : 'left';
$display = $layout === 'split' ? 'flex' : 'block';
$maxWidth = $layout === 'center' ? '800px' : '100%';
$html .= '
';
$html .= '
';
$html .= '
' . htmlspecialchars($heading) . '
';
if ($subheading) {
$html .= '
' . htmlspecialchars($subheading) . '
';
}
$html .= '
';
$html .= '
';
$html .= '
';
$html .= '
';
return $html;
}
}
/**
* Render Divider Component
*/
if (!function_exists('renderDivider')) {
function renderDivider($props) {
$style = $props['style'] ?? 'solid';
$width = $props['width'] ?? '100%';
$thickness = $props['thickness'] ?? '2px';
$color = $props['color'] ?? '#dee2e6';
$gradientStart = $props['gradientStart'] ?? '#667eea';
$gradientEnd = $props['gradientEnd'] ?? '#f093fb';
$marginTop = $props['marginTop'] ?? '30px';
$marginBottom = $props['marginBottom'] ?? '30px';
$alignment = $props['alignment'] ?? 'center';
$maxWidth = $props['maxWidth'] ?? '100%';
$showIcon = $props['showIcon'] ?? false;
$icon = $props['icon'] ?? 'bi-circle-fill';
$iconColor = $props['iconColor'] ?? '#667eea';
$justifyContent = $alignment === 'left' ? 'flex-start' : ($alignment === 'right' ? 'flex-end' : 'center');
$html = '';
if (!$showIcon) {
if ($style === 'gradient') {
$html .= '
';
} else {
$borderStyle = $style === 'solid' ? 'none' : $thickness . ' ' . $style . ' ' . $color;
$bgColor = $style === 'solid' ? $color : 'transparent';
$html .= '
';
}
} else {
$html .= '
';
if ($style === 'gradient') {
$html .= '
';
} else {
$borderStyle = $style === 'solid' ? 'none' : $thickness . ' ' . $style . ' ' . $color;
$bgColor = $style === 'solid' ? $color : 'transparent';
$html .= '
';
}
$html .= '
';
if ($style === 'gradient') {
$html .= '
';
} else {
$html .= '
';
}
$html .= '
';
}
$html .= '
';
return $html;
}
}
/**
* Render Spacer Component
*/
if (!function_exists('renderSpacer')) {
function renderSpacer($props) {
$height = $props['height'] ?? '50px';
$backgroundColor = $props['backgroundColor'] ?? 'transparent';
return '';
}
/**
* Render Menu Component
*/
function renderMenu($props, $websiteId = null) {
// Get menu from website
$menu = null;
if ($websiteId && isset($props['menuId']) && $props['menuId']) {
$menu = \App\Models\Menu::where('id', $props['menuId'])
->where('website_id', $websiteId)
->with('items')
->first();
}
// If no menu found, show message in builder preview
if (!$menu || !$menu->items->count()) {
return 'No menu items found. Please create a menu in Menu Management.
';
}
// Extract props
$layout = $props['layout'] ?? 'horizontal';
$alignment = $props['alignment'] ?? 'left';
$pointer = $props['pointer'] ?? 'underline';
$animation = $props['animation'] ?? 'fade';
$textColor = $props['textColor'] ?? '#000000';
$hoverColor = $props['hoverColor'] ?? '#667eea';
$activeColor = $props['activeColor'] ?? '#667eea';
$backgroundColor = $props['backgroundColor'] ?? 'transparent';
$submenuBgColor = $props['submenuBgColor'] ?? '#ffffff';
$padding = $props['padding'] ?? '10px 15px';
$gap = $props['gap'] ?? '20px';
$fontSize = $props['fontSize'] ?? '16px';
$fontWeight = $props['fontWeight'] ?? '500';
$mobileBreakpoint = $props['mobileBreakpoint'] ?? 768;
$submenuIndicator = $props['submenuIndicator'] ?? true;
// Generate unique ID for this menu instance
$menuId = 'menu-' . uniqid();
// Build menu HTML
$html = '';
// Add CSS for hover effects and pointer styles
$html .= '';
// Add JavaScript for mobile toggle
$html .= '';
return $html;
}
}
/**
* Render Product Grid component - E-commerce Module
*/
if (!function_exists('renderProductGrid')) {
function renderProductGrid($props, $websiteId = null, $currencySymbol = 'RM', $websiteSubdomain = null) {
// Check if Product model exists
if (!class_exists('\App\Models\Product')) {
return 'E-commerce module not available
';
}
$displayType = $props['displayType'] ?? 'by_category';
$columns = $props['columns'] ?? 3;
$showPrice = $props['showPrice'] ?? true;
$showDescription = $props['showDescription'] ?? true;
$showAddToCart = $props['showAddToCart'] ?? true;
$categoryFilter = $props['categoryFilter'] ?? 'all';
$limit = $props['limit'] ?? 12;
$sortBy = $props['sortBy'] ?? 'newest';
$cardStyle = $props['cardStyle'] ?? 'modern';
$spacingStyles = getSpacingStyles($props);
// Fetch products from database
$query = \App\Models\Product::where('is_active', true);
// IMPORTANT: Filter by website_id for multi-tenancy
if ($websiteId) {
$query->where('website_id', $websiteId);
}
// Configure based on display type
switch ($displayType) {
case 'featured':
$query->where('featured', true);
break;
case 'best_sellers':
// Order by popularity if available
$query->orderBy('created_at', 'desc');
break;
case 'latest':
$query->orderBy('created_at', 'desc');
break;
case 'by_category':
default:
// Filter by category using relationship
if ($categoryFilter !== 'all') {
$query->whereHas('categories', function($q) use ($categoryFilter) {
$q->where('slug', $categoryFilter);
});
}
break;
}
// Apply sorting (only if not best_sellers or latest which have their own)
if ($displayType !== 'best_sellers' && $displayType !== 'latest') {
switch ($sortBy) {
case 'newest':
$query->orderBy('created_at', 'desc');
break;
case 'oldest':
$query->orderBy('created_at', 'asc');
break;
case 'price_low':
$query->orderBy('price', 'asc');
break;
case 'price_high':
$query->orderBy('price', 'desc');
break;
case 'name':
$query->orderBy('name', 'asc');
break;
}
}
$products = $query->limit($limit)->get();
if ($products->isEmpty()) {
$html = '';
$html .= '
';
$html .= '
';
$html .= '
No Products Found
';
$html .= '
Add products in your e-commerce dashboard to display them here.
';
$html .= '
';
return $html;
}
$html = '';
$html .= '
';
foreach ($products as $product) {
$cardBg = $cardStyle === 'modern' ? '#fff' : 'transparent';
$borderRadius = $cardStyle === 'modern' ? '12px' : '0';
$boxShadow = $cardStyle === 'modern' ? '0 2px 8px rgba(0,0,0,0.1)' : 'none';
$border = $cardStyle === 'bordered' ? '1px solid #dee2e6' : 'none';
// Product URL - use subdomain if available
if ($websiteSubdomain) {
$mainDomain = config('app.domain', 'neosolvix.test');
$productUrl = 'http://' . $websiteSubdomain . '.' . $mainDomain . '/product/' . $product->slug;
} else {
$productUrl = '/product/' . $product->slug;
}
$html .= '
';
$html .= '';
// Product Image - use main_image accessor which gets first image from images array
$imageUrl = $product->main_image ?? 'https://via.placeholder.com/400x400/667eea/ffffff?text=Product';
// If it's already a full path, use it as-is
if ($product->main_image && !str_starts_with($product->main_image, 'http') && !str_starts_with($product->main_image, '/')) {
$imageUrl = '/storage/' . $product->main_image;
}
$html .= '
';
$html .= '
 . ')
';
if ($product->stock_quantity == 0) {
$html .= '
Out of Stock
';
}
$html .= '
';
// Product Info
$html .= '
';
$html .= '
' . htmlspecialchars($product->name) . '
';
if ($showDescription && $product->description) {
$html .= '
' . htmlspecialchars(substr($product->description, 0, 100)) . '
';
}
if ($showPrice) {
$html .= '
';
if ($product->sale_price) {
$html .= '' . htmlspecialchars($currencySymbol) . number_format($product->sale_price, 2) . '';
$html .= '' . htmlspecialchars($currencySymbol) . number_format($product->price, 2) . '';
} else {
$html .= '' . htmlspecialchars($currencySymbol) . number_format($product->price, 2) . '';
}
$html .= '
';
}
if ($showAddToCart) {
$disabled = $product->stock_quantity == 0 ? 'disabled' : '';
$buttonText = $product->stock_quantity == 0 ? 'Out of Stock' : 'View Details';
$html .= '
';
}
$html .= '
';
}
$html .= '
';
return $html;
}
}
/**
* Render Booking Form component - Booking Module
*/
if (!function_exists('renderBookingForm')) {
function renderBookingForm($props, $websiteId = null, $currencySymbol = 'RM') {
$formTitle = $props['formTitle'] ?? 'Book an Appointment';
$formDescription = $props['formDescription'] ?? 'Select a service and choose your preferred date and time.';
$showServiceSelection = $props['showServiceSelection'] ?? true;
$showDatePicker = $props['showDatePicker'] ?? true;
$showTimePicker = $props['showTimePicker'] ?? true;
$submitButtonText = $props['submitButtonText'] ?? 'Book Now';
$submitButtonColor = $props['submitButtonColor'] ?? '#667eea';
$backgroundColor = $props['backgroundColor'] ?? '#f8f9fa';
$borderRadius = $props['borderRadius'] ?? '8px';
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = 'background-color: ' . $backgroundColor;
$spacingStyles[] = 'border-radius: ' . $borderRadius;
// Time slots
$timeSlots = [];
for ($hour = 9; $hour <= 17; $hour++) {
$timeSlots[] = str_pad($hour, 2, '0', STR_PAD_LEFT) . ':00';
if ($hour < 17) {
$timeSlots[] = str_pad($hour, 2, '0', STR_PAD_LEFT) . ':30';
}
}
$html = '';
return $html;
}
}
/**
* Render Payment Form component - Payment Forms Module
*/
if (!function_exists('renderPaymentForm')) {
function renderPaymentForm($props) {
$formTitle = $props['formTitle'] ?? 'Make a Payment';
$formDescription = $props['formDescription'] ?? 'Enter the amount and complete your payment securely.';
$showAmountField = $props['showAmountField'] ?? true;
$fixedAmount = $props['fixedAmount'] ?? null;
$amountLabel = $props['amountLabel'] ?? 'Amount';
$currency = $props['currency'] ?? 'USD';
$currencySymbol = $props['currencySymbol'] ?? '$';
$showPurposeField = $props['showPurposeField'] ?? true;
$purposeLabel = $props['purposeLabel'] ?? 'Payment For';
$submitButtonText = $props['submitButtonText'] ?? 'Pay Now';
$submitButtonColor = $props['submitButtonColor'] ?? '#667eea';
$backgroundColor = $props['backgroundColor'] ?? '#f8f9fa';
$borderRadius = $props['borderRadius'] ?? '8px';
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = 'background-color: ' . $backgroundColor;
$spacingStyles[] = 'border-radius: ' . $borderRadius;
$html = '';
return $html;
}
}
/**
* Render Posts Component
*/
if (!function_exists('renderPosts')) {
function renderPosts($props, $websiteId = null, $websiteSubdomain = null) {
// Debug log
\Log::info('renderPosts called', ['websiteId' => $websiteId, 'subdomain' => $websiteSubdomain, 'props' => $props]);
// Check if Post model exists
if (!class_exists('\App\Models\Post')) {
\Log::warning('Post model does not exist');
return 'Posts component requires Posts module
';
}
// Get props
$filterType = $props['filterType'] ?? 'latest';
$categoryFilter = $props['categoryFilter'] ?? null;
$tagFilter = $props['tagFilter'] ?? null;
$displayType = $props['displayType'] ?? 'grid';
$postsPerRow = $props['postsPerRow'] ?? 3;
$limit = $props['limit'] ?? 6;
$excerptLength = $props['excerptLength'] ?? 120;
$showFeaturedImage = $props['showFeaturedImage'] ?? true;
$showExcerpt = $props['showExcerpt'] ?? true;
$showAuthor = $props['showAuthor'] ?? true;
$showDate = $props['showDate'] ?? true;
$showCategory = $props['showCategory'] ?? true;
$showReadMore = $props['showReadMore'] ?? true;
// Get spacing styles
$spacingStyles = getSpacingStyles($props);
// Build query
$query = \App\Models\Post::where('website_id', $websiteId)
->published()
->with(['author', 'category', 'tags']);
// Apply filters
switch ($filterType) {
case 'featured':
$query->featured();
break;
case 'category':
if ($categoryFilter) {
$query->where('post_category_id', $categoryFilter);
}
break;
case 'tag':
if ($tagFilter) {
$query->whereHas('tags', function($q) use ($tagFilter) {
$q->where('post_tags.id', $tagFilter);
});
}
break;
case 'latest':
default:
// Latest is default, no additional filter needed
break;
}
// Order and limit
$posts = $query->latest()->limit($limit)->get();
// Debug log
\Log::info('Posts query result', ['count' => $posts->count(), 'posts' => $posts->pluck('title')->toArray()]);
// Start HTML
$html = '';
if ($posts->isEmpty()) {
$html .= '
No posts found.
';
} else {
// Container class based on layout
$containerClass = $displayType === 'grid' ? 'row g-4' : 'posts-list';
$html .= '
';
foreach ($posts as $post) {
// Column class for grid layout
if ($displayType === 'grid') {
$colSize = 12 / $postsPerRow;
$html .= '
';
}
// Card wrapper
$cardClass = $displayType === 'grid' ? 'card h-100' : 'card mb-4';
$html .= '
';
// Row for list layout
if ($displayType === 'list') {
$html .= '
';
}
// Featured Image
if ($showFeaturedImage && $post->featured_image) {
$imageColClass = $displayType === 'list' ? 'col-md-4' : '';
if ($imageColClass) {
$html .= '
';
}
// Get the correct image URL (add /storage/ prefix if not already a full URL)
$imageUrl = $post->featured_image;
if (!str_starts_with($imageUrl, 'http') && !str_starts_with($imageUrl, '/storage/')) {
$imageUrl = '/storage/' . $imageUrl;
}
$imageHeight = $displayType === 'grid' ? '220px' : '100%';
$html .= '
 . ')
title) . '" ';
$html .= 'style="height: ' . $imageHeight . '; object-fit: cover;">';
if ($imageColClass) {
$html .= '
';
}
}
// Post Content
$contentColClass = ($displayType === 'list' && $showFeaturedImage && $post->featured_image) ? 'col-md-8' : '';
if ($contentColClass) {
$html .= '
';
}
$html .= '
';
// Category Badge
if ($showCategory && $post->category) {
$html .= '
' . htmlspecialchars($post->category->name) . '';
}
// Post Title
$html .= '
' . htmlspecialchars($post->title) . '
';
// Post Meta
$html .= '
';
if ($showAuthor && $post->author) {
$html .= ' ' . htmlspecialchars($post->author->name) . '';
}
if ($showDate) {
$html .= ' ' . $post->published_date . '';
}
$html .= ' ' . $post->views_count . ' views';
$html .= ' ' . $post->reading_time . ' min';
$html .= '
';
// Excerpt
if ($showExcerpt) {
$excerpt = $post->excerpt ?: strip_tags($post->content);
$excerpt = substr($excerpt, 0, $excerptLength);
if (strlen($post->excerpt ?: strip_tags($post->content)) > $excerptLength) {
$excerpt .= '...';
}
$html .= '
' . htmlspecialchars($excerpt) . '
';
}
// Read More Button
if ($showReadMore) {
// Generate correct post URL with subdomain
$postUrl = $websiteSubdomain ? '/' . $websiteSubdomain . '/post/' . $post->slug : '/posts/' . $post->slug;
$html .= '
';
$html .= 'Read More ';
$html .= '';
}
$html .= '
'; // End card-body
if ($contentColClass) {
$html .= '
'; // End content column
}
if ($displayType === 'list') {
$html .= '
'; // End row g-0
}
$html .= '
'; // End card
if ($displayType === 'grid') {
$html .= '
'; // End column
}
}
$html .= '
'; // End container
}
$html .= '
'; // End posts-component
// Add hover styles
$html .= '';
return $html;
}
}
/**
* Render FeaturedProduct Component
*/
if (!function_exists('renderFeaturedProduct')) {
function renderFeaturedProduct($props, $websiteId = null, $currencySymbol = 'RM') {
// Check if Product model exists
if (!class_exists('\App\Models\Product')) {
return 'Featured Product component requires E-commerce module
';
}
$productId = $props['productId'] ?? null;
$layout = $props['layout'] ?? 'image-left';
$showBadge = $props['showBadge'] ?? true;
$badgeText = $props['badgeText'] ?? 'Featured';
$badgeColor = $props['badgeColor'] ?? '#ff6b6b';
$showDescription = $props['showDescription'] ?? true;
$showPrice = $props['showPrice'] ?? true;
$showSalePrice = $props['showSalePrice'] ?? true;
$buttonText = $props['buttonText'] ?? 'View Product';
$buttonColor = $props['buttonColor'] ?? '#667eea';
$backgroundColor = $props['backgroundColor'] ?? '#ffffff';
$borderRadius = $props['borderRadius'] ?? '12px';
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = 'background-color: ' . $backgroundColor;
$spacingStyles[] = 'border-radius: ' . $borderRadius;
// Fetch product
$product = null;
if ($productId) {
$query = \App\Models\Product::where('id', $productId);
// IMPORTANT: Filter by website_id for multi-tenancy
if ($websiteId) {
$query->where('website_id', $websiteId);
}
$product = $query->first();
}
// Use demo product if not found
if (!$product) {
$product = (object)[
'name' => 'Premium Wireless Headphones',
'description' => 'Experience crystal-clear audio with our premium wireless headphones. Featuring active noise cancellation, 30-hour battery life, and comfortable over-ear design perfect for all-day listening.',
'price' => 299.99,
'sale_price' => 249.99,
'main_image' => 'https://via.placeholder.com/800x600/667eea/ffffff?text=Featured+Product',
'stock_quantity' => 10
];
}
$displayPrice = ($showSalePrice && isset($product->sale_price) && $product->sale_price) ? $product->sale_price : $product->price;
$hasDiscount = $showSalePrice && isset($product->sale_price) && $product->sale_price && $product->sale_price < $product->price;
$isOutOfStock = ($product->stock_quantity ?? 0) <= 0;
$html = '';
// Layout wrapper
$flexDirection = $layout === 'image-left' ? 'row' : ($layout === 'image-right' ? 'row-reverse' : 'column');
if ($layout === 'image-top') {
$html .= '
';
} else {
$html .= '
';
}
// Image section
$html .= '
';
$html .= '
';
if ($showBadge && $badgeText) {
$html .= '
';
$html .= htmlspecialchars($badgeText);
$html .= '
';
}
$imageUrl = $product->main_image ?? '';
if ($imageUrl && !str_starts_with($imageUrl, 'http') && !str_starts_with($imageUrl, '/')) {
$imageUrl = '/storage/' . $imageUrl;
}
$html .= '
 . ')
';
$html .= '
';
// Content section
$html .= '
';
$html .= '
';
$html .= htmlspecialchars($product->name);
$html .= '
';
if ($showDescription && $product->description) {
$html .= '
';
$html .= htmlspecialchars($product->description);
$html .= '
';
}
if ($showPrice) {
$html .= '
';
$html .= '
';
$html .= '';
$html .= htmlspecialchars($currencySymbol) . number_format($displayPrice, 2);
$html .= '';
if ($hasDiscount) {
$html .= '';
$html .= htmlspecialchars($currencySymbol) . number_format($product->price, 2);
$html .= '';
$html .= '';
$html .= 'Save ' . htmlspecialchars($currencySymbol) . number_format($product->price - $product->sale_price, 2);
$html .= '';
}
$html .= '
';
}
if ($isOutOfStock) {
$html .= '
';
$html .= 'Out of Stock';
$html .= '
';
}
$html .= '
';
$html .= '
'; // Close content section
$html .= '
'; // Close layout wrapper
$html .= '
'; // Close component
return $html;
}
}
/**
* Render AddToCart Component
*/
if (!function_exists('renderAddToCart')) {
function renderAddToCart($props, $websiteId = null, $currencySymbol = 'RM') {
// Check if Product model exists
if (!class_exists('\App\Models\Product')) {
return '
Add to Cart component requires E-commerce module
';
}
$productId = $props['productId'] ?? null;
$showProductInfo = $props['showProductInfo'] ?? true;
$showQuantitySelector = $props['showQuantitySelector'] ?? true;
$buttonText = $props['buttonText'] ?? 'Add to Cart';
$buttonColor = $props['buttonColor'] ?? '#667eea';
$buttonSize = $props['buttonSize'] ?? 'medium';
$layout = $props['layout'] ?? 'vertical';
$backgroundColor = $props['backgroundColor'] ?? '#f8f9fa';
$borderRadius = $props['borderRadius'] ?? '8px';
$spacingStyles = getSpacingStyles($props);
$spacingStyles[] = 'background-color: ' . $backgroundColor;
$spacingStyles[] = 'border-radius: ' . $borderRadius;
$buttonSizes = [
'small' => 'padding: 8px 16px; font-size: 14px;',
'medium' => 'padding: 12px 24px; font-size: 16px;',
'large' => 'padding: 16px 32px; font-size: 18px;'
];
// Fetch product
$product = null;
if ($productId) {
$query = \App\Models\Product::where('id', $productId);
// IMPORTANT: Filter by website_id for multi-tenancy
if ($websiteId) {
$query->where('website_id', $websiteId);
}
$product = $query->first();
}
// Use demo product if not found
if (!$product) {
$product = (object)[
'name' => 'Sample Product',
'price' => 49.99,
'sale_price' => null,
'main_image' => 'https://via.placeholder.com/120x120/667eea/ffffff?text=Product',
'stock_quantity' => 10
];
}
$displayPrice = $product->sale_price ?? $product->price;
$isOutOfStock = ($product->stock_quantity ?? 0) <= 0;
$html = '
';
if ($layout === 'vertical') {
// Product Info
if ($showProductInfo) {
$imageUrl = $product->main_image ?? '';
if ($imageUrl && !str_starts_with($imageUrl, 'http') && !str_starts_with($imageUrl, '/')) {
$imageUrl = '/storage/' . $imageUrl;
}
$html .= '
';
$html .= '
 . ')
';
$html .= '
';
$html .= '
' . htmlspecialchars($product->name) . '
';
$html .= '
' . htmlspecialchars($currencySymbol) . number_format($displayPrice, 2) . '
';
$html .= '
';
}
// Quantity Selector
if ($showQuantitySelector) {
$html .= '
';
$html .= '
';
$html .= '
';
$html .= '';
$html .= '';
$html .= '';
$html .= '
';
}
// Add to Cart Button
$html .= '
';
} elseif ($layout === 'horizontal') {
// Product Info
if ($showProductInfo) {
$imageUrl = $product->main_image ?? '';
if ($imageUrl && !str_starts_with($imageUrl, 'http') && !str_starts_with($imageUrl, '/')) {
$imageUrl = '/storage/' . $imageUrl;
}
$html .= '
';
$html .= '
 . ')
';
$html .= '
';
$html .= '
' . htmlspecialchars($product->name) . '
';
$html .= '
' . htmlspecialchars($currencySymbol) . number_format($displayPrice, 2) . '
';
$html .= '
';
}
$html .= '
';
if ($showQuantitySelector) {
$html .= '
';
$html .= '
';
$html .= '
';
$html .= '';
$html .= '';
$html .= '';
$html .= '
';
}
$html .= '
';
$html .= '
';
$html .= '
';
} else { // compact
$html .= '
';
if ($showQuantitySelector) {
$html .= '
';
$html .= '';
$html .= '';
$html .= '';
$html .= '
';
}
$html .= '
';
$html .= '
';
}
$html .= '
';
return $html;
}
}
/**
* Render ResourcesList Component
*/
if (!function_exists('renderResourcesList')) {
function renderResourcesList($props, $websiteId = null, $currencySymbol = 'RM') {
// Check if BookingResource model exists
if (!class_exists('\App\Models\BookingResource')) {
return '
Resources List component requires Booking module
';
}
$displayMode = $props['displayMode'] ?? 'all';
$selectedResources = $props['selectedResources'] ?? [];
$resourceTypes = $props['resourceTypes'] ?? [];
$columns = $props['columns'] ?? 3;
$showPrice = $props['showPrice'] ?? true;
$showCapacity = $props['showCapacity'] ?? true;
$showDescription = $props['showDescription'] ?? true;
$showType = $props['showType'] ?? true;
$showBookButton = $props['showBookButton'] ?? true;
$bookButtonText = $props['bookButtonText'] ?? 'Book Now';
$bookButtonColor = $props['bookButtonColor'] ?? '#667eea';
$cardStyle = $props['cardStyle'] ?? 'modern';
$limit = $props['limit'] ?? 12;
$sortBy = $props['sortBy'] ?? 'name';
$spacingStyles = getSpacingStyles($props);
// Fetch resources based on display mode
$query = \App\Models\BookingResource::query();
// IMPORTANT: Filter by website_id for multi-tenancy
if ($websiteId) {
$query->where('website_id', $websiteId);
}
if ($displayMode === 'selected' && !empty($selectedResources)) {
$query->whereIn('id', $selectedResources);
}
if (!empty($resourceTypes)) {
$query->whereIn('resource_type', $resourceTypes);
}
// Apply sorting
switch ($sortBy) {
case 'name':
$query->orderBy('name', 'asc');
break;
case 'price_low':
$query->orderBy('base_price', 'asc');
break;
case 'price_high':
$query->orderBy('base_price', 'desc');
break;
case 'newest':
$query->orderBy('created_at', 'desc');
break;
}
$resources = $query->limit($limit)->get();
if ($resources->isEmpty()) {
return '
No Resources Available
' . ($displayMode === 'selected' && empty($selectedResources) ? 'Please select resources to display' : 'No resources found matching your criteria') . '
';
}
$html = '
';
$html .= '
';
foreach ($resources as $resource) {
$colClass = 'col-md-' . (12 / $columns);
$cardStyleCSS = match($cardStyle) {
'minimal' => 'border-radius: 8px; overflow: hidden; background-color: #f8f9fa; border: none;',
'bordered' => 'border-radius: 8px; overflow: hidden; background-color: #fff; border: 2px solid #e9ecef;',
default => 'border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); overflow: hidden; background-color: #fff; border: none;'
};
$html .= '
';
$html .= '
';
// Image
$html .= '
';
if ($resource->main_image) {
$html .= '

';
} else {
$iconClass = match($resource->resource_type) {
'property' => 'bi-house',
'court' => 'bi-dribbble',
'gym' => 'bi-heart-pulse',
'room' => 'bi-door-open',
'equipment' => 'bi-tools',
'service' => 'bi-gear',
'vehicle' => 'bi-car-front',
'facility' => 'bi-building',
'appointment' => 'bi-calendar-check',
'class' => 'bi-mortarboard',
default => 'bi-bookmark'
};
$html .= '
';
$html .= '';
$html .= '
';
}
// Status Badge
if (!$resource->is_active) {
$html .= '
Unavailable
';
}
// Type Badge
if ($showType) {
$typeDisplay = match($resource->resource_type) {
'property' => 'Property',
'court' => 'Sports Court',
'gym' => 'Gym/Fitness',
'room' => 'Room/Space',
'equipment' => 'Equipment',
'service' => 'Service',
'vehicle' => 'Vehicle',
'facility' => 'Facility',
'appointment' => 'Appointment',
'class' => 'Class/Course',
default => ucfirst($resource->resource_type)
};
$iconClass = match($resource->resource_type) {
'property' => 'bi-house',
'court' => 'bi-dribbble',
'gym' => 'bi-heart-pulse',
'room' => 'bi-door-open',
'equipment' => 'bi-tools',
'service' => 'bi-gear',
'vehicle' => 'bi-car-front',
'facility' => 'bi-building',
'appointment' => 'bi-calendar-check',
'class' => 'bi-mortarboard',
default => 'bi-bookmark'
};
$html .= '
';
$html .= '' . $typeDisplay;
$html .= '
';
}
$html .= '
'; // Close image
// Details
$html .= '
';
$html .= '
';
$html .= htmlspecialchars($resource->name);
$html .= '
';
if ($resource->resource_category) {
$html .= '
';
$html .= htmlspecialchars($resource->resource_category);
$html .= '
';
}
if ($showDescription && $resource->short_description) {
$html .= '
';
$html .= htmlspecialchars($resource->short_description);
$html .= '
';
}
// Price and meta
$html .= '
';
if ($showPrice && $resource->base_price) {
$priceType = $resource->price_type ? str_replace('per_', '', $resource->price_type) : '';
$html .= '
';
$html .= htmlspecialchars($currencySymbol) . ' ' . number_format($resource->base_price, 2);
if ($priceType) {
$html .= 'per ' . $priceType . '';
}
$html .= '
';
}
$html .= '
';
if ($showCapacity && $resource->capacity) {
$html .= '
Up to ' . $resource->capacity . ' ' . ($resource->capacity === 1 ? 'person' : 'people') . '
';
}
if ($resource->min_booking_duration) {
$html .= '
Min ' . $resource->min_booking_duration . ' min
';
}
$html .= '
';
// Amenities
if ($resource->amenities && is_array($resource->amenities)) {
$html .= '
';
foreach (array_slice($resource->amenities, 0, 3) as $amenity) {
$html .= '';
$html .= htmlspecialchars($amenity);
$html .= '';
}
if (count($resource->amenities) > 3) {
$html .= '+' . (count($resource->amenities) - 3) . ' more';
}
$html .= '
';
}
// Book Button
if ($showBookButton && $resource->is_active) {
$html .= '
';
}
$html .= '
'; // Close details
$html .= '
'; // Close card
$html .= '
'; // Close column
}
$html .= '
'; // Close row and component
return $html;
}
} // End if (!function_exists('renderNode'))