{{-- Frontend Renderer for Page Builder V2 Pages This file renders pages created with the React + Craft.js builder --}} @php \Log::info('==================== GLOBAL SECTIONS RENDER-V2 START ===================='); // Get the builder data - For Global Sections, use $section->builder_data (not builder_v2_data) $builderData = $section->builder_data ?? []; // Extract components from Craft.js format $rootNode = $builderData['ROOT'] ?? null; // Debug: Log ROOT structure if ($rootNode) { \Log::info('ROOT Node', ['type' => $rootNode['type']['resolvedName'] ?? 'unknown', 'childNodes' => $rootNode['nodes'] ?? []]); } // Get website and its settings $currentWebsite = $section->website ?? null; $currentWebsiteId = $section->website_id ?? null; $currencySymbol = $currentWebsite ? $currentWebsite->getCurrencySymbol() : 'RM'; $websiteSubdomain = $currentWebsite ? $currentWebsite->subdomain : null; /** * Recursively render a Craft.js node and its children */ 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', $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( '%s', $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( '%s', 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('%s', 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('%s', htmlspecialchars($image), htmlspecialchars($title) ); } elseif ($imagePosition === 'bottom') { $html .= sprintf('%s', 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( ''; 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( '
%s
', $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 .= ''; // 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 = ''; 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 .= '

Invalid video URL

'; } $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 = '
'; if ($formTitle) { $html .= '

' . htmlspecialchars($formTitle) . '

'; } if ($formDescription) { $html .= '

' . htmlspecialchars($formDescription) . '

'; } $html .= '
'; foreach ($fields as $field) { $type = $field['type'] ?? 'text'; $label = $field['label'] ?? ''; $placeholder = $field['placeholder'] ?? ''; $required = $field['required'] ?? false; $rows = $field['rows'] ?? 4; $html .= '
'; $html .= ''; if ($type === 'textarea') { $html .= sprintf( '', htmlspecialchars($placeholder), $required ? 'required' : '', $rows ); } elseif ($type === 'select') { $options = $field['options'] ?? []; $html .= sprintf(''; } else { $html .= sprintf( '', htmlspecialchars($type), htmlspecialchars($placeholder), $required ? 'required' : '' ); } $html .= '
'; } $html .= sprintf( '', $submitButtonColor, htmlspecialchars($submitButtonText) ); $html .= '
'; $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 .= '
'; 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'] ?? '') . ''; $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 .= '
'; $btnBg = $buttonStyle === 'solid' ? $buttonColor : 'transparent'; $btnColor = $buttonStyle === 'solid' ? $buttonTextColor : $buttonColor; $html .= '' . htmlspecialchars($buttonText) . ' '; if ($secondaryButtonText) { $html .= '' . htmlspecialchars($secondaryButtonText) . ''; } $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 .= '' . htmlspecialchars($product->name) . ''; 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 = '
'; if ($formTitle) { $html .= '

' . htmlspecialchars($formTitle) . '

'; } if ($formDescription) { $html .= '

' . htmlspecialchars($formDescription) . '

'; } $html .= '
'; $html .= csrf_field(); // Service Selection if ($showServiceSelection) { $html .= '
'; $html .= ''; $html .= '
'; } // Date Picker if ($showDatePicker) { $minDate = date('Y-m-d'); $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; } // Time Picker if ($showTimePicker) { $html .= '
'; $html .= ''; $html .= '
'; } $html .= '
'; // Customer Information $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= ''; $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 = '
'; if ($formTitle) { $html .= '

' . htmlspecialchars($formTitle) . '

'; } if ($formDescription) { $html .= '

' . htmlspecialchars($formDescription) . '

'; } $html .= '
'; $html .= csrf_field(); // Amount Field if ($fixedAmount) { $html .= '
'; $html .= 'Payment Amount: ' . $currencySymbol . number_format($fixedAmount, 2) . ' ' . $currency; $html .= '
'; $html .= ''; } elseif ($showAmountField) { $html .= '
'; $html .= ''; $html .= '
'; $html .= '' . $currencySymbol . ''; $html .= ''; $html .= '
'; $html .= '' . $currency . ''; $html .= '
'; } // Purpose Field if ($showPurposeField) { $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; } $html .= '
'; // Billing Information $html .= '
Billing Information
'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; // Payment Details $html .= '
Payment Details
'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; $html .= '
'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; $html .= '
'; $html .= '
Your payment information is secure and encrypted
'; $html .= ''; $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 = '