1621 lines
62 KiB
TypeScript
1621 lines
62 KiB
TypeScript
/**
|
|
* Clean WordPress/Elementor content and convert to styled React components
|
|
*/
|
|
|
|
import Image from "next/image"
|
|
import React, { ReactNode } from "react"
|
|
import {
|
|
cleanHtmlEntities,
|
|
getImagePath,
|
|
getImageAlt,
|
|
ImageMapping,
|
|
} from "@/lib/wordpress-content"
|
|
import { VendingMachineCard } from "@/components/vending-machine-card"
|
|
import { ImageCarousel } from "@/components/image-carousel"
|
|
|
|
interface CleanContentOptions {
|
|
imageMapping?: ImageMapping
|
|
pageTitle?: string // Page title to check for duplicates
|
|
prioritizeFirstImage?: boolean
|
|
}
|
|
|
|
/**
|
|
* Clean and convert WordPress Elementor HTML to styled React components
|
|
*/
|
|
export function cleanWordPressContent(
|
|
html: string,
|
|
options: CleanContentOptions = {}
|
|
): ReactNode[] {
|
|
if (!html || typeof html !== "string") {
|
|
return []
|
|
}
|
|
|
|
const { imageMapping = {}, pageTitle, prioritizeFirstImage = false } = options
|
|
const components: ReactNode[] = []
|
|
let hasPrioritizedImage = false
|
|
|
|
try {
|
|
// Update location links to new format (currently just returns HTML unchanged)
|
|
// TODO: If location links need updating in the future, implement URL pattern matching here
|
|
// For example, convert old WordPress URLs to new Next.js route format
|
|
// Example: /vending-machines/salt-lake-city/ -> /vending-machines-salt-lake-city-utah
|
|
const htmlWithUpdatedLinks = html
|
|
|
|
// Remove Elementor wrapper and data attributes
|
|
let cleaned = htmlWithUpdatedLinks
|
|
// Remove Elementor wrapper div
|
|
.replace(/<div[^>]*data-elementor-type[^>]*>/gi, "")
|
|
.replace(/<\/div>\s*$/, "")
|
|
// Remove all Elementor data attributes
|
|
.replace(/\s+data-[^=]*="[^"]*"/gi, "")
|
|
// Remove Elementor-specific classes but keep useful ones
|
|
.replace(/\s+class="elementor[^"]*"/gi, "")
|
|
.replace(/\s+class="ekit[^"]*"/gi, "")
|
|
// Remove SVG decorative shapes
|
|
.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, "")
|
|
// Remove empty divs
|
|
.replace(/<div[^>]*>\s*<\/div>/gi, "")
|
|
|
|
// Extract sections (Elementor sections become our sections)
|
|
const sectionRegex = /<section[^>]*>([\s\S]*?)<\/section>/gi
|
|
let sectionMatch
|
|
const sections: string[] = []
|
|
|
|
while ((sectionMatch = sectionRegex.exec(cleaned)) !== null) {
|
|
sections.push(sectionMatch[1])
|
|
}
|
|
|
|
// If no sections found, treat entire content as one section
|
|
if (sections.length === 0) {
|
|
sections.push(cleaned)
|
|
}
|
|
|
|
sections.forEach((sectionHtml, sectionIndex) => {
|
|
const sectionComponents: ReactNode[] = []
|
|
|
|
// Extract funfact widgets BEFORE cleaning (they contain SVG icons and special structure)
|
|
const funfactRegex =
|
|
/<div[^>]*elementor-widget-elementskit-funfact[^>]*>([\s\S]*?)<\/div>\s*<\/div>\s*<\/div>/gi
|
|
const funfacts: Array<{ number: string; title: string; index: number }> =
|
|
[]
|
|
let funfactMatch
|
|
|
|
funfactRegex.lastIndex = 0
|
|
while ((funfactMatch = funfactRegex.exec(sectionHtml)) !== null) {
|
|
const funfactHtml = funfactMatch[0]
|
|
// Extract text content (remove HTML tags and SVG)
|
|
let text = funfactHtml.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, "")
|
|
text = text.replace(/<[^>]+>/g, " ")
|
|
text = text.replace(/\s+/g, " ").trim()
|
|
|
|
// Try to find number and title patterns
|
|
// Funfacts typically have format: "NUMBER TITLE" or just text
|
|
const parts = text.split(/\s+/)
|
|
let number = ""
|
|
let title = ""
|
|
|
|
// Check if first part is a number
|
|
if (parts.length > 0 && /^\d+[+\-]?$/.test(parts[0])) {
|
|
number = parts[0]
|
|
title = parts.slice(1).join(" ")
|
|
} else {
|
|
// Try to extract from HTML structure
|
|
const numberMatch = funfactHtml.match(/funfact-number[^>]*>([^<]+)</i)
|
|
const titleMatch = funfactHtml.match(/funfact-title[^>]*>([^<]+)</i)
|
|
number = numberMatch ? numberMatch[1].trim() : ""
|
|
title = titleMatch ? titleMatch[1].trim() : text
|
|
}
|
|
|
|
if (text) {
|
|
funfacts.push({
|
|
number: number || text.split(" ")[0] || "",
|
|
title: title || text.substring(number.length).trim() || text,
|
|
index: funfactMatch.index,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Extract and process headings
|
|
const headingRegex = /<h([1-6])[^>]*>(.*?)<\/h[1-6]>/gi
|
|
let headingMatch
|
|
const headings: Array<{ level: number; content: string; index: number }> =
|
|
[]
|
|
let isFirstHeading = true
|
|
|
|
// Helper function to check if headings are similar (for duplicate detection)
|
|
const areHeadingsSimilar = (
|
|
heading1: string,
|
|
heading2: string
|
|
): boolean => {
|
|
const h1 = heading1.trim().toLowerCase()
|
|
const h2 = heading2.trim().toLowerCase()
|
|
|
|
// Exact match
|
|
if (h1 === h2) return true
|
|
|
|
// Check if one contains the other
|
|
if (h1.includes(h2) || h2.includes(h1)) {
|
|
const shorter = h1.length < h2.length ? h1 : h2
|
|
const longer = h1.length >= h2.length ? h1 : h2
|
|
|
|
// If shorter is at least 60% of longer, consider similar
|
|
if (shorter.length / longer.length >= 0.6) {
|
|
return true
|
|
}
|
|
|
|
// If shorter is a single word and appears as a word (not substring) in longer, consider similar
|
|
// This catches cases like "Reviews" in "Rocky Mountain Vending: Reviews & Testimonials"
|
|
if (shorter.split(/\s+/).length === 1) {
|
|
// Check if it appears as a whole word (with word boundaries)
|
|
const wordRegex = new RegExp(
|
|
`\\b${shorter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
"i"
|
|
)
|
|
if (wordRegex.test(longer)) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// If shorter is multiple words and all words appear in longer, consider similar
|
|
const shorterWords = shorter.split(/\s+/).filter((w) => w.length > 2) // Filter out short words like "a", "an", "the"
|
|
if (shorterWords.length >= 2) {
|
|
const allWordsMatch = shorterWords.every((word) => {
|
|
const wordRegex = new RegExp(
|
|
`\\b${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
"i"
|
|
)
|
|
return wordRegex.test(longer)
|
|
})
|
|
if (allWordsMatch) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if they start with the same words (for cases where one is a longer version)
|
|
const h1Words = h1.split(/\s+/).slice(0, 3) // First 3 words
|
|
const h2Words = h2.split(/\s+/).slice(0, 3)
|
|
if (h1Words.length >= 2 && h2Words.length >= 2) {
|
|
// Check if first 2 words match
|
|
if (h1Words[0] === h2Words[0] && h1Words[1] === h2Words[1]) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
headingRegex.lastIndex = 0
|
|
while ((headingMatch = headingRegex.exec(sectionHtml)) !== null) {
|
|
const headingContent = cleanHtmlEntities(headingMatch[2])
|
|
const headingLevel = parseInt(headingMatch[1])
|
|
|
|
// Skip heading if:
|
|
// 1. It's the first heading and matches the page title, OR
|
|
// 2. It's an h1 that matches the page title (h1 should only be used for page title)
|
|
if (pageTitle) {
|
|
if (areHeadingsSimilar(headingContent, pageTitle)) {
|
|
// Skip if it's the first heading, or if it's an h1 (h1 should only be page title)
|
|
if (isFirstHeading || headingLevel === 1) {
|
|
isFirstHeading = false
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
headings.push({
|
|
level: headingLevel,
|
|
content: headingContent,
|
|
index: headingMatch.index,
|
|
})
|
|
isFirstHeading = false
|
|
}
|
|
|
|
// Check for slider/carousel widgets first (like elementskit-client-logo)
|
|
// Look for the widget container that has elementskit-client-logo - match the entire widget including nested divs
|
|
const sliderWidgetRegex =
|
|
/<div[^>]*elementor-widget-elementskit-client-logo[^>]*>([\s\S]*?)(?:<\/div>\s*){3,}/gi
|
|
let sliderMatch = sliderWidgetRegex.exec(sectionHtml)
|
|
|
|
// Also try to find the slider div directly with the comment
|
|
if (!sliderMatch) {
|
|
const sliderDivRegex =
|
|
/<div[^>]*elementskit-clients-slider[^>]*>([\s\S]*?)<!--\s*\.elementskit-clients-slider\s+END\s*-->/gi
|
|
sliderMatch = sliderDivRegex.exec(sectionHtml)
|
|
}
|
|
|
|
// Try a simpler approach - find any div with swiper-wrapper
|
|
if (!sliderMatch) {
|
|
const swiperWrapperRegex =
|
|
/<div[^>]*swiper-wrapper[^>]*>([\s\S]*?)<\/div>\s*<\/div>/gi
|
|
sliderMatch = swiperWrapperRegex.exec(sectionHtml)
|
|
}
|
|
|
|
let sliderImages: Array<{
|
|
src: string
|
|
alt: string
|
|
width?: number
|
|
height?: number
|
|
title?: string
|
|
}> = []
|
|
let sliderIndex: number | null = null
|
|
|
|
if (sliderMatch) {
|
|
sliderIndex = sliderMatch.index
|
|
const sliderContent = sliderMatch[1]
|
|
// Extract all images from the slider - look in swiper-slide divs
|
|
const slideRegex =
|
|
/<div[^>]*swiper-slide[^>]*>([\s\S]*?)<\/div>\s*<\/div>/gi
|
|
let slideMatch
|
|
|
|
slideRegex.lastIndex = 0
|
|
while ((slideMatch = slideRegex.exec(sliderContent)) !== null) {
|
|
const slideContent = slideMatch[1]
|
|
const imgMatch = slideContent.match(/<img([^>]*)>/i)
|
|
|
|
if (imgMatch) {
|
|
const imgTag = imgMatch[0]
|
|
const srcMatch = imgTag.match(/src=["']([^"']+)["']/i)
|
|
const altMatch = imgTag.match(/alt=["']([^"']*)["']/i)
|
|
const titleMatch =
|
|
slideContent.match(/title=["']([^"']*)["']/i) ||
|
|
imgTag.match(/title=["']([^"']*)["']/i)
|
|
const widthMatch = imgTag.match(/width=["']?(\d+)["']?/i)
|
|
const heightMatch = imgTag.match(/height=["']?(\d+)["']?/i)
|
|
|
|
if (srcMatch) {
|
|
// Fix malformed URLs (e.g., https:///wp-content -> https://rockymountainvending.com/wp-content)
|
|
let imageSrc = srcMatch[1]
|
|
if (
|
|
imageSrc.startsWith("https:///") ||
|
|
imageSrc.startsWith("http:///")
|
|
) {
|
|
imageSrc = imageSrc.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
imageSrc.startsWith("https://wp-content") ||
|
|
imageSrc.startsWith("http://wp-content")
|
|
) {
|
|
imageSrc = imageSrc.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
}
|
|
|
|
sliderImages.push({
|
|
src: imageSrc,
|
|
alt: altMatch
|
|
? cleanHtmlEntities(altMatch[1])
|
|
: titleMatch
|
|
? cleanHtmlEntities(titleMatch[1])
|
|
: "",
|
|
width: widthMatch ? parseInt(widthMatch[1]) : undefined,
|
|
height: heightMatch ? parseInt(heightMatch[1]) : undefined,
|
|
title: titleMatch
|
|
? cleanHtmlEntities(titleMatch[1])
|
|
: undefined,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no slides found, try direct image extraction from slider content
|
|
if (sliderImages.length === 0) {
|
|
const sliderImageRegex = /<img([^>]*)>/gi
|
|
let sliderImgMatch
|
|
|
|
sliderImageRegex.lastIndex = 0
|
|
while (
|
|
(sliderImgMatch = sliderImageRegex.exec(sliderContent)) !== null
|
|
) {
|
|
const imgTag = sliderImgMatch[0]
|
|
const srcMatch = imgTag.match(/src=["']([^"']+)["']/i)
|
|
const altMatch = imgTag.match(/alt=["']([^"']*)["']/i)
|
|
const titleMatch = imgTag.match(/title=["']([^"']*)["']/i)
|
|
const widthMatch = imgTag.match(/width=["']?(\d+)["']?/i)
|
|
const heightMatch = imgTag.match(/height=["']?(\d+)["']?/i)
|
|
|
|
if (srcMatch) {
|
|
// Fix malformed URLs (e.g., https:///wp-content -> https://rockymountainvending.com/wp-content)
|
|
let imageSrc = srcMatch[1]
|
|
if (
|
|
imageSrc.startsWith("https:///") ||
|
|
imageSrc.startsWith("http:///")
|
|
) {
|
|
imageSrc = imageSrc.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
imageSrc.startsWith("https://wp-content") ||
|
|
imageSrc.startsWith("http://wp-content")
|
|
) {
|
|
imageSrc = imageSrc.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
}
|
|
|
|
sliderImages.push({
|
|
src: imageSrc,
|
|
alt: altMatch
|
|
? cleanHtmlEntities(altMatch[1])
|
|
: titleMatch
|
|
? cleanHtmlEntities(titleMatch[1])
|
|
: "",
|
|
width: widthMatch ? parseInt(widthMatch[1]) : undefined,
|
|
height: heightMatch ? parseInt(heightMatch[1]) : undefined,
|
|
title: titleMatch
|
|
? cleanHtmlEntities(titleMatch[1])
|
|
: undefined,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract Elementor gallery widget items (e.g., repairs page image grid)
|
|
const galleryImages: Array<{
|
|
src: string
|
|
alt: string
|
|
width?: number
|
|
height?: number
|
|
index: number
|
|
}> = []
|
|
const galleryImageRegex =
|
|
/<div[^>]*class="[^"]*e-gallery-image[^"]*"[^>]*data-thumbnail="([^"]+)"[^>]*data-width="(\d+)"[^>]*data-height="(\d+)"[^>]*aria-label="([^"]*)"[^>]*>/gi
|
|
let galleryMatch
|
|
|
|
galleryImageRegex.lastIndex = 0
|
|
while ((galleryMatch = galleryImageRegex.exec(sectionHtml)) !== null) {
|
|
let imageSrc = galleryMatch[1]
|
|
|
|
// Fix malformed URLs (e.g., https:///wp-content -> https://rockymountainvending.com/wp-content)
|
|
if (
|
|
imageSrc.startsWith("https:///") ||
|
|
imageSrc.startsWith("http:///")
|
|
) {
|
|
imageSrc = imageSrc.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
imageSrc.startsWith("https://wp-content") ||
|
|
imageSrc.startsWith("http://wp-content")
|
|
) {
|
|
imageSrc = imageSrc.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
}
|
|
|
|
const width = parseInt(galleryMatch[2], 10)
|
|
const height = parseInt(galleryMatch[3], 10)
|
|
const alt = cleanHtmlEntities(galleryMatch[4] || "")
|
|
|
|
galleryImages.push({
|
|
src: imageSrc,
|
|
alt,
|
|
width: Number.isNaN(width) ? undefined : width,
|
|
height: Number.isNaN(height) ? undefined : height,
|
|
index: galleryMatch.index,
|
|
})
|
|
}
|
|
|
|
// Extract and process regular images (excluding slider images)
|
|
const imageRegex = /<img([^>]*)>/gi
|
|
const images: Array<{
|
|
src: string
|
|
alt: string
|
|
width?: number
|
|
height?: number
|
|
index: number
|
|
}> = []
|
|
let imageMatch
|
|
|
|
imageRegex.lastIndex = 0
|
|
while ((imageMatch = imageRegex.exec(sectionHtml)) !== null) {
|
|
// Skip if this image is part of the slider
|
|
if (
|
|
sliderIndex !== null &&
|
|
imageMatch.index >= sliderIndex &&
|
|
imageMatch.index < sliderIndex + sliderMatch![0].length
|
|
) {
|
|
continue
|
|
}
|
|
|
|
const imgTag = imageMatch[0]
|
|
const srcMatch = imgTag.match(/src=["']([^"']+)["']/i)
|
|
const altMatch = imgTag.match(/alt=["']([^"']*)["']/i)
|
|
const widthMatch = imgTag.match(/width=["']?(\d+)["']?/i)
|
|
const heightMatch = imgTag.match(/height=["']?(\d+)["']?/i)
|
|
|
|
if (srcMatch) {
|
|
// Fix malformed URLs (e.g., https:///wp-content -> https://rockymountainvending.com/wp-content)
|
|
let imageSrc = srcMatch[1]
|
|
if (
|
|
imageSrc.startsWith("https:///") ||
|
|
imageSrc.startsWith("http:///")
|
|
) {
|
|
imageSrc = imageSrc.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
imageSrc.startsWith("https://wp-content") ||
|
|
imageSrc.startsWith("http://wp-content")
|
|
) {
|
|
imageSrc = imageSrc.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
}
|
|
// Fix specific auto-repair image URL
|
|
if (
|
|
imageSrc.includes("Vending-Machine-at-an-Auto-Repair-Facility") &&
|
|
!imageSrc.includes("rockymountainvending.com")
|
|
) {
|
|
imageSrc =
|
|
"https://rockymountainvending.com/wp-content/uploads/2024/01/Vending-Machine-at-an-Auto-Repair-Facility-768x1024.webp"
|
|
}
|
|
|
|
images.push({
|
|
src: imageSrc,
|
|
alt: altMatch ? cleanHtmlEntities(altMatch[1]) : "",
|
|
width: widthMatch ? parseInt(widthMatch[1]) : undefined,
|
|
height: heightMatch ? parseInt(heightMatch[1]) : undefined,
|
|
index: imageMatch.index,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Extract paragraphs
|
|
const paragraphRegex = /<p[^>]*>(.*?)<\/p>/gi
|
|
const paragraphs: Array<{
|
|
content: string
|
|
index: number
|
|
hasList: boolean
|
|
}> = []
|
|
let paraMatch
|
|
|
|
paragraphRegex.lastIndex = 0
|
|
while ((paraMatch = paragraphRegex.exec(sectionHtml)) !== null) {
|
|
const content = paraMatch[1]
|
|
const hasList = /<(ul|ol)[^>]*>/i.test(content)
|
|
const cleanContent = cleanHtmlEntities(content).trim()
|
|
|
|
if (cleanContent.length > 0) {
|
|
paragraphs.push({
|
|
content: cleanContent,
|
|
index: paraMatch.index,
|
|
hasList,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Extract lists - handle nested structures better
|
|
// First try to find lists at the top level
|
|
const listRegex = /<(ul|ol)[^>]*>([\s\S]*?)<\/\1>/gi
|
|
const lists: Array<{
|
|
listType: "ul" | "ol"
|
|
items: Array<{ text: string; html: string }>
|
|
index: number
|
|
}> = []
|
|
let listMatch
|
|
|
|
// Reset regex lastIndex
|
|
listRegex.lastIndex = 0
|
|
while ((listMatch = listRegex.exec(sectionHtml)) !== null) {
|
|
const listType = listMatch[1] as "ul" | "ol"
|
|
const listContent = listMatch[2]
|
|
const itemRegex = /<li[^>]*>(.*?)<\/li>/gi
|
|
const items: Array<{ text: string; html: string }> = []
|
|
let itemMatch
|
|
|
|
// Reset item regex
|
|
itemRegex.lastIndex = 0
|
|
while ((itemMatch = itemRegex.exec(listContent)) !== null) {
|
|
const itemHtml = itemMatch[1]
|
|
// Clean HTML entities but preserve basic formatting
|
|
const cleanText = cleanHtmlEntities(itemHtml)
|
|
items.push({
|
|
text: cleanText,
|
|
html: itemHtml, // Keep original HTML for processing
|
|
})
|
|
}
|
|
|
|
if (items.length > 0) {
|
|
lists.push({
|
|
listType: listType,
|
|
items,
|
|
index: listMatch.index,
|
|
})
|
|
}
|
|
}
|
|
|
|
// If no lists found at top level, try to find them in paragraphs (they might be nested)
|
|
if (lists.length === 0) {
|
|
// Look for lists inside paragraph content - need to use original HTML, not cleaned
|
|
const paragraphRegexOriginal = /<p[^>]*>(.*?)<\/p>/gi
|
|
let paraMatchOriginal
|
|
|
|
paragraphRegexOriginal.lastIndex = 0
|
|
while (
|
|
(paraMatchOriginal = paragraphRegexOriginal.exec(sectionHtml)) !==
|
|
null
|
|
) {
|
|
const paraContent = paraMatchOriginal[1]
|
|
const paraListRegex = /<(ul|ol)[^>]*>([\s\S]*?)<\/\1>/gi
|
|
let paraListMatch
|
|
|
|
paraListRegex.lastIndex = 0
|
|
while ((paraListMatch = paraListRegex.exec(paraContent)) !== null) {
|
|
const listType = paraListMatch[1] as "ul" | "ol"
|
|
const listContent = paraListMatch[2]
|
|
const itemRegex = /<li[^>]*>(.*?)<\/li>/gi
|
|
const items: Array<{ text: string; html: string }> = []
|
|
let itemMatch
|
|
|
|
itemRegex.lastIndex = 0
|
|
while ((itemMatch = itemRegex.exec(listContent)) !== null) {
|
|
const itemHtml = itemMatch[1]
|
|
items.push({
|
|
text: cleanHtmlEntities(itemHtml),
|
|
html: itemHtml,
|
|
})
|
|
}
|
|
|
|
if (items.length > 0) {
|
|
lists.push({
|
|
listType: listType,
|
|
items,
|
|
index: paraMatchOriginal.index + (paraListMatch.index || 0),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Group images that should be side-by-side (images close together in same section)
|
|
const imageGroups: Array<Array<(typeof images)[0] & { type: "image" }>> =
|
|
[]
|
|
const processedImageIndices = new Set<number>()
|
|
|
|
images.forEach((img, idx) => {
|
|
if (processedImageIndices.has(idx)) return
|
|
|
|
// Check if this image is in a column layout
|
|
const imgContext = sectionHtml.substring(
|
|
Math.max(0, img.index - 2000),
|
|
Math.min(sectionHtml.length, img.index + 2000)
|
|
)
|
|
const isInColumn = /elementor-col-(33|50|66|25|75)/i.test(imgContext)
|
|
|
|
const group = [img]
|
|
processedImageIndices.add(idx)
|
|
|
|
// Find other images close to this one (within 2000 chars)
|
|
images.forEach((otherImg, otherIdx) => {
|
|
if (otherIdx !== idx && !processedImageIndices.has(otherIdx)) {
|
|
const distance = Math.abs(otherImg.index - img.index)
|
|
|
|
// Check if they're in the same section (no section break between them)
|
|
const betweenContent = sectionHtml.substring(
|
|
Math.min(img.index, otherImg.index),
|
|
Math.max(img.index, otherImg.index)
|
|
)
|
|
const hasSectionBreak = /<\/section>/i.test(betweenContent)
|
|
|
|
if (distance < 2000 && !hasSectionBreak) {
|
|
// If first image is in a column, check if second is too
|
|
if (isInColumn) {
|
|
const otherContext = sectionHtml.substring(
|
|
Math.max(0, otherImg.index - 2000),
|
|
Math.min(sectionHtml.length, otherImg.index + 2000)
|
|
)
|
|
const otherIsInColumn = /elementor-col-(33|50|66|25|75)/i.test(
|
|
otherContext
|
|
)
|
|
|
|
if (otherIsInColumn) {
|
|
group.push(otherImg)
|
|
processedImageIndices.add(otherIdx)
|
|
}
|
|
} else {
|
|
// If first image is not in column, group any nearby images (for general side-by-side)
|
|
group.push(otherImg)
|
|
processedImageIndices.add(otherIdx)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// Only create group if there are 2+ images
|
|
if (group.length > 1) {
|
|
imageGroups.push(
|
|
group.map((img) => ({ ...img, type: "image" as const }))
|
|
)
|
|
} else {
|
|
// Single image, don't group
|
|
processedImageIndices.delete(idx)
|
|
}
|
|
})
|
|
|
|
// Check if this section should have machine cards (detect "Vending Machines for Gyms" pattern)
|
|
let shouldShowMachineCards = false
|
|
let machineCardsHeadingIndex: number | null = null
|
|
let machineCardsParagraphIndex: number | null = null
|
|
|
|
// Look for heading "Vending Machines for Gyms" or similar patterns
|
|
// Also match "Vending Machines We Use for..." variations
|
|
const gymMachinesHeading = headings.find((h) => {
|
|
const cleanHeading = cleanHtmlEntities(
|
|
h.content.replace(/<[^>]+>/g, "")
|
|
).toLowerCase()
|
|
return (
|
|
(cleanHeading.includes("vending machines for") ||
|
|
cleanHeading.includes("vending machines we use")) &&
|
|
(cleanHeading.includes("gym") ||
|
|
cleanHeading.includes("studio") ||
|
|
cleanHeading.includes("community") ||
|
|
cleanHeading.includes("dance") ||
|
|
cleanHeading.includes("car wash"))
|
|
)
|
|
})
|
|
|
|
if (gymMachinesHeading) {
|
|
machineCardsHeadingIndex = gymMachinesHeading.index
|
|
// Look for paragraph about machines - more flexible pattern matching
|
|
const gymMachinesParagraph = paragraphs.find((p) => {
|
|
const cleanPara = cleanHtmlEntities(
|
|
p.content.replace(/<[^>]+>/g, "")
|
|
).toLowerCase()
|
|
// Match various patterns:
|
|
// - "Many of our [gyms/studios/community centers/dance studios/car washes]"
|
|
// - "Primarily we will install"
|
|
// - "Many of our dance studios have just one"
|
|
// - "Many of our community centers have many machines"
|
|
// - "We offer vending machines that are perfectly suited"
|
|
// - Any paragraph that mentions QFV, AMS, Crane, USI, or Seaga machines
|
|
return (
|
|
(cleanPara.includes("many of our") &&
|
|
(cleanPara.includes("gym") ||
|
|
cleanPara.includes("studio") ||
|
|
cleanPara.includes("community") ||
|
|
cleanPara.includes("dance") ||
|
|
cleanPara.includes("car wash"))) ||
|
|
cleanPara.includes("primarily we will install") ||
|
|
(cleanPara.includes("many of our") &&
|
|
cleanPara.includes("vending machine")) ||
|
|
(cleanPara.includes("we offer vending machines") &&
|
|
(cleanPara.includes("car wash") ||
|
|
cleanPara.includes("location"))) ||
|
|
cleanPara.includes("qfv") ||
|
|
cleanPara.includes("ams") ||
|
|
cleanPara.includes("crane") ||
|
|
cleanPara.includes("usi") ||
|
|
cleanPara.includes("seaga")
|
|
)
|
|
})
|
|
|
|
if (gymMachinesParagraph) {
|
|
machineCardsParagraphIndex = gymMachinesParagraph.index
|
|
shouldShowMachineCards = true
|
|
}
|
|
}
|
|
|
|
// Machine data from machines-we-use page - QFV 50 should always be first
|
|
const gymMachines = [
|
|
{
|
|
name: "QFV 50 Combo",
|
|
description:
|
|
"The Ultimate Combo Machine: The QFV 50 stands out as our top combo machine, offering a remarkable blend of style and functionality. With 30 customizable drink selections, it caters to a variety of tastes and preferences.",
|
|
image: "/images/wordpress/EH0A1551-HDR.webp",
|
|
alt: "Quick Fresh Vending QFV 50 combo vending machine showcasing dual compartments for snacks and drinks, with dimensions and payment system details.",
|
|
selections: 50,
|
|
items: 300,
|
|
},
|
|
{
|
|
name: "Crane Bev Max 4",
|
|
description:
|
|
"A Beverage Powerhouse: Bev Max 4 is a testament to efficiency in the vending world, boasting an impressive 48 drink selections. Its standout feature is the incredibly low fault rate of just 0.003%.",
|
|
image: "/images/wordpress/Crane-Bev-Max-4-Drink.webp",
|
|
alt: "Crane Bev Max 4 Classic",
|
|
selections: 48,
|
|
items: 400,
|
|
},
|
|
{
|
|
name: "USI 3130",
|
|
description:
|
|
"Compact Snack Solution: The USI 3130 is designed for locations where space is at a premium. Despite its compact size, it doesn't compromise on functionality.",
|
|
image:
|
|
"/images/wordpress/USI-3130-pps9s29iuucicxizctvma2nj0qezz66y25gy5nrsf4.webp",
|
|
alt: "Rocky Mountain Vending USI 3130",
|
|
selections: 24,
|
|
items: 420,
|
|
},
|
|
{
|
|
name: "AMS 39 VCB",
|
|
description:
|
|
"Reliability Redefined: The AMS 39 VCB is a dependable combo machine that consistently meets our high standards. Its robust design ensures long-term reliability.",
|
|
image: "/images/wordpress/AMS-39-VCB-Combo.webp",
|
|
alt: "AMS 39 VCB",
|
|
selections: 36,
|
|
items: 375,
|
|
},
|
|
{
|
|
name: "Crane Merchant Media Snack",
|
|
description:
|
|
"The Snack Giant: When it comes to snack machines, the Crane Merchant Media Snack is unparalleled in capacity. It's the largest snack machine available, capable of holding a diverse range of snack options.",
|
|
image:
|
|
"/images/wordpress/Crane-Merchant-6-Media-Snack-pps9rxkbwo62qvpt49uhflu81t25wooadi7ir9yra8.webp",
|
|
alt: "Crane Merchant Media 6",
|
|
selections: 44,
|
|
items: 375,
|
|
},
|
|
]
|
|
|
|
// Filter out images that are part of the machine cards section (they'll be replaced)
|
|
const imagesToExclude = new Set<number>()
|
|
const imageGroupsToExclude = new Set<number>()
|
|
if (shouldShowMachineCards && machineCardsParagraphIndex !== null) {
|
|
// Find all images after the paragraph that should be replaced
|
|
// Look for images in the next section after the paragraph (within 10000 chars to catch all)
|
|
const sectionEndIndex = machineCardsParagraphIndex + 10000
|
|
images.forEach((img, idx) => {
|
|
if (
|
|
img.index > machineCardsParagraphIndex! &&
|
|
img.index < sectionEndIndex
|
|
) {
|
|
// Check if it's one of the machine images (by checking alt text or src)
|
|
const imgAlt = img.alt.toLowerCase()
|
|
const imgSrc = img.src.toLowerCase()
|
|
// Exclude ANY image in elementor/thumbs folder (these are the old machine images)
|
|
// Also exclude any image that matches machine patterns
|
|
if (
|
|
imgSrc.includes("elementor/thumbs") ||
|
|
imgAlt.includes("crane bev max") ||
|
|
imgAlt.includes("bev max") ||
|
|
imgAlt.includes("usi 3130") ||
|
|
imgAlt.includes("usi") ||
|
|
imgAlt.includes("ams 39") ||
|
|
imgAlt.includes("ams") ||
|
|
imgAlt.includes("crane merchant") ||
|
|
imgAlt.includes("merchant media") ||
|
|
imgAlt.includes("qfv") ||
|
|
imgAlt.includes("quick fresh") ||
|
|
imgAlt.includes("seaga") ||
|
|
imgAlt.includes("seage") ||
|
|
imgSrc.includes("bev-max") ||
|
|
imgSrc.includes("usi-3130") ||
|
|
imgSrc.includes("ams-39") ||
|
|
imgSrc.includes("merchant") ||
|
|
imgSrc.includes("qfv") ||
|
|
imgSrc.includes("qfv-50") ||
|
|
imgSrc.includes("seaga") ||
|
|
imgSrc.includes("seage")
|
|
) {
|
|
imagesToExclude.add(idx)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Also exclude image groups if they're in the machine cards section
|
|
imageGroups.forEach((group, groupIdx) => {
|
|
if (
|
|
group[0].index > machineCardsParagraphIndex! &&
|
|
group[0].index < sectionEndIndex
|
|
) {
|
|
// Check if any image in the group matches machine patterns
|
|
// Prioritize checking for elementor/thumbs first (most reliable indicator)
|
|
const hasMachineImage = group.some((img) => {
|
|
const imgAlt = img.alt.toLowerCase()
|
|
const imgSrc = img.src.toLowerCase()
|
|
return (
|
|
imgSrc.includes("elementor/thumbs") ||
|
|
imgAlt.includes("crane bev max") ||
|
|
imgAlt.includes("bev max") ||
|
|
imgAlt.includes("usi 3130") ||
|
|
imgAlt.includes("usi") ||
|
|
imgAlt.includes("ams 39") ||
|
|
imgAlt.includes("ams") ||
|
|
imgAlt.includes("crane merchant") ||
|
|
imgAlt.includes("merchant media") ||
|
|
imgAlt.includes("qfv") ||
|
|
imgAlt.includes("quick fresh") ||
|
|
imgAlt.includes("seaga") ||
|
|
imgAlt.includes("seage") ||
|
|
imgSrc.includes("bev-max") ||
|
|
imgSrc.includes("usi-3130") ||
|
|
imgSrc.includes("ams-39") ||
|
|
imgSrc.includes("merchant") ||
|
|
imgSrc.includes("qfv") ||
|
|
imgSrc.includes("qfv-50") ||
|
|
imgSrc.includes("seaga") ||
|
|
imgSrc.includes("seage")
|
|
)
|
|
})
|
|
if (hasMachineImage) {
|
|
imageGroupsToExclude.add(groupIdx)
|
|
// Mark all images in this group as processed so they're excluded
|
|
group.forEach((img) => {
|
|
const imgIndex = images.findIndex((i) => i.index === img.index)
|
|
if (imgIndex !== -1) {
|
|
processedImageIndices.add(imgIndex)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Process content in order
|
|
const allElements = [
|
|
...headings.map((h) => ({ type: "heading" as const, ...h })),
|
|
...funfacts.map((f) => ({ type: "funfact" as const, ...f })),
|
|
...(sliderImages.length > 0
|
|
? [
|
|
{
|
|
type: "slider" as const,
|
|
images: sliderImages,
|
|
index: sliderIndex!,
|
|
},
|
|
]
|
|
: []),
|
|
...(galleryImages.length > 0
|
|
? galleryImages.map((img) => ({
|
|
type: "gallery" as const,
|
|
...img,
|
|
}))
|
|
: []),
|
|
...imageGroups
|
|
.filter((group, groupIdx) => !imageGroupsToExclude.has(groupIdx))
|
|
.map((group, groupIdx) => ({
|
|
type: "image-group" as const,
|
|
images: group,
|
|
index: group[0].index,
|
|
})),
|
|
...images
|
|
.filter(
|
|
(img, idx) =>
|
|
!processedImageIndices.has(idx) && !imagesToExclude.has(idx)
|
|
)
|
|
.map((img) => ({ type: "image" as const, ...img })),
|
|
...paragraphs.map((p) => ({ type: "paragraph" as const, ...p })),
|
|
...lists.map((l) => ({
|
|
type: "list" as const,
|
|
listType: l.listType,
|
|
items: l.items,
|
|
index: l.index,
|
|
})),
|
|
// Add machine cards after the paragraph if detected
|
|
...(shouldShowMachineCards && machineCardsParagraphIndex !== null
|
|
? [
|
|
{
|
|
type: "machine-cards" as const,
|
|
index: machineCardsParagraphIndex + 1000,
|
|
},
|
|
]
|
|
: []),
|
|
].sort((a, b) => a.index - b.index)
|
|
|
|
// Render elements
|
|
allElements.forEach((element) => {
|
|
if (element.type === "heading") {
|
|
const headingElement = element as {
|
|
type: "heading"
|
|
level: number
|
|
content: string
|
|
index: number
|
|
}
|
|
const HeadingTag =
|
|
`h${headingElement.level}` as keyof React.JSX.IntrinsicElements
|
|
// Clean HTML tags from heading content
|
|
let cleanHeadingContent = headingElement.content
|
|
// Remove HTML tags but preserve text content
|
|
cleanHeadingContent = cleanHeadingContent.replace(/<[^>]+>/g, "")
|
|
// Clean HTML entities
|
|
cleanHeadingContent = cleanHtmlEntities(cleanHeadingContent)
|
|
|
|
sectionComponents.push(
|
|
<HeadingTag
|
|
key={`heading-${sectionIndex}-${headingElement.index}`}
|
|
className={`font-bold mb-4 ${
|
|
headingElement.level === 1
|
|
? "text-4xl md:text-5xl"
|
|
: headingElement.level === 2
|
|
? "text-2xl md:text-3xl tracking-tight text-balance"
|
|
: headingElement.level === 3
|
|
? "text-xl md:text-2xl tracking-tight"
|
|
: "text-lg md:text-xl font-semibold tracking-tight"
|
|
}`}
|
|
>
|
|
{cleanHeadingContent}
|
|
</HeadingTag>
|
|
)
|
|
} else if (element.type === "slider") {
|
|
// Render slider images as a horizontal scrolling carousel with auto-rotation
|
|
const carouselImages = element.images.map((img) => {
|
|
const imagePath = getImagePath(img.src, imageMapping)
|
|
const alt = getImageAlt(
|
|
img.src,
|
|
imageMapping,
|
|
img.alt || img.title || ""
|
|
)
|
|
|
|
// Use the mapped local path if available, otherwise fix malformed URLs
|
|
let finalSrc = imagePath
|
|
|
|
// If imagePath is a local path (starts with /), use it directly
|
|
if (imagePath.startsWith("/") && !imagePath.startsWith("//")) {
|
|
finalSrc = imagePath
|
|
} else {
|
|
// Fix malformed URLs in imagePath first
|
|
if (
|
|
imagePath.startsWith("https:///") ||
|
|
imagePath.startsWith("http:///")
|
|
) {
|
|
finalSrc = imagePath.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
imagePath.startsWith("https://wp-content") ||
|
|
imagePath.startsWith("http://wp-content")
|
|
) {
|
|
finalSrc = imagePath.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
} else if (imagePath.startsWith("http")) {
|
|
finalSrc = imagePath // Keep as-is if valid URL
|
|
} else {
|
|
// Fallback to original src and fix it
|
|
finalSrc = img.src
|
|
if (
|
|
finalSrc.startsWith("https:///") ||
|
|
finalSrc.startsWith("http:///")
|
|
) {
|
|
finalSrc = finalSrc.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
finalSrc.startsWith("https://wp-content") ||
|
|
finalSrc.startsWith("http://wp-content")
|
|
) {
|
|
finalSrc = finalSrc.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
} else if (finalSrc.startsWith("http")) {
|
|
finalSrc = finalSrc // Keep as-is if valid URL
|
|
} else if (finalSrc.startsWith("/")) {
|
|
finalSrc = `https://rockymountainvending.com${finalSrc}`
|
|
} else {
|
|
finalSrc = `https://rockymountainvending.com/${finalSrc}`
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
src: finalSrc,
|
|
alt: alt,
|
|
title: img.title,
|
|
}
|
|
})
|
|
|
|
sectionComponents.push(
|
|
<div
|
|
key={`slider-${sectionIndex}-${element.index}`}
|
|
className="my-8 w-full"
|
|
>
|
|
<ImageCarousel
|
|
images={carouselImages}
|
|
autoScrollInterval={3000}
|
|
/>
|
|
</div>
|
|
)
|
|
} else if (element.type === "gallery") {
|
|
const imagePath = getImagePath(element.src, imageMapping)
|
|
const alt = getImageAlt(element.src, imageMapping, element.alt)
|
|
|
|
// Use the mapped local path if available, otherwise fix malformed URLs
|
|
let finalSrc = imagePath
|
|
|
|
if (imagePath.startsWith("/") && !imagePath.startsWith("//")) {
|
|
finalSrc = imagePath
|
|
} else {
|
|
if (
|
|
imagePath.startsWith("https:///") ||
|
|
imagePath.startsWith("http:///")
|
|
) {
|
|
finalSrc = imagePath.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
imagePath.startsWith("https://wp-content") ||
|
|
imagePath.startsWith("http://wp-content")
|
|
) {
|
|
finalSrc = imagePath.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
} else if (imagePath.startsWith("http")) {
|
|
finalSrc = imagePath
|
|
} else {
|
|
finalSrc = element.src
|
|
if (
|
|
finalSrc.startsWith("https:///") ||
|
|
finalSrc.startsWith("http:///")
|
|
) {
|
|
finalSrc = finalSrc.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
finalSrc.startsWith("https://wp-content") ||
|
|
finalSrc.startsWith("http://wp-content")
|
|
) {
|
|
finalSrc = finalSrc.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
} else if (finalSrc.startsWith("http")) {
|
|
finalSrc = finalSrc
|
|
} else if (finalSrc.startsWith("/")) {
|
|
finalSrc = `https://rockymountainvending.com${finalSrc}`
|
|
} else {
|
|
finalSrc = `https://rockymountainvending.com/${finalSrc}`
|
|
}
|
|
}
|
|
}
|
|
|
|
// Constrain image dimensions per style guide (max 600px for standalone images)
|
|
const constrainedWidth = element.width
|
|
? Math.min(element.width, 600)
|
|
: 600
|
|
const constrainedHeight = element.height
|
|
? Math.min(element.height, 600)
|
|
: 600
|
|
|
|
sectionComponents.push(
|
|
<div
|
|
key={`gallery-${sectionIndex}-${element.index}`}
|
|
className="my-6 w-full max-w-md mx-auto"
|
|
>
|
|
<div className="relative w-full overflow-hidden rounded-[1.5rem] border border-border/60 bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(247,244,236,0.94))] p-3 shadow-[0_10px_28px_rgba(15,23,42,0.06)]">
|
|
{element.width && element.height ? (
|
|
<Image
|
|
src={finalSrc}
|
|
alt={alt}
|
|
width={constrainedWidth}
|
|
height={constrainedHeight}
|
|
className="h-auto w-full rounded-[1rem] object-contain"
|
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
|
|
priority={prioritizeFirstImage && !hasPrioritizedImage}
|
|
/>
|
|
) : (
|
|
<div className="relative aspect-video w-full rounded-[1rem] bg-muted">
|
|
<Image
|
|
src={finalSrc}
|
|
alt={alt}
|
|
fill
|
|
className="object-contain"
|
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
|
|
priority={prioritizeFirstImage && !hasPrioritizedImage}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
if (prioritizeFirstImage && !hasPrioritizedImage) {
|
|
hasPrioritizedImage = true
|
|
}
|
|
} else if (element.type === "image-group") {
|
|
// Render grouped images side-by-side
|
|
const groupImages = element.images
|
|
const imageCount = groupImages.length
|
|
|
|
sectionComponents.push(
|
|
<div
|
|
key={`image-group-${sectionIndex}-${element.index}`}
|
|
className="my-6 w-full"
|
|
>
|
|
<div
|
|
className={`flex flex-col md:flex-row gap-4 ${imageCount === 2 ? "md:gap-6" : "md:gap-4"}`}
|
|
>
|
|
{groupImages.map((img, imgIdx) => {
|
|
const imagePath = getImagePath(img.src, imageMapping)
|
|
const alt = getImageAlt(img.src, imageMapping, img.alt)
|
|
|
|
// Use the mapped local path if available, otherwise fix malformed URLs
|
|
let finalSrc = imagePath
|
|
|
|
// If imagePath is a local path (starts with /), use it directly
|
|
if (
|
|
imagePath.startsWith("/") &&
|
|
!imagePath.startsWith("//")
|
|
) {
|
|
finalSrc = imagePath
|
|
} else {
|
|
// Fix malformed URLs in imagePath first
|
|
if (
|
|
imagePath.startsWith("https:///") ||
|
|
imagePath.startsWith("http:///")
|
|
) {
|
|
finalSrc = imagePath.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
imagePath.startsWith("https://wp-content") ||
|
|
imagePath.startsWith("http://wp-content")
|
|
) {
|
|
finalSrc = imagePath.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
} else if (imagePath.startsWith("http")) {
|
|
finalSrc = imagePath // Keep as-is if valid URL
|
|
} else {
|
|
// Fallback to original src and fix it
|
|
finalSrc = img.src
|
|
if (
|
|
finalSrc.startsWith("https:///") ||
|
|
finalSrc.startsWith("http:///")
|
|
) {
|
|
finalSrc = finalSrc.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
finalSrc.startsWith("https://wp-content") ||
|
|
finalSrc.startsWith("http://wp-content")
|
|
) {
|
|
finalSrc = finalSrc.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
} else if (finalSrc.startsWith("http")) {
|
|
finalSrc = finalSrc // Keep as-is if valid URL
|
|
} else if (finalSrc.startsWith("/")) {
|
|
finalSrc = `https://rockymountainvending.com${finalSrc}`
|
|
} else {
|
|
finalSrc = `https://rockymountainvending.com/${finalSrc}`
|
|
}
|
|
}
|
|
}
|
|
|
|
// Constrain image dimensions per style guide (max 300px for grid images)
|
|
const constrainedWidth = img.width
|
|
? Math.min(img.width, 300)
|
|
: 300
|
|
const constrainedHeight = img.height
|
|
? Math.min(img.height, 300)
|
|
: 300
|
|
|
|
return (
|
|
<div
|
|
key={imgIdx}
|
|
className={`flex-1 max-w-xs mx-auto ${imageCount === 2 ? "md:w-1/2" : imageCount === 3 ? "md:w-1/3" : "md:w-1/4"}`}
|
|
>
|
|
<div className="relative w-full overflow-hidden rounded-lg bg-muted shadow-sm">
|
|
{img.width && img.height ? (
|
|
<Image
|
|
src={finalSrc}
|
|
alt={alt}
|
|
width={constrainedWidth}
|
|
height={constrainedHeight}
|
|
className="object-contain w-full h-auto"
|
|
sizes={
|
|
imageCount === 2
|
|
? "(max-width: 768px) 100vw, 50vw"
|
|
: imageCount === 3
|
|
? "(max-width: 768px) 100vw, 33vw"
|
|
: "(max-width: 768px) 100vw, 25vw"
|
|
}
|
|
priority={
|
|
prioritizeFirstImage && !hasPrioritizedImage
|
|
}
|
|
/>
|
|
) : (
|
|
<div className="relative aspect-video w-full bg-muted">
|
|
<Image
|
|
src={finalSrc}
|
|
alt={alt}
|
|
fill
|
|
className="object-contain"
|
|
sizes={
|
|
imageCount === 2
|
|
? "(max-width: 768px) 100vw, 50vw"
|
|
: imageCount === 3
|
|
? "(max-width: 768px) 100vw, 33vw"
|
|
: "(max-width: 768px) 100vw, 25vw"
|
|
}
|
|
priority={
|
|
prioritizeFirstImage && !hasPrioritizedImage
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
if (prioritizeFirstImage && !hasPrioritizedImage) {
|
|
hasPrioritizedImage = true
|
|
}
|
|
} else if (element.type === "image") {
|
|
const imagePath = getImagePath(element.src, imageMapping)
|
|
const alt = getImageAlt(element.src, imageMapping, element.alt)
|
|
|
|
// Use the mapped local path if available, otherwise fix malformed URLs
|
|
let finalSrc = imagePath
|
|
|
|
// If imagePath is a local path (starts with /), use it directly
|
|
if (imagePath.startsWith("/") && !imagePath.startsWith("//")) {
|
|
finalSrc = imagePath
|
|
} else {
|
|
// Fix malformed URLs in imagePath first
|
|
if (
|
|
imagePath.startsWith("https:///") ||
|
|
imagePath.startsWith("http:///")
|
|
) {
|
|
finalSrc = imagePath.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
imagePath.startsWith("https://wp-content") ||
|
|
imagePath.startsWith("http://wp-content")
|
|
) {
|
|
finalSrc = imagePath.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
} else if (imagePath.startsWith("http")) {
|
|
finalSrc = imagePath // Keep as-is if valid URL
|
|
} else {
|
|
// Fallback to original src and fix it
|
|
finalSrc = element.src
|
|
if (
|
|
finalSrc.startsWith("https:///") ||
|
|
finalSrc.startsWith("http:///")
|
|
) {
|
|
finalSrc = finalSrc.replace(
|
|
/^https?:\/\//,
|
|
"https://rockymountainvending.com/"
|
|
)
|
|
} else if (
|
|
finalSrc.startsWith("https://wp-content") ||
|
|
finalSrc.startsWith("http://wp-content")
|
|
) {
|
|
finalSrc = finalSrc.replace(
|
|
/^https?:\/\/wp-content/,
|
|
"https://rockymountainvending.com/wp-content"
|
|
)
|
|
} else if (finalSrc.startsWith("http")) {
|
|
finalSrc = finalSrc // Keep as-is if valid URL
|
|
} else if (finalSrc.startsWith("/")) {
|
|
finalSrc = `https://rockymountainvending.com${finalSrc}`
|
|
} else {
|
|
finalSrc = `https://rockymountainvending.com/${finalSrc}`
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if this image was in a column layout by looking at surrounding HTML
|
|
// Look for elementor-col classes near this image's position
|
|
const imageContext = sectionHtml.substring(
|
|
Math.max(0, element.index - 1000),
|
|
Math.min(sectionHtml.length, element.index + 1000)
|
|
)
|
|
const hasColumn33 = /elementor-col-33|elementor-col-25/i.test(
|
|
imageContext
|
|
)
|
|
const hasColumn50 = /elementor-col-50/i.test(imageContext)
|
|
const hasColumn66 = /elementor-col-66|elementor-col-75/i.test(
|
|
imageContext
|
|
)
|
|
|
|
// Check if there are multiple images in the same section (side-by-side layout)
|
|
const imagesInSection = images.filter(
|
|
(img) =>
|
|
img.index >= element.index - 2000 &&
|
|
img.index <= element.index + 2000 &&
|
|
img.index !== element.index
|
|
)
|
|
|
|
// Determine container width based on original layout - follow style guide
|
|
let containerClass = "w-full"
|
|
let maxWidthClass = "max-w-md"
|
|
let imageMaxWidth = 600 // Max 600px for standalone images per style guide
|
|
let imageMaxHeight = 600
|
|
|
|
if (
|
|
hasColumn33 ||
|
|
(imagesInSection.length >= 2 && imagesInSection.length < 4)
|
|
) {
|
|
containerClass = "w-full md:w-1/3"
|
|
maxWidthClass = "max-w-xs"
|
|
imageMaxWidth = 300 // Max 300px for grid images per style guide
|
|
imageMaxHeight = 300
|
|
} else if (hasColumn50 || imagesInSection.length === 1) {
|
|
containerClass = "w-full md:w-1/2"
|
|
maxWidthClass = "max-w-sm"
|
|
imageMaxWidth = 300 // Max 300px for grid images per style guide
|
|
imageMaxHeight = 300
|
|
} else if (hasColumn66) {
|
|
containerClass = "w-full md:w-2/3"
|
|
maxWidthClass = "max-w-md"
|
|
imageMaxWidth = 500 // Slightly larger for 2/3 width
|
|
imageMaxHeight = 500
|
|
}
|
|
|
|
// Constrain image dimensions per style guide
|
|
const constrainedWidth = element.width
|
|
? Math.min(element.width, imageMaxWidth)
|
|
: imageMaxWidth
|
|
const constrainedHeight = element.height
|
|
? Math.min(element.height, imageMaxHeight)
|
|
: imageMaxHeight
|
|
|
|
sectionComponents.push(
|
|
<div
|
|
key={`image-${sectionIndex}-${element.index}`}
|
|
className={`my-6 ${containerClass} ${maxWidthClass} mx-auto`}
|
|
>
|
|
<div className="relative w-full overflow-hidden rounded-[1.5rem] border border-border/60 bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(247,244,236,0.94))] p-3 shadow-[0_10px_28px_rgba(15,23,42,0.06)]">
|
|
{element.width && element.height ? (
|
|
<Image
|
|
src={finalSrc}
|
|
alt={alt}
|
|
width={constrainedWidth}
|
|
height={constrainedHeight}
|
|
className="h-auto w-full rounded-[1rem] object-contain"
|
|
sizes={
|
|
hasColumn33
|
|
? "(max-width: 768px) 100vw, 33vw"
|
|
: hasColumn50
|
|
? "(max-width: 768px) 100vw, 50vw"
|
|
: hasColumn66
|
|
? "(max-width: 768px) 100vw, 66vw"
|
|
: "(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
|
|
}
|
|
priority={prioritizeFirstImage && !hasPrioritizedImage}
|
|
/>
|
|
) : (
|
|
<div className="relative aspect-video w-full rounded-[1rem] bg-muted">
|
|
<Image
|
|
src={finalSrc}
|
|
alt={alt}
|
|
fill
|
|
className="object-contain"
|
|
sizes={
|
|
hasColumn33
|
|
? "(max-width: 768px) 100vw, 33vw"
|
|
: hasColumn50
|
|
? "(max-width: 768px) 100vw, 50vw"
|
|
: hasColumn66
|
|
? "(max-width: 768px) 100vw, 66vw"
|
|
: "(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
|
|
}
|
|
priority={prioritizeFirstImage && !hasPrioritizedImage}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
if (prioritizeFirstImage && !hasPrioritizedImage) {
|
|
hasPrioritizedImage = true
|
|
}
|
|
} else if (element.type === "paragraph") {
|
|
// Process paragraph HTML to handle nested tags (em, strong, links, etc.)
|
|
let processedHtml = element.content
|
|
|
|
// Convert strong/bold
|
|
processedHtml = processedHtml.replace(
|
|
/<strong[^>]*>(.*?)<\/strong>/gi,
|
|
"<strong>$1</strong>"
|
|
)
|
|
processedHtml = processedHtml.replace(
|
|
/<b[^>]*>(.*?)<\/b>/gi,
|
|
"<strong>$1</strong>"
|
|
)
|
|
|
|
// Convert emphasis/italic
|
|
processedHtml = processedHtml.replace(
|
|
/<em[^>]*>(.*?)<\/em>/gi,
|
|
"<em>$1</em>"
|
|
)
|
|
processedHtml = processedHtml.replace(
|
|
/<i[^>]*>(.*?)<\/i>/gi,
|
|
"<em>$1</em>"
|
|
)
|
|
|
|
// Convert links
|
|
processedHtml = processedHtml.replace(
|
|
/<a[^>]+href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi,
|
|
(_match: string, href: string, text: string) => {
|
|
return `<a href="${href}" class="text-secondary hover:underline font-medium">${text}</a>`
|
|
}
|
|
)
|
|
|
|
// Clean HTML entities
|
|
processedHtml = cleanHtmlEntities(processedHtml)
|
|
|
|
sectionComponents.push(
|
|
<p
|
|
key={`para-${sectionIndex}-${element.index}`}
|
|
className="mb-6 text-base leading-7 text-muted-foreground md:text-lg md:leading-8"
|
|
dangerouslySetInnerHTML={{ __html: processedHtml }}
|
|
/>
|
|
)
|
|
} else if (element.type === "list") {
|
|
const listElement = element as {
|
|
type: "list"
|
|
listType: "ul" | "ol"
|
|
items: Array<{ text: string; html: string }>
|
|
index: number
|
|
}
|
|
const ListTag = listElement.listType === "ul" ? "ul" : "ol"
|
|
|
|
// Process list items to handle nested HTML
|
|
const processedItems = listElement.items.map(
|
|
(item: { text: string; html: string }) => {
|
|
// Extract and process nested HTML (strong, em, links, etc.)
|
|
let processed = item.html
|
|
|
|
// Convert strong/bold
|
|
processed = processed.replace(
|
|
/<strong[^>]*>(.*?)<\/strong>/gi,
|
|
"<strong>$1</strong>"
|
|
)
|
|
processed = processed.replace(
|
|
/<b[^>]*>(.*?)<\/b>/gi,
|
|
"<strong>$1</strong>"
|
|
)
|
|
|
|
// Convert emphasis/italic
|
|
processed = processed.replace(
|
|
/<em[^>]*>(.*?)<\/em>/gi,
|
|
"<em>$1</em>"
|
|
)
|
|
processed = processed.replace(
|
|
/<i[^>]*>(.*?)<\/i>/gi,
|
|
"<em>$1</em>"
|
|
)
|
|
|
|
// Convert links
|
|
processed = processed.replace(
|
|
/<a[^>]+href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi,
|
|
(match, href, text) => {
|
|
return `<a href="${href}" class="text-secondary hover:underline font-medium">${text}</a>`
|
|
}
|
|
)
|
|
|
|
// Clean remaining HTML entities
|
|
processed = cleanHtmlEntities(processed)
|
|
|
|
return processed
|
|
}
|
|
)
|
|
|
|
sectionComponents.push(
|
|
<ListTag
|
|
key={`list-${sectionIndex}-${listElement.index}`}
|
|
className={`mb-8 space-y-3 ${
|
|
listElement.listType === "ul" ? "list-disc" : "list-decimal"
|
|
} ml-6 text-base text-muted-foreground md:text-lg`}
|
|
>
|
|
{processedItems.map((item: string, itemIndex: number) => (
|
|
<li
|
|
key={itemIndex}
|
|
className="leading-relaxed"
|
|
dangerouslySetInnerHTML={{ __html: item }}
|
|
/>
|
|
))}
|
|
</ListTag>
|
|
)
|
|
} else if (element.type === "machine-cards") {
|
|
// Render machine cards using the same component from machines-we-use page
|
|
sectionComponents.push(
|
|
<div
|
|
key={`machine-cards-${sectionIndex}-${element.index}`}
|
|
className="my-12"
|
|
>
|
|
<div className="grid gap-6 md:gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
{gymMachines.map((machine) => (
|
|
<VendingMachineCard
|
|
key={machine.name}
|
|
name={machine.name}
|
|
description={machine.description}
|
|
image={machine.image}
|
|
alt={machine.alt}
|
|
selections={machine.selections}
|
|
items={machine.items}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
})
|
|
|
|
// If we have components, wrap them in a section
|
|
if (sectionComponents.length > 0) {
|
|
components.push(
|
|
<section key={`section-${sectionIndex}`} className="mb-12 last:mb-0">
|
|
{sectionComponents}
|
|
</section>
|
|
)
|
|
}
|
|
})
|
|
|
|
// If no components were created, create a fallback
|
|
if (components.length === 0) {
|
|
const textContent = cleanHtmlEntities(
|
|
html
|
|
.replace(/<[^>]+>/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim()
|
|
)
|
|
|
|
if (textContent.length > 50) {
|
|
components.push(
|
|
<div key="fallback" className="prose prose-lg max-w-none">
|
|
<p className="mb-4 text-muted-foreground leading-relaxed">
|
|
{textContent}
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
return components
|
|
} catch (error) {
|
|
// Enhanced error logging for development
|
|
if (process.env.NODE_ENV === "development") {
|
|
console.error("Error cleaning WordPress content:", error)
|
|
if (error instanceof Error) {
|
|
console.error("Error message:", error.message)
|
|
console.error("Error stack:", error.stack)
|
|
}
|
|
console.error("HTML length:", html?.length || 0)
|
|
console.error(
|
|
"HTML preview:",
|
|
html?.substring(0, 500) || "No HTML provided"
|
|
)
|
|
} else {
|
|
// Minimal logging in production
|
|
console.error("Error processing WordPress content")
|
|
}
|
|
|
|
// Return error fallback with raw content preview
|
|
return [
|
|
<div key="error" className="prose max-w-none">
|
|
<p className="text-red-600 mb-4">Error processing content.</p>
|
|
{process.env.NODE_ENV === "development" && error instanceof Error && (
|
|
<p className="text-sm text-muted-foreground mb-2">{error.message}</p>
|
|
)}
|
|
{html && html.length > 0 && (
|
|
<div
|
|
className="prose prose-lg max-w-none"
|
|
dangerouslySetInnerHTML={{
|
|
__html: html,
|
|
}}
|
|
/>
|
|
)}
|
|
</div>,
|
|
]
|
|
}
|
|
}
|