svarog-ui-core
Version:
Core components of Svarog UI without themes
322 lines (280 loc) • 9.52 kB
JavaScript
// src/components/BlogDetail/BlogDetail.js
import {
createComponent,
createElement,
} from '../../utils/componentFactory.js';
import { withThemeAwareness } from '../../utils/composition.js';
import { validateRequiredProps } from '../../utils/validation.js';
// CSS injection imports
import { createStyleInjector } from '../../utils/styleInjection.js';
import { blogDetailStyles } from './BlogDetail.styles.js';
// Create style injector for BlogDetail component
const injectBlogDetailStyles = createStyleInjector('BlogDetail');
/**
* Creates a BlogDetail component for displaying full blog post content
*
* @param {Object} props - BlogDetail properties
* @param {string} [props.title=''] - Blog post title
* @param {string} [props.content=''] - Blog post HTML content
* @param {string} [props.imageUrl=''] - URL of featured image
* @param {string} [props.featuredImage=''] - DEPRECATED: Use imageUrl instead
* @param {string} [props.publishedDate=''] - Published date string
* @param {string} [props.author=''] - Author name
* @param {Array} [props.categories=[]] - Post categories
* @param {string} [props.className=''] - Additional CSS class names
* @returns {Object} BlogDetail component API
*/
const createBlogDetail = (props) => {
// Inject styles on component creation
injectBlogDetailStyles(blogDetailStyles);
// Migrate legacy props
const migrateLegacyProps = (originalProps) => {
const migratedProps = { ...originalProps };
if ('featuredImage' in originalProps && !('imageUrl' in originalProps)) {
console.warn(
'[BlogDetail] featuredImage is deprecated, use imageUrl instead'
);
migratedProps.imageUrl = originalProps.featuredImage;
}
return migratedProps;
};
// Apply prop migration
const normalizedProps = migrateLegacyProps(props);
// Define prop requirements
const propRequirements = {
title: { required: false, type: 'string' },
content: { required: false, type: 'string' },
imageUrl: { required: false, type: 'string' },
publishedDate: { required: false, type: 'string' },
author: { required: false, type: 'string' },
categories: { required: false, type: 'array' },
className: { required: false, type: 'string' },
};
try {
// Validate required props
validateRequiredProps(normalizedProps, propRequirements, 'BlogDetail');
// Set default props and merge with provided props
const {
title = '',
content = '',
imageUrl = '',
publishedDate = '',
author = '',
categories = [],
className = '',
} = normalizedProps;
/**
* Helper function to format date
* @param {string} dateString - ISO date string
* @returns {string} Formatted date string
*/
const formatDate = (dateString) => {
if (!dateString) {
return '';
}
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
} catch (error) {
console.error('BlogDetail: Error formatting date', error);
return '';
}
};
/**
* Creates the blog detail element using DocumentFragment for performance
* @returns {HTMLElement} Article element
*/
const createArticleElement = () => {
const fragment = document.createDocumentFragment();
const article = createElement('article', {
classes: `blog-detail ${className}`.trim(),
});
try {
// Create header section
const header = createElement('header', {
classes: 'blog-detail__header',
});
// Add title (check if not empty)
if (title) {
const titleElement = createElement('h1', {
classes: 'blog-detail__title',
text: title,
});
header.appendChild(titleElement);
}
// Create metadata section
const metaContainer = createMetaSection();
if (metaContainer.children.length > 0) {
header.appendChild(metaContainer);
}
article.appendChild(header);
// Add featured image if available
if (imageUrl) {
article.appendChild(createImageSection());
}
// Add content section
article.appendChild(createContentSection());
fragment.appendChild(article);
return fragment.firstChild;
} catch (error) {
console.error('BlogDetail: Error creating article element', error);
return createElement('article', {
classes: 'blog-detail blog-detail--error',
text: 'Error rendering blog content',
});
}
};
/**
* Creates the metadata section with author, date, and categories
* @returns {HTMLElement} Metadata container element
*/
const createMetaSection = () => {
const metaContainer = createElement('div', {
classes: 'blog-detail__meta',
});
if (author) {
const authorElement = createElement('span', {
classes: 'blog-detail__author',
text: `By ${author}`,
});
metaContainer.appendChild(authorElement);
}
if (publishedDate) {
const dateElement = createElement('span', {
classes: 'blog-detail__date',
text: formatDate(publishedDate),
});
metaContainer.appendChild(dateElement);
}
if (categories.length > 0) {
const categoriesContainer = createElement('div', {
classes: 'blog-detail__categories',
});
categories.forEach((category, index) => {
const categoryElement = createElement('span', {
classes: 'blog-detail__category',
text: category,
});
categoriesContainer.appendChild(categoryElement);
if (index < categories.length - 1) {
const separator = document.createTextNode(', ');
categoriesContainer.appendChild(separator);
}
});
metaContainer.appendChild(categoriesContainer);
}
return metaContainer;
};
/**
* Creates the featured image section
* @returns {HTMLElement} Image container element
*/
const createImageSection = () => {
const imageContainer = createElement('div', {
classes: 'blog-detail__image-container',
});
const imageElement = createElement('img', {
classes: 'blog-detail__image',
attributes: {
src: imageUrl,
alt: title || 'Blog featured image',
loading: 'lazy',
},
});
imageContainer.appendChild(imageElement);
return imageContainer;
};
/**
* Creates the content section with blog post content
* @returns {HTMLElement} Content container element
*/
const createContentSection = () => {
try {
return createElement('div', {
classes: 'blog-detail__content',
html: content,
});
} catch (error) {
console.error('BlogDetail: Error rendering HTML content', error);
return createElement('div', {
classes: 'blog-detail__content blog-detail__content--error',
text: 'Error rendering blog content',
});
}
};
// Create the article element
const element = createArticleElement();
// Return the public API
return {
/**
* Gets the blog detail element
* @returns {HTMLElement} The blog detail element
*/
getElement() {
return element;
},
/**
* Updates the blog detail with new props
* @param {Object} newProps - New properties to update
* @returns {Object} Updated component
*/
update(newProps) {
try {
const updatedProps = { ...normalizedProps, ...newProps };
const newBlogDetail = createBlogDetail(updatedProps);
// Replace the old element if it's in the DOM
if (element.parentNode) {
element.parentNode.replaceChild(
newBlogDetail.getElement(),
element
);
}
return newBlogDetail;
} catch (error) {
console.error('BlogDetail: Error updating component', error);
return this;
}
},
/**
* Clean up resources
*/
destroy() {
// No event listeners to clean up in this component
// But we include the method for API consistency
},
/**
* Handle theme changes
* @param {string} theme - New theme
* @param {string} previousTheme - Previous theme
*/
onThemeChange(theme, previousTheme) {
// Theme changes are handled through CSS variables
console.debug(
`BlogDetail: theme changed from ${previousTheme} to ${theme}`
);
},
};
} catch (error) {
console.error('BlogDetail: Error creating component', error);
// Return a minimal valid component in case of errors
const errorElement = createElement('div', {
classes: 'blog-detail blog-detail--error',
text: `Error creating BlogDetail: ${error.message}`,
});
return {
getElement: () => errorElement,
update: () => this,
destroy: () => {},
onThemeChange: () => {},
};
}
};
// Create the component factory with theme awareness
export default createComponent(
'BlogDetail',
withThemeAwareness(createBlogDetail)
);