UNPKG

@skhemata/skhemata-blog

Version:

Skhemata Blog Web Component. This web component provides several sub components in addition to main component, allowing featured blogs, blog listing and blog post display.

397 lines (363 loc) 13 kB
/** * * Lit Blog Post Element * * */ // Import litelement base class, html helper function & typescript decorators import { SkhemataBase, html, css, CSSResult, property } from '@skhemata/skhemata-base'; import { faLinkedin, faTwitter, faFacebook, } from '@fortawesome/free-brands-svg-icons'; import { faCalendarAlt, faTag, faFolder, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@riovir/wc-fontawesome'; // Import custom element directives import { stringToHtml } from '@skhemata/skhemata-base/dist/directives/stringToHtml.js'; import { customDateFormat } from '@skhemata/skhemata-base/dist/directives/customDateFormat.js'; import { decodeHtmlEntities } from '@skhemata/skhemata-base/dist/directives/decodeHtmlEntities.js'; import { SkhemataBlogPostStyle } from '../style/SkhemataBlogPostStyle'; import { SkhemataBlogSharedStyle } from '../style/SkhemataBlogSharedStyle'; import { translationEngDefault } from '../translation/SkhemataBlogPost/eng'; export class SkhemataBlogPost extends SkhemataBase { @property({ type: Object, attribute: 'api-wordpress' }) apiWordpress = { url: '' }; @property({ type: String, attribute: 'blog-page-path' }) blogPagePath = ''; @property({ type: Boolean, attribute: 'has-featured-image' }) hasFeaturedImage = false; // Component specific properties @property({ type: String }) slug?: string = ''; @property({ type: Object }) private blogPost: any; @property({ type: Object }) translationData = { eng: translationEngDefault, }; static get styles(): CSSResult[] { return <CSSResult[]>[ ...super.styles, SkhemataBlogPostStyle, SkhemataBlogSharedStyle, css` .blog-category-item { display: inline; cursor: pointer; color: var(--skhemata-blog-link-color, var(--default-blue)); transition: all 0.3s ease 0s; } .blog-category-item:hover { color: rgb(28, 119, 185); } `, ]; } static get scopedElements() { return { 'fa-icon': FontAwesomeIcon, }; } constructor() { super(); window.onhashchange = () => { this.shadowRoot ?.getElementById(window.location.hash.slice(1)) ?.scrollIntoView(); }; } handleGoBack() { this.dispatchEvent( new CustomEvent('navigate', { detail: { slug: '', }, composed: true, bubbles: true, }) ); } /** * Implement `render` to define a template for your element. * Use JS template literals */ protected render() { return this.blogPost ? html` <div class="blog-post-author-info"> <figure class="blog-post-author-avatar image is-64x64"> <img class="is-rounded" src="${this.blogPost.author.avatar_url[96]}" alt="avatar" /> </figure> <h5 class="blog-post-author"> <div> By <span class="blog-author-name" @click="${() => { this.filterPostsBy(this.blogPost.author.id, 'a'); }}" @keydown=${(e: any) => { if (e.keyCode === '13') this.filterPostsBy(this.blogPost.author.id, 'a'); }} >${this.blogPost.author.name}</span > </div> <div class="social-links"> <span class="link-label">Share:</span> <a href="${`https://www.twitter.com/home?status=${window.location}`}" target="_blank" > <fa-icon class="social-icon" .icon=${faTwitter}></fa-icon> </a> <a href="${`https://www.linkedin.com/shareArticle?mini=true&url=${window.location}`}" target="_blank" > <fa-icon class="social-icon" .icon=${faLinkedin}></fa-icon> </a> <a href="${`https://www.facebook.com/sharer/sharer.php?u=${window.location}`}" target="_blank" > <fa-icon class="social-icon" .icon=${faFacebook}></fa-icon> </a> </div> </h5> <span class="blog-post-date" ><fa-icon .icon=${faCalendarAlt}></fa-icon> ${customDateFormat( this.blogPost.date, 'MMMM DD, YYYY' )}</span > </div> <h1 class="blog-post-title title is-2"> ${decodeHtmlEntities(this.blogPost.title)} </h1> ${this.hasFeaturedImage ? html` <figure class="image"> <img class="blog-post-featured-image" src="${this.blogPost.featured_media.source_url}" alt="${this.blogPost.featured_media.alt_text}" title="${this.blogPost.featured_media.title}" /> </figure>` : html``} <div class="blog-post-content content"> ${stringToHtml(this.blogPost.content.replace('slack', 'slick'))} </div> <div class="columns"> <div class="column"> <button class="button back-button is-link is-rounded" @click=${this.handleGoBack} > ${this.getStr('SkhemataBlogPost.backToBlog')} </button> </div> </div> <div class="columns blog-item-meta-info"> <div class="column is-half has-text-left"> <fa-icon .icon=${faFolder}></fa-icon> ${this.getStr('SkhemataBlogPost.categories')}: ${this.blogPost.categories ? this.blogPost.categories.map( (item: any, index: Number, arr: any) => html` <div class="blog-category-item" value=${item.id} @click="${() => { this.filterPostsBy(item.id, 'c'); }}" @keydown=${(e: any) => { if (e.keyCode === '13') this.filterPostsBy(item.id, 'c'); }} > ${item.name} </div> ${arr.length > 1 && arr.length - 1 !== index ? html`, ` : html``} ` ) : ''} </div> <div class="column is-half has-text-right-desktop"> <fa-icon .icon=${faTag}></fa-icon> ${this.getStr('SkhemataBlogPost.tags')}: ${this.blogPost.tags ? this.blogPost.tags.map( (item: any, index: Number, arr: any) => html` <div class="blog-category-item" value=${item.id} @click="${() => { this.filterPostsBy(item.id, 't'); }}" @keydown=${(e: any) => { if (e.keyCode === '13') this.filterPostsBy(item.id, 't'); }} > ${item.name} </div> ${arr.length > 1 && arr.length - 1 !== index ? html`, ` : html``} ` ) : ''} </div> </div> ` : html``; } /** * Implement firstUpdated to perform one-time work after * the element’s template has been created. */ async firstUpdated() { await super.firstUpdated(); this.getPost(); } /** * Fetch a single post based on post id from WP REST API */ private getPost() { // Use fetch method to make a request // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch fetch(`${this.apiWordpress.url}/posts?_embed&slug=${this.slug}`) .then(response => { const contentType = response.headers.get('Content-Type'); // Check if response header content type is json if (contentType && contentType.includes('application/json')) { return response.json(); } // Throw error if above condition isn't met throw new TypeError('The format is not JSON.'); }) .then(data => { if (Array.isArray(data) && data.length > 0) { const postData = data[0]; if (postData && postData.status === 'publish') { // Filter out all the categories of the post const filteredCategories = postData._embedded['wp:term'].filter( (term: any) => { if (term) { for (let index = 0; index < term.length; index += 1) { return term[index].taxonomy === 'category'; } } return true; } ); // Filter out all the tags of the post const filteredTags = postData._embedded['wp:term'].filter( (term: any) => { if (term) { for (let index = 0; index < term.length; index += 1) { return term[index].taxonomy === 'post_tag'; } } return true; } ); const featuredMedia = postData._embedded['wp:featuredmedia'] ? { source_url: postData._embedded['wp:featuredmedia'][0].source_url, alt_text: postData._embedded['wp:featuredmedia'][0].alt_text, title: postData._embedded['wp:featuredmedia'][0].title, } : { source_url: '', alt_text: '', title: '', }; // Pass data to Object to be used to bind data on the template this.blogPost = { id: postData.id, title: postData.title.rendered, content: postData.content.rendered.replaceAll( 'href="#', `href="${window.location.href}#` ), date: postData.date, author: { id: postData._embedded.author[0].id, name: postData._embedded.author[0].name, description: postData._embedded.author[0].description, avatar_url: postData._embedded.author[0].avatar_urls, }, categories: filteredCategories[0], excerpt: postData.excerpt.rendered, tags: filteredTags[0], featured_media: featuredMedia, }; this.setMetaTags(); } } }); } setMetaTags() { const metaDesc = document.querySelector("meta[name='description' i]"); const excerpt = this.blogPost.excerpt.replace(/<[^>]*>?/gm, ''); if (metaDesc) { metaDesc.setAttribute('content', excerpt); } else { const newMetaDesc = document.createElement('meta'); newMetaDesc.name = 'description'; newMetaDesc.content = excerpt; document.getElementsByTagName('head')[0].appendChild(newMetaDesc); } const title = document.querySelector('title'); if (title) { title.innerHTML = this.blogPost.title; } else { const newTitle = document.createElement('title'); newTitle.innerHTML = this.blogPost.title; document.getElementsByTagName('head')[0].appendChild(newTitle); } const metaKeywords = document.querySelector("meta[name='keywords' i]"); let keywordsText = ''; for (let i = 0; i < this.blogPost.tags.length; i += 1) { keywordsText += `${this.blogPost.tags[i].name}, `; } if (metaKeywords) { metaKeywords.setAttribute('content', keywordsText); } else { const newMetaKeywords = document.createElement('meta'); newMetaKeywords.name = 'keywords'; newMetaKeywords.content = keywordsText; document.getElementsByTagName('head')[0].appendChild(newMetaKeywords); } } filterPostsBy(id: string, queryId: string) { const params = new URLSearchParams(window.location.search); if (params.get(queryId) === id) { params.delete(queryId); } else { params.set(queryId, id); } window.history.pushState( {}, '', decodeURIComponent(`${this.blogPagePath}?${params.toString()}`) ); window.scrollTo({ top: 0 }); window.dispatchEvent(new Event('popstate')); } }