"
);
}
return (
'
"
);
}
function escapeAttribute(value) {
return String(value || "")
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(//g, ">");
}
function buildFeaturedCard(post) {
var title = decodeEntities(post.title && post.title.rendered);
var excerpt = truncate(stripHtml(post.excerpt && post.excerpt.rendered), 175);
var category = getCategoryName(post);
return (
'
' +
createImageMarkup(post, "wsn-featured-image") +
'' +
'
' +
'' +
escapeAttribute(category) +
"" +
'' +
formatDate(post.date) +
"" +
"
" +
"
" +
(excerpt ? "
" + escapeAttribute(excerpt) + "
" : "") +
'
' +
"Read the story →" +
"" +
"
" +
""
);
}
function buildPostCard(post) {
var title = decodeEntities(post.title && post.title.rendered);
var excerpt = truncate(stripHtml(post.excerpt && post.excerpt.rendered), 155);
var category = getCategoryName(post);
return (
'
' +
createImageMarkup(post, "wsn-post-image") +
'' +
'
' +
'' +
escapeAttribute(category) +
"" +
'' +
formatDate(post.date) +
"" +
"
" +
"
" +
(excerpt ? "
" + escapeAttribute(excerpt) + "
" : "") +
'
' +
"Read article →" +
"" +
"
" +
""
);
}
function renderMessage(container, title, message) {
container.innerHTML =
'
' +
"
" + escapeAttribute(title) + "
" +
"
" + escapeAttribute(message) + "
" +
"
";
}
async function loadFeaturedPosts() {
try {
var response = await fetch(
API_BASE +
"/posts?_embed=1&per_page=" +
FEATURED_COUNT +
"&orderby=date&order=desc",
{
credentials: "same-origin"
}
);
if (!response.ok) {
throw new Error("Unable to load featured posts.");
}
var posts = await response.json();
state.featuredIds = posts.map(function (post) {
return post.id;
});
if (!posts.length) {
renderMessage(
featuredGrid,
"No stories published yet",
"New WebSafe Samoa stories will appear here after publication."
);
return;
}
featuredGrid.innerHTML = posts.map(buildFeaturedCard).join("");
} catch (error) {
renderMessage(
featuredGrid,
"Latest stories could not be loaded",
"Please refresh the page or check again shortly."
);
}
}
async function loadCategories() {
try {
var response = await fetch(
API_BASE +
"/categories?per_page=100&hide_empty=true&orderby=count&order=desc",
{
credentials: "same-origin"
}
);
if (!response.ok) {
throw new Error("Unable to load categories.");
}
var categories = await response.json();
categories
.filter(function (category) {
return category.slug !== "uncategorized";
})
.forEach(function (category) {
var button = document.createElement("button");
button.className = "wsn-category-button";
button.type = "button";
button.dataset.category = String(category.id);
button.textContent = decodeEntities(category.name);
categoryList.appendChild(button);
});
} catch (error) {
/* The All filter remains available if category loading fails. */
}
}
function showArchiveSkeletons() {
postGrid.innerHTML =
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
"
" +
"
" +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
"
" +
"
" +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
"
" +
"
";
}
function buildPostsEndpoint() {
var parameters = new URLSearchParams({
_embed: "1",
per_page: String(POSTS_PER_PAGE),
page: String(state.page),
orderby: "date",
order: "desc"
});
if (state.category) {
parameters.set("categories", String(state.category));
}
if (state.search) {
parameters.set("search", state.search);
}
return API_BASE + "/posts?" + parameters.toString();
}
function updateFilterInterface() {
var categoryButtons = categoryList.querySelectorAll(
".wsn-category-button"
);
categoryButtons.forEach(function (button) {
var selected =
Number(button.dataset.category || 0) === Number(state.category);
button.classList.toggle("is-active", selected);
button.setAttribute("aria-pressed", selected ? "true" : "false");
});
resetButton.classList.toggle(
"is-visible",
Boolean(state.category || state.search)
);
}
async function loadPosts(options) {
options = options || {};
if (state.loading) {
return;
}
state.loading = true;
postGrid.setAttribute("aria-busy", "true");
loadMoreButton.disabled = true;
loadMoreButton.textContent = "Loading…";
if (options.reset) {
state.page = 1;
showArchiveSkeletons();
}
try {
var response = await fetch(buildPostsEndpoint(), {
credentials: "same-origin"
});
if (!response.ok) {
throw new Error("Unable to load posts.");
}
var posts = await response.json();
state.totalPages = Number(
response.headers.get("X-WP-TotalPages") || 1
);
state.totalPosts = Number(
response.headers.get("X-WP-Total") || posts.length
);
/*
* On the unfiltered first page, remove the three featured posts from
* the archive grid to avoid immediate duplication. Filtered/search
* results always show every matching post.
*/
var displayedPosts = posts;
if (
state.page === 1 &&
!state.category &&
!state.search &&
state.featuredIds.length
) {
displayedPosts = posts.filter(function (post) {
return state.featuredIds.indexOf(post.id) === -1;
});
}
var markup = displayedPosts.map(buildPostCard).join("");
if (state.page === 1) {
if (markup) {
postGrid.innerHTML = markup;
} else {
renderMessage(
postGrid,
"No additional stories yet",
"The newest stories are featured above. More posts will appear here as they are published."
);
}
} else if (markup) {
postGrid.insertAdjacentHTML("beforeend", markup);
}
var resultLabel = state.totalPosts === 1 ? "post" : "posts";
resultsCount.textContent =
state.totalPosts + " " + resultLabel + " found";
loadMoreButton.hidden = state.page >= state.totalPages;
loadMoreButton.disabled = false;
loadMoreButton.textContent = "Load more posts";
updateFilterInterface();
} catch (error) {
if (state.page === 1) {
renderMessage(
postGrid,
"News could not be loaded",
"Please refresh the page or check again shortly."
);
}
resultsCount.textContent = "Unable to load posts";
loadMoreButton.hidden = true;
} finally {
state.loading = false;
postGrid.setAttribute("aria-busy", "false");
loadMoreButton.disabled = false;
loadMoreButton.textContent = "Load more posts";
}
}
categoryList.addEventListener("click", function (event) {
var button = event.target.closest(".wsn-category-button");
if (!button) {
return;
}
state.category = Number(button.dataset.category || 0);
loadPosts({ reset: true });
});
searchForm.addEventListener("submit", function (event) {
event.preventDefault();
state.search = searchInput.value.trim();
loadPosts({ reset: true });
});
resetButton.addEventListener("click", function () {
state.category = 0;
state.search = "";
searchInput.value = "";
loadPosts({ reset: true });
});
loadMoreButton.addEventListener("click", function () {
if (state.page