@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.
695 lines (628 loc) • 22.4 kB
text/typescript
/**
*
* Lit Blog List Element
*
* */
// Import litelement base class, html helper function & typescript decorators
import { html, css, CSSResult, SkhemataBase, property } from '@skhemata/skhemata-base';
import {
faCalendarAlt,
faFolder,
faTag,
} 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';
import { customDateFormat } from '@skhemata/skhemata-base/dist/directives/customDateFormat';
import { decodeHtmlEntities } from '@skhemata/skhemata-base/dist/directives/decodeHtmlEntities';
import { SkhemataBlogListStyle } from '../style/SkhemataBlogListStyle';
import { SkhemataBlogSharedStyle } from '../style/SkhemataBlogSharedStyle';
import { translationEngDefault } from '../translation/SkhemataBlogList/eng';
// Import element dependencies
import './SkhemataBlogSearch';
export class SkhemataBlogList extends SkhemataBase {
// Property decorator (requires TypeScript or Babel)
// Attributes that can be passed into different elements
apiWordpress = {
url: ''
};
blogPagePath = '';
postsPerPage = 4;
pagerType = "infinite";
currentPage = 1;
searchedBlogPosts = '';
private blogPosts: any = [];
private blogFeatures: any = [];
totalPages = 0;
totalCount = 0;
maxLoadCount = 0;
private count = 1;
translationData = {
eng: translationEngDefault,
};
static get styles() {
return <CSSResult[]>[
super.styles,
SkhemataBlogListStyle,
SkhemataBlogSharedStyle,
css`
.traditional-pager {
text-align: center;
}
`
];
}
static get scopedElements() {
return {
'fa-icon': FontAwesomeIcon,
};
}
constructor() {
// Always call super() first
super();
window.addEventListener(
'popstate',
() => {
if(this.pagerType === "traditional") {
this.loadPostPage();
} else {
this.getPosts();
}
},
false
);
}
willUpdate(changedProperties: Map<string, any>){
if(changedProperties.has('apiWordpress')){
this.getPosts();
}
if(changedProperties.has('postsPerPage')){
this.getPosts();
}
if(changedProperties.has('pagerType')){
this.getPosts();
}
super.willUpdate(changedProperties);
}
/**
* dispatch navigate event
* @param slug post slug
*/
navigateToPost(slug: string) {
this.dispatchEvent(
new CustomEvent('navigate', {
detail: {
slug,
},
composed: true,
bubbles: true,
})
);
window.dispatchEvent(new CustomEvent('clearblogsearch'));
}
/**
* Implement `render` to define a template for your element.
* Use JS template literals
*/
protected render() {
let previous = html``;
let next = html``;
let previousPages = html``;
let nextPages = html``;
let count = 1;
const params = new URLSearchParams(window.location.search);
let page = 1;
const leadingTrailingCount = 3;
const pParam = params.get('p');
if(pParam != null && !Number.isNaN(pParam) ) {
const pageParam = parseInt(pParam, 10);
page = pageParam;
}
previous = html`<button ="${() => this.setPageNumber(1)}" class="button" > First </button>
<button ="${() => this.setPageNumber(page-1)}" class="button" > Previous </button>`;
if (page <= 1) {
previous = html``;
}
next = html`<button ="${() => this.setPageNumber(page+1)}" class="button" > Next </button>
<button ="${() => this.setPageNumber(this.totalPages)}" class="button" > Last </button>`;
if (page >= this.totalPages) {
next = html``;
}
for (let index = page; index > 1; index -= 1) {
if(count > leadingTrailingCount) {
break;
}
previousPages = html`<button ="${(event: any) => this.goToButtonPage(event)}" page-value="${page-count}" class="button" >${page-count}</button>${previousPages}`;
count += 1;
}
count = 1;
for (let index = page; index < this.totalPages; index += 1) {
if(count > leadingTrailingCount) {
break;
}
nextPages = html`${nextPages}<button ="${(event: any) => this.goToButtonPage(event)}" page-value="${page+count}" class="button" >${page+count}</button>`;
count += 1;
}
let pagination = html``;
if (this.totalPages > 1) {
pagination = html`
<div class="traditional-pager">
${previous}${previousPages}<button ="" class="button" ><b>${page}</b></button>${nextPages}${next}
</div>`;
}
return html`
${this.blogFeatures.map(
(post: any) =>
html`
<figure
class="image feature-img blog-featured-img mb-4"
=${() => this.navigateToPost(post.slug)}
=${(e: any) => {
if (e.code === '13') this.navigateToPost(post.slug);
}}
>
${post._embedded['wp:featuredmedia']['0'].source_url !== undefined
? html`<img
src="${post._embedded['wp:featuredmedia']['0'].source_url}"
alt="featured"
/> `
: html``}
<div class="blog-feature-container">
<div class="blog-feature-attr">
<div class="article-date">
<br />
<fa-icon .icon=${faCalendarAlt}></fa-icon>
${this.formatDate(post.date)}
</div>
<div class="feature-ribbon" style="padding: 5px 10px;">
Featured Article
</div>
</div>
<div class="blog-feature-content-container has-text-white">
<strong class="is-size-5 has-text-white"
><span
>${decodeHtmlEntities(post.title.rendered)}</span
></strong
>
<br />
<p class="mb-1">
${stringToHtml(
post.excerpt.rendered
.replace('<p>', '')
.replace('</p>', '')
)}
</p>
</div>
</div>
</figure>
`
)}
<div class="blog-list">
${this.blogPosts.map(
(post: any) => html`
<div class="blog-item card">
<div class="blog-meta desktop">
<span class="blog-post-date">
<fa-icon .icon=${faCalendarAlt}></fa-icon>
${customDateFormat(post.date, 'MMMM DD, YYYY')}
</span>
</div>
<div class="card-content">
<div class="blog-author-info">
<figure class="blog-author-avatar image is-64x64">
<img
src="${post._embedded?.author[0].avatar_urls[96]}"
class="is-rounded"
alt="avatar"
/>
</figure>
<h5>
By
<span
class="blog-author-name"
="${() => {
this.filterPostsBy(post._embedded?.author[0].id, 'a');
}}"
=${(e: any) => {
if (e.keyCode === '13')
this.filterPostsBy(post._embedded?.author[0].id, 'a');
}}
>${post._embedded?.author[0].name}</span
>
</h5>
<div class="blog-meta mobile">
<span class="blog-post-date">
<fa-icon .icon=${faCalendarAlt}></fa-icon>
${customDateFormat(post.date, 'MMMM DD, YYYY')}
</span>
</div>
</div>
<button
class="button is-ghost blog-title"
=${() => this.navigateToPost(post.slug)}
>
<h2 class="title is-4">
${decodeHtmlEntities(post.title.rendered)}
</h2>
</button>
<div class="blog-excerpt">
${stringToHtml(post.excerpt.rendered)}
</div>
<div class="blog-item-meta-info">
<div class="columns">
<div class="column">
<fa-icon .icon=${faFolder}></fa-icon>
${this.getStr('SkhemataBlogList.categories')}:
${post.categories
? post.categories
.filter((obj: any) => {
if (!obj.name.includes('Featured Articles')) {
return obj;
}
return false;
})
.map(
(item: any, index: Number, arr: any) => html`
<div
class="blog-category-item"
value=${item.id}
="${() => {
this.filterPostsBy(item.id, 'c');
}}"
=${(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>
<div class="columns">
<div class="column">
<span class="icon is-small">
<fa-icon .icon=${faTag}></fa-icon>
</span>
${this.getStr('SkhemataBlogList.tags')}:
${post._embedded['wp:term'][1]
? post._embedded['wp:term'][1].map(
(item: any, index: Number, arr: any) => html`
<div
class="blog-category-item"
value=${item.id}
="${() => {
this.filterPostsBy(item.id, 't');
}}"
=${(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>
</div>
</div>
</div>
`
)}
</div>
${(this.pagerType === "traditional") ?
pagination :
html`<div class="load-more-button">
${this.count < this.maxLoadCount
? html`<button ="${this.loadMorePosts}" class="button">
${this.getStr('SkhemataBlogList.showMoreButton')}
</button>`
: ``}
</div>`
}
`;
}
/**
* Implement firstUpdated to perform one-time work after
* the element’s template has been created.
*/
async firstUpdated() {
await super.firstUpdated();
this.getFeatures();
this.getPosts();
}
/**
* Fetch Featured Posts from WP REST API
*/
private getFeatures() {
// Use fetch method to make a request
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
const params = new URLSearchParams(window.location.search);
const search = params.get('s');
let searchParams = '';
let categoryParams = '';
if (search && search.length > 3) {
searchParams = `&search=${search}`;
}
fetch(
`${this.apiWordpress.url}/categories/?search=featured-articles-landing`
)
.then(response => {
if(!response.ok){
return;
}
this.totalPages = Number(response.headers.get('X-WP-TotalPages'));
this.totalCount = Number(response.headers.get('X-WP-Total'));
const contentType = response.headers.get('Content-Type');
// Check if response header content type is json
if (contentType && contentType.includes('application/json')) {
return response.json();
}
return new TypeError('The format is not JSON.');
// Throw error if above condition isn't met
// throw new TypeError('The format is not JSON.');
})
.then(categories => {
if(categories?.length > 0){
categoryParams = `&categories=${categories[0].id}`;
fetch(
`${this.apiWordpress.url}/posts?_embed${searchParams}${categoryParams}&filter[orderby]=date&order=desc`
)
.then(response => {
this.totalPages = Number(response.headers.get('X-WP-TotalPages'));
this.totalCount = Number(response.headers.get('X-WP-Total'));
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 (typeof data !== 'undefined') {
// Loop through data
// data.forEach((element: any) =>
// SkhemataBlogFeatured.formatCategories(element)
// );
this.blogFeatures = data.map(SkhemataBlogList.formatCategories);
}
});
}
})
}
/**
* Fetch Posts from WP REST API
*/
private async getPosts() {
// Use fetch method to make a request
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
const params = new URLSearchParams(window.location.search);
const search = params.get('s');
const category = params.get('c');
const author = params.get('a');
const tag = params.get('t');
let searchParams = '';
let categoryParams = '';
let authorParams = '';
let tagParams = '';
const orderbyParam = `&orderby=relevance`;
if (search && search.length > 3) {
searchParams = `&search=${search}${orderbyParam}`;
}
if (category) {
categoryParams = `&categories=${category}`;
}
if (author) {
authorParams = `&author=${author}`;
}
if (tag) {
tagParams = `&tags=${tag}`;
}
await fetch(
`${this.apiWordpress.url}/posts?_embed${searchParams}${categoryParams}${authorParams}${tagParams}&per_page=${this.postsPerPage}`
)
.then(response => {
if(!response.ok){
return;
}
this.totalPages = Number(response.headers.get('X-WP-TotalPages'));
this.totalCount = Number(response.headers.get('X-WP-Total'));
const contentType = response.headers.get('Content-Type');
this.maxLoadCount = Math.ceil(this.totalCount / 10);
// 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 (typeof data !== 'undefined') {
this.blogPosts = data.map(SkhemataBlogList.formatCategories);
}
}).catch(() => {
this.blogPosts = [];
});
}
/**
* Load more posts event handler
*/
private loadMorePosts() {
this.count += 1;
const params = new URLSearchParams(window.location.search);
const search = params.get('s');
const category = params.get('c');
const author = params.get('a');
const tag = params.get('t');
let tagParams = '';
let searchParams = '';
let categoryParams = '';
let authorParams = '';
const orderbyParam = `&orderby=relevance`;
if (search && search.length > 3) {
searchParams = `&search=${search}${orderbyParam}`;
}
if (category) {
categoryParams = `&categories=${category}`;
}
if (author) {
authorParams = `&author=${author}`;
}
if (tag) {
tagParams = `&tags=${tag}`;
}
fetch(
`${this.apiWordpress.url}/posts?_embed&page=${this.count}${searchParams}${categoryParams}${authorParams}${tagParams}&per_page=${this.postsPerPage}`
)
.then(response => response.json())
.then(data => {
const posts: [] = data;
this.blogPosts = [
...this.blogPosts,
...posts.map(SkhemataBlogList.formatCategories),
];
});
}
private static formatCategories(data: any) {
// Filter out all the categories of the post
const formattedData = data;
if (data && data._embedded) {
const filteredCategories = data._embedded['wp:term'].filter(
(term: any, index: any) =>
term.length > 0 && term[index] ? term[index].taxonomy === 'category' : false
);
[formattedData.categories] = filteredCategories;
}
return formattedData;
}
private formatDate = (date: string) => {
const dateObj = new Date(date);
const month = new Intl.DateTimeFormat('en-US', { month: 'short' }).format(
dateObj
);
return `${month} ${dateObj.getDate()} ${dateObj.getFullYear()}`;
};
/**
* Filters post based on query params
* @param id tag/category/author id
* @param queryId query param id
*/
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'));
}
cleanExcerpt = (excerpt: string = '') =>
excerpt.replace(/<a.*>.*?.<\/span>/g, '');
private goToButtonPage(event: any) {
let page = 1;
const buttonValue = event.target.attributes["page-value"].value;
if(buttonValue != null) {
page = parseInt(buttonValue, 10);
}
this.setPageNumber(page);
}
private setPageNumber(page: any) {
let setPage = page;
if(setPage < 1) {
setPage = 1;
}
if(setPage > this.totalPages) {
setPage = this.totalPages;
}
const params = new URLSearchParams(window.location.search);
params.set('p', setPage);
this.currentPage = setPage;
window.history.pushState(
{},
'',
`/${this.blogPagePath}?${params.toString()}`
);
window.dispatchEvent(new Event('popstate'));
}
private loadPostPage() {
const params = new URLSearchParams(window.location.search);
const page = params.get('p');
const search = params.get('s');
const category = params.get('c');
const author = params.get('a');
const tag = params.get('t');
let tagParams = '';
let searchParams = '';
let categoryParams = '';
let authorParams = '';
const orderbyParam = `&orderby=relevance`;
if (search && search.length > 3) {
searchParams = `&search=${search}${orderbyParam}`;
}
if (category) {
categoryParams = `&categories=${category}`;
}
if (author) {
authorParams = `&author=${author}`;
}
if (tag) {
tagParams = `&tags=${tag}`;
}
fetch(
`${this.apiWordpress.url}/posts?_embed&page=${page}${searchParams}${categoryParams}${authorParams}${tagParams}&per_page=${this.postsPerPage}`
)
.then(response => {
if(response.status === 200) {
this.totalPages = Number(response.headers.get('X-WP-TotalPages'));
this.totalCount = Number(response.headers.get('X-WP-Total'));
const contentType = response.headers.get('Content-Type');
this.maxLoadCount = Math.ceil(this.totalCount/10);
// 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.');
} else {
return response.json();
}
})
.then(response => {
// Set page to 1 if invalid page number.
if(response.code && response.code === "rest_post_invalid_page_number") {
this.setPageNumber(1);
} else if (!response.code && typeof response !== 'undefined') {
this.blogPosts = response.map(SkhemataBlogList.formatCategories);
}
});
}
}