Last active
May 15, 2023 17:10
-
-
Save cdsaenz/c331b81c1639adb5921da4ffc9408144 to your computer and use it in GitHub Desktop.
WordPress Template for JavaScript Blog Articles Load with Infinity Scroll
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Template Name: Blog Infinity | |
* CSDev | |
* Assign to a page called "Blog" or something | |
* Requires Bootstrap 5 but can be easily refactored to your Stylesheet | |
*/ | |
add_action('wp_head', function () { | |
?> | |
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script> | |
<?php | |
}); | |
get_header(); | |
?> | |
<style> | |
.spinner-container { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
.spinner-container:before { | |
content: ""; | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0, 0, 0, 0.5); | |
} | |
.spinner { | |
border: 16px solid #f3f3f3; | |
border-top: 16px solid #316275; | |
border-radius: 50%; | |
width: 60px; | |
height: 60px; | |
animation: spin 2s linear infinite; | |
} | |
@keyframes spin { | |
0% { | |
transform: rotate(0deg); | |
} | |
100% { | |
transform: rotate(360deg); | |
} | |
} | |
.post-thumbnail-placeholder { | |
width: 400px; | |
height: 400px; | |
background-color: #eee; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
.post-thumbnail-placeholder p { | |
color: #999; | |
font-size: 16px; | |
font-weight: bold; | |
} | |
</style> | |
<div id="content" class="site-content"> | |
<main id="main" class="site-main"> | |
<!-- Title & Description --> | |
<header class="page-header mb-4 text-center px-5 pb-4 mx-5"> | |
<div class="text-center"> | |
<h1 class="header-title"> | |
<?php the_title() ?> | |
</h1> | |
<div class="magazine-subtitle"> | |
<?php the_content() ?> | |
</div> | |
</div> | |
</header> | |
<!-- Alpine Posts --> | |
<div x-data="postsInit()" x-init="initData()"> | |
<!-- Category filter --> | |
<div class="container"> | |
<div class="row"> | |
<div class="col-12 col-md-4 pb-1 pb-md-5" id="category-filter"> | |
<span class="d-inline-block me-2"> | |
<label for="category-select"><?= __('Category','csdev') ?></label> | |
</span> | |
<!-- Categories dropdown --> | |
<div class="d-inline-block"> | |
<select name="category-select" id="category-select" x-model="selectedCategory" @change="loadWithNewCategory()"> | |
<option value=""><?= __('All Categories', 'csdev') ?></option> | |
<template x-for="category in categories" :key="category.id"> | |
<option :value="category.id" x-text="category.name"></option> | |
</template> | |
</select> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Posts --> | |
<template x-for="post in posts"> | |
<div class="card horizontal border-0 mb-4"> | |
<div class="row"> | |
<!-- Meta --> | |
<div class="col-12 col-md-2 text-center"> | |
<p class="badge rounded-pill bg-light text-dark" x-text="post.id"></p> | |
<p x-text="post.modified.substring(0, 10)"></p> | |
<!-- categories here --> | |
<div class="category-badge mb-2"> | |
<template x-for="(categoryName, index) in post.categoriesNames" :key="index"> | |
<span x-text="categoryName" class="badge text-bg-secondary bg-opacity-25 text-decoration-none"></span> | |
</template> | |
</div> | |
</div> | |
<!-- Featured Image--> | |
<div class="col-12 col-md-3"> | |
<div class="blog-post-image"> | |
<template x-if="post.thumbnail"> | |
<img :src="post.thumbnail" :alt="post.title.rendered" /> | |
</template> | |
<template x-if="!post.thumbnail"> | |
<div class="post-thumbnail-placeholder rounded"> | |
<p><?= __('No image available', 'csdev') ?></p> | |
</div> | |
</template> | |
</div> | |
</div> | |
<!-- Titulo --> | |
<div class="col-12 col-md-5"> | |
<div class="card-body"> | |
<!-- Title --> | |
<h2 class="h3 blog-post-title"> | |
<a :href="post.link"> | |
<span x-text="post.title.rendered"></span> | |
</a> | |
</h2> | |
<!-- Excerpt & Read more --> | |
<div class="card-text mt-auto fst-italic"> | |
<span x-html="post.excerpt.rendered"></span> | |
<a class="read-more" :href="post.link"> | |
<?php _e('Read More', 'csdev'); ?> | |
</a> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> | |
<!-- Load More Button - alternative to Infinite Scroll --> | |
<template x-if="nextPage <= totalPages && !isLoading"> | |
<div class="text-center"> | |
<button @click.prevent="fetchPosts(nextPage)" class="btn btn-lg btn-primary"> | |
<?= __('Load More', 'csdev') ?> | |
</button> | |
</div> | |
</template> | |
<!-- Spinner --> | |
<div x-show="isLoading" class="spinner-container"> | |
<div class="spinner"></div> | |
</div> | |
<!-- Infinite Scroll Observed --> | |
<div id="loader"></div> | |
</div> | |
</main><!-- #main --> | |
</div> | |
<?php get_footer(); ?> | |
<script> | |
function postsInit() { | |
return { | |
isLoading: true, | |
posts: [], | |
categories: [], | |
selectedCategory: "", | |
currentPage: 0, | |
nextPage: 0, | |
totalPages: 0, | |
async initData() { | |
await this.fetchCategories(); | |
this.fetchPosts(1); | |
this.setupInfiniteScroll(); | |
}, | |
/* ASYNC Fetch categories from WP REST API */ | |
async fetchCategories() { | |
let catsURL = '<?= rest_url('wp/v2/') ?>categories'; | |
const response = await fetch(catsURL); | |
const data = await response.json(); | |
this.categories = data.filter(category => category.count > 0); | |
}, | |
/* reload but clear everything */ | |
loadWithNewCategory() { | |
this.posts = []; | |
this.fetchPosts(1, this.selectedCategory); | |
}, | |
/* obserer */ | |
setupInfiniteScroll() { | |
// Create a new Intersection Observer instance | |
const observer = new IntersectionObserver((entries) => { | |
// When the observed element enters the viewport, load more posts | |
if (entries[0].isIntersecting) { | |
if (this.nextPage <= this.totalPages && !this.isLoading) { | |
this.fetchPosts(this.nextPage); | |
} | |
} | |
}, { | |
// Set the threshold to 0 to get it triggered when any part is visible | |
// or adjust the margins below | |
rootMargin: '0px 0px 100px 0px', | |
}); | |
// Observe the element at the bottom of the page | |
const loader = document.getElementById('loader'); | |
observer.observe(loader); | |
}, | |
/* load EXTRA posts for a page (and category if any selected )*/ | |
fetchPosts(page) { | |
let postsURL = `<?= rest_url('wp/v2/') ?>posts?per_page=2&page=${page}&_embed`; | |
let mediaURL = '<?= rest_url('wp/v2/') ?>media'; | |
if (this.selectedCategory) { | |
postsURL = `<?= rest_url('wp/v2/') ?>posts?per_page=2&page=${page}&categories=${this.selectedCategory}&_embed`; | |
} | |
this.isLoading = true; | |
fetch(postsURL) | |
.then(res => { | |
this.totalPages = res.headers.get('X-WP-Totalpages'); | |
if (res.ok) { | |
return res.json(); | |
} else { | |
throw new Error('Error fetching data'); | |
} | |
}) | |
.then(data => { | |
// Add the posts to the array | |
data.forEach(post => { | |
// Add the thumbnail URL to the post object | |
if (post._embedded['wp:featuredmedia'] && post._embedded['wp:featuredmedia'][0].source_url) { | |
post.thumbnail = post._embedded['wp:featuredmedia'][0].source_url; | |
} else { | |
post.thumbnail = false; | |
} | |
// add category name(s) to post | |
post.categoriesNames = post.categories.map(catId => { | |
const cat = this.categories.find(cat => cat.id === catId); | |
return cat ? cat.name : ''; | |
}); | |
this.posts.push(post); | |
}); | |
this.currentPage = page; | |
this.nextPage = this.currentPage + 1; | |
/* A little wait so it's all rendered */ | |
setTimeout(() => { | |
this.isLoading = false; | |
}, 500); | |
}); | |
} | |
} | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment