:root {
--main-green: #2e6243;
--main-green-dark: #3a7e47;
--main-green-light: #a9ccb5;
--accent-yellow: #f8de4c;
--gray-light: #f8f9fa;
--gray-mid: #e9ecef;
--gray-dark: #555;
--font-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: var(--font-base);
background: #f0f2f5;
color: var(--gray-dark);
}
.testimonials-filter .container {
width: 1640px !important;
margin: 0 auto;
padding: 2rem 3rem;
display: flex;
gap: 2rem;
flex-wrap: nowrap;
}
.filter-section,
.results-section {
background: #fff;
border-radius: 1rem;
padding: 2rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
}
.filter-section {
width: 400px;
flex-shrink: 0;
}
.results-section {
flex: 1;
overflow: hidden;
max-height: 80vh;
overflow-y: auto;
}
.filter-section h2 {
font-size: 1.5rem;
margin-bottom: 1.5rem;
font-weight: 600;
color: #2c3e50;
}
.filter-grid {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.filter-group label {
display: block;
font-size: 0.875rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.5rem;
color: var(--gray-dark);
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
min-height: 48px;
border: 2px solid var(--gray-mid);
border-radius: 0.5rem;
background: white;
transition: 0.3s ease;
}
.search-input:focus {
border-color: var(--main-green-dark);
box-shadow: 0 0 0 3px rgba(46, 98, 67, 0.2);
outline: none;
}
select {
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
min-height: 48px;
border: 2px solid var(--gray-mid);
border-radius: 0.5rem;
background: var(--main-green);
color: white;
appearance: none;
transition: 0.3s ease;
cursor: pointer;
}
select:focus {
border-color: var(--main-green-dark);
box-shadow: 0 0 0 3px rgba(46, 98, 67, 0.2);
outline: none;
}
select option {
background: var(--main-green);
color: white;
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 2rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
transition: 0.2s ease;
}
.btn-primary {
background: var(--main-green);
border: 2px solid var(--main-green);
color: white;
}
.btn-primary:hover {
background: var(--main-green-dark);
border-color: var(--main-green-dark);
box-shadow: 0 4px 12px rgba(46, 98, 67, 0.3);
transform: translateY(-2px);
}
.btn-secondary {
background: var(--main-green-light);
color: white;
}
.btn-secondary:hover {
background: var(--main-green-dark);
transform: translateY(-2px);
}
/* Results Section Styles */
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--gray-mid);
}
.results-header h3 {
font-size: 1.5rem;
margin: 0 0 0.5rem 0;
color: #2c3e50;
}
.results-count {
font-size: 0.875rem;
color: var(--gray-dark);
}
.sort-dropdown {
width: auto;
min-width: 180px;
}
/* Testimonials Grid */
.testimonials-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 1.5rem;
}
.testimonial-item {
background: white;
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: 0.3s ease;
border-left: 4px solid var(--main-green);
cursor: pointer;
text-decoration: none;
color: inherit;
display: block;
}
.testimonial-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
text-decoration: none;
color: inherit;
}
.testimonial-item, .testimonial-item * {
text-decoration: none !important;
}
.testimonial-item .author-info h5 {
text-decoration: underline !important;
}
.testimonial-item:hover .author-info h5 {
text-decoration: underline !important;
text-decoration-thickness: 1.8px !important;
}
.testimonial-content {
padding: 1.5rem;
}
.testimonial-quote {
font-size: 1rem;
line-height: 1.6;
color: #333;
margin-bottom: 1.5rem;
font-style: italic;
position: relative;
}
.testimonial-author {
display: flex;
align-items: center;
gap: 1rem;
}
.author-info h5 {
margin: 0;
color: #2c3e50;
font-size: 1.1rem;
font-weight: 600;
}
.author-info p {
margin: 5px 0 0 0;
color: var(--gray-dark);
font-size: 0.875rem;
}
.testimonial-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.meta-tag {
background: #333;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 500;
}
.testimonial-date {
font-size: 0.75rem;
color: #999;
margin-top: 1rem;
text-align: right;
}
/* Loading and No Results States */
.loading-state,
.no-results {
text-align: center;
padding: 3rem 2rem;
color: var(--gray-dark);
}
.loading-state p {
font-size: 1.1rem;
margin: 0;
}
.no-results h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: #2c3e50;
}
.no-results p {
font-size: 1rem;
margin: 0;
}
/* Pagination */
.pagination-controls {
display: flex;
justify-content: center;
margin-top: 2rem;
}
.load-more-btn {
background: var(--main-green);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: 0.2s ease;
}
.load-more-btn:hover:not(:disabled) {
background: var(--main-green-dark);
transform: translateY(-2px);
}
.load-more-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Error State */
.error-state {
background: #fee;
border: 2px solid #fcc;
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
color: #c33;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.testimonials-filter .container {
flex-direction: column;
padding: 1.5rem;
width: 100% !important;
}
.filter-section {
width: 100%;
}
.results-header {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.testimonials-grid {
grid-template-columns: 1fr;
}
}
Newest First
Oldest First
Name A-Z
Name Z-A
// WordPress REST API Configuration
const WP_CONFIG = {
baseUrl: window.location.origin,
restBase: '/wp-json/wp/v2',
postsEndpoint: '/posts',
testimonialsCategory: 28, // Update this to your actual testimonials category ID
perPage: 100, // Load more at once
maxPages: 50
};
let allTestimonialsData = []; // All testimonials for search
let displayedTestimonialsData = []; // Only first 20 for display
let filteredData = [];
let totalTestimonials = 0;
let currentDisplayPage = 1; // Track current page for display
let isLoading = false;
let isLoadingAll = false; // Background loading state
// Get WordPress nonce for security
function getWordPressNonce() {
if (typeof wpApiSettings !== 'undefined' && wpApiSettings.nonce) {
return wpApiSettings.nonce;
}
const nonceMeta = document.querySelector('meta[name="wp-rest-nonce"]');
if (nonceMeta) {
return nonceMeta.getAttribute('content');
}
return null;
}
// Show error message
function showError(message) {
const errorElement = document.getElementById('error-state');
errorElement.textContent = message;
errorElement.style.display = 'block';
console.error('Testimonials Error:', message);
}
// Hide error message
function hideError() {
const errorElement = document.getElementById('error-state');
errorElement.style.display = 'none';
}
// Fetch first 20 testimonials quickly
async function fetchInitialTestimonials() {
if (isLoading) return [];
try {
isLoading = true;
hideError();
let url = `${WP_CONFIG.baseUrl}${WP_CONFIG.restBase}${WP_CONFIG.postsEndpoint}?_embed&per_page=20&categories=${WP_CONFIG.testimonialsCategory}&orderby=date&order=desc`;
const headers = {
'Content-Type': 'application/json',
};
const nonce = getWordPressNonce();
if (nonce) {
headers['X-WP-Nonce'] = nonce;
}
console.log('Fetching first 20 testimonials...');
const response = await fetch(url, {
method: 'GET',
headers: headers
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Get total count from headers
totalTestimonials = parseInt(response.headers.get('X-WP-Total')) || 20;
const posts = await response.json();
console.log('Fetched initial testimonials:', posts.length);
if (!Array.isArray(posts)) {
throw new Error('Invalid response format from WordPress API');
}
return posts.map(post => transformTestimonial(post));
} catch (error) {
console.error('Error fetching initial testimonials:', error);
showError(`Failed to load testimonials: ${error.message}`);
return [];
} finally {
isLoading = false;
}
}
// Fetch all testimonials in background for search
async function fetchAllTestimonialsBackground() {
if (isLoadingAll) return;
try {
isLoadingAll = true;
console.log('Loading all testimonials in background for search...');
// Update status
const searchStatus = document.getElementById('search-status');
if (searchStatus) {
searchStatus.textContent = 'Loading testimonials in background...';
}
let allTestimonials = [];
let currentPage = 1;
let hasMore = true;
while (hasMore && currentPage 1) {
hasMore = false;
break;
}
console.warn(`Background loading page ${currentPage} failed:`, response.status);
break;
}
const posts = await response.json();
if (!Array.isArray(posts) || posts.length === 0) {
hasMore = false;
break;
}
allTestimonials = allTestimonials.concat(posts);
console.log(`Background loaded page ${currentPage}, got ${posts.length} testimonials. Total: ${allTestimonials.length}`);
// Update status with progress
if (searchStatus) {
searchStatus.textContent = `Loading testimonials... ${allTestimonials.length} loaded`;
}
if (posts.length setTimeout(resolve, 100));
}
console.log('Background loading complete. Total testimonials:', allTestimonials.length);
totalTestimonials = allTestimonials.length;
allTestimonialsData = allTestimonials.map(post => transformTestimonial(post));
// Enable search functionality
enableSearch();
// Update the count display and load more button
updateResultsCount(displayedTestimonialsData.length);
updateLoadMoreButton();
} catch (error) {
console.error('Error in background loading:', error);
// Enable search anyway with limited data
enableSearch();
} finally {
isLoadingAll = false;
}
}
// Enable search functionality
function enableSearch() {
const searchInput = document.getElementById('search-input');
const searchStatus = document.getElementById('search-status');
const applyBtn = document.querySelector('.btn-primary');
const clearBtn = document.querySelector('.btn-secondary');
// Enable search input
searchInput.disabled = false;
searchInput.placeholder = 'Search by name, company, or content...';
searchInput.classList.remove('search-disabled');
// Enable buttons
applyBtn.disabled = false;
applyBtn.classList.remove('search-disabled');
clearBtn.disabled = false;
clearBtn.classList.remove('search-disabled');
// Update status
if (searchStatus) {
searchStatus.textContent = `Search ready! ${totalTestimonials} testimonials loaded.`;
setTimeout(() => {
searchStatus.style.display = 'none';
}, 3000);
}
console.log('Search functionality enabled');
}
// Transform WordPress post to testimonial format
function transformTestimonial(post) {
const content = post.content.rendered.replace(/]*>/g, '').trim();
const tags = post._embedded && post._embedded['wp:term'] && post._embedded['wp:term'][1]
? post._embedded['wp:term'][1]
: [];
// Extract author info from title
const titleParts = post.title.rendered.split(',');
const author = titleParts[0] ? titleParts[0].trim() : 'Anonymous';
const jobTitle = titleParts[1] ? titleParts[1].trim() : '';
const company = titleParts[2] ? titleParts[2].trim() : '';
// Get job title and company from tags as fallback
const jobTitleFromTags = tags.find(tag =>
tag.name.includes('Manager') ||
tag.name.includes('Director') ||
tag.name.includes('Chief') ||
tag.name.includes('President') ||
tag.name.includes('CEO') ||
tag.name.includes('Administrator') ||
tag.name.includes('Clerk') ||
tag.name.includes('Coordinator')
);
const companyFromTags = tags.find(tag =>
tag.name.includes('City of') ||
tag.name.includes('County') ||
tag.name.includes('Corporation') ||
tag.name.includes('Company') ||
tag.name.includes('Authority') ||
tag.name.includes('District') ||
tag.name.includes('Foundation') ||
tag.name.includes('Group')
);
return {
id: post.id,
author: author,
jobTitle: jobTitle || (jobTitleFromTags ? jobTitleFromTags.name : ''),
company: company || (companyFromTags ? companyFromTags.name : ''),
content: content,
date: new Date(post.date),
permalink: post.link,
tags: tags.map(tag => tag.name)
};
}
// Initialize testimonials display - fast!
async function initializeTestimonials() {
showLoading(true);
try {
// Load first 20 quickly
displayedTestimonialsData = await fetchInitialTestimonials();
filteredData = [...displayedTestimonialsData];
console.log('Initial load complete:', displayedTestimonialsData.length);
if (displayedTestimonialsData.length === 0) {
showNoResults();
updateResultsCount(0);
} else {
displayTestimonials(filteredData);
updateResultsCount(displayedTestimonialsData.length);
updateLoadMoreButton(); // Show load more if there are more testimonials
}
showLoading(false);
// Start background loading for search (don't wait for it)
fetchAllTestimonialsBackground();
} catch (error) {
console.error('Error initializing testimonials:', error);
showError('Failed to initialize testimonials');
showLoading(false);
}
}
// Populate filter dropdowns
function populateFilters() {
// Job titles
const jobTitles = [...new Set(testimonialsData
.map(t => t.jobTitle)
.filter(jt => jt)
.sort())];
const jobTitleFilter = document.getElementById('job-title-filter');
jobTitleFilter.innerHTML = 'All Job Titles';
jobTitles.forEach(jobTitle => {
const option = document.createElement('option');
option.value = jobTitle;
option.textContent = jobTitle;
jobTitleFilter.appendChild(option);
});
// Companies
const companies = [...new Set(testimonialsData
.map(t => t.company)
.filter(c => c)
.sort())];
const companyFilter = document.getElementById('company-filter');
companyFilter.innerHTML = 'All Companies';
companies.forEach(company => {
const option = document.createElement('option');
option.value = company;
option.textContent = company;
companyFilter.appendChild(option);
});
}
// Apply filters - searches all if available, first 20 if not
function applyFilters() {
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
if (!searchTerm) {
// If no search term, show first 20 testimonials
filteredData = [...displayedTestimonialsData];
updateResultsCount(displayedTestimonialsData.length);
} else {
// Search across available testimonials (all if loaded, first 20 if still loading)
const searchData = allTestimonialsData.length > 0 ? allTestimonialsData : displayedTestimonialsData;
filteredData = searchData.filter(testimonial => {
return testimonial.author.toLowerCase().includes(searchTerm) ||
testimonial.company.toLowerCase().includes(searchTerm) ||
testimonial.jobTitle.toLowerCase().includes(searchTerm) ||
testimonial.content.toLowerCase().includes(searchTerm);
});
console.log('Search results:', filteredData.length, 'from', searchData.length, 'testimonials');
updateResultsCount(filteredData.length);
}
if (filteredData.length === 0) {
showNoResults();
} else {
displayTestimonials(filteredData);
}
}
// Clear filters
function clearFilters() {
document.getElementById('search-input').value = '';
filteredData = [...displayedTestimonialsData]; // Back to first 20
displayTestimonials(filteredData);
updateResultsCount(displayedTestimonialsData.length);
}
// Sort results
function sortResults() {
const sortValue = document.getElementById('sort-dropdown').value;
filteredData.sort((a, b) => {
switch (sortValue) {
case 'date-desc':
return new Date(b.date) - new Date(a.date);
case 'date-asc':
return new Date(a.date) - new Date(b.date);
case 'name-asc':
return a.author.localeCompare(b.author);
case 'name-desc':
return b.author.localeCompare(a.author);
default:
return 0;
}
});
displayTestimonials(filteredData);
}
// Load more testimonials for display
async function loadMoreTestimonials() {
if (isLoading) return;
const loadMoreBtn = document.getElementById('load-more-btn');
const originalText = loadMoreBtn.textContent;
loadMoreBtn.textContent = 'Loading...';
loadMoreBtn.disabled = true;
try {
isLoading = true;
currentDisplayPage++;
let newTestimonials = [];
// If we have all testimonials loaded in background, use those
if (allTestimonialsData.length > 0) {
const startIndex = (currentDisplayPage - 1) * 20;
const endIndex = startIndex + 20;
newTestimonials = allTestimonialsData.slice(startIndex, endIndex);
console.log(`Loading from cached: ${startIndex}-${endIndex}, got ${newTestimonials.length}`);
} else {
// Fetch from API
newTestimonials = await fetchTestimonialsPage(currentDisplayPage);
console.log(`Loading from API page ${currentDisplayPage}, got ${newTestimonials.length}`);
}
if (newTestimonials.length > 0) {
displayedTestimonialsData = displayedTestimonialsData.concat(newTestimonials);
console.log(`Total displayed now: ${displayedTestimonialsData.length}`);
// Update filtered data and display
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
if (!searchTerm) {
filteredData = [...displayedTestimonialsData];
} else {
// Re-apply search with expanded data
applyFilters();
updateLoadMoreButton();
return; // applyFilters will handle the display
}
displayTestimonials(filteredData);
updateResultsCount(displayedTestimonialsData.length);
} else {
console.log('No more testimonials to load');
// If we got 0 new testimonials, we've reached the end
currentDisplayPage--; // Revert page increment
}
updateLoadMoreButton();
} catch (error) {
console.error('Error loading more testimonials:', error);
showError('Failed to load more testimonials');
currentDisplayPage--; // Revert page increment
} finally {
isLoading = false;
loadMoreBtn.textContent = originalText;
loadMoreBtn.disabled = false;
}
}
// Fetch a specific page of testimonials
async function fetchTestimonialsPage(page) {
try {
let url = `${WP_CONFIG.baseUrl}${WP_CONFIG.restBase}${WP_CONFIG.postsEndpoint}?_embed&per_page=20&page=${page}&categories=${WP_CONFIG.testimonialsCategory}&orderby=date&order=desc`;
const headers = {
'Content-Type': 'application/json',
};
const nonce = getWordPressNonce();
if (nonce) {
headers['X-WP-Nonce'] = nonce;
}
console.log(`Fetching page ${page} testimonials...`);
const response = await fetch(url, {
method: 'GET',
headers: headers
});
if (!response.ok) {
if (response.status === 400) {
// No more pages
return [];
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const posts = await response.json();
if (!Array.isArray(posts)) {
throw new Error('Invalid response format from WordPress API');
}
return posts.map(post => transformTestimonial(post));
} catch (error) {
console.error(`Error fetching page ${page}:`, error);
return [];
}
}
// Display testimonials
function displayTestimonials(testimonials) {
const grid = document.getElementById('testimonials-grid');
const noResults = document.getElementById('no-results');
if (testimonials.length === 0) {
grid.style.display = 'none';
noResults.style.display = 'block';
return;
}
grid.style.display = 'grid';
noResults.style.display = 'none';
grid.innerHTML = testimonials.map(testimonial => {
return `
`;
}).join('');
}
// Show no results
function showNoResults() {
const grid = document.getElementById('testimonials-grid');
const noResults = document.getElementById('no-results');
grid.style.display = 'none';
noResults.style.display = 'block';
}
// Update results count
function updateResultsCount(count) {
const countElement = document.getElementById('results-count');
if (count === totalTestimonials && totalTestimonials > 0) {
countElement.textContent = `Showing all ${totalTestimonials} testimonials`;
} else if (totalTestimonials > 0) {
countElement.textContent = `Showing ${count} of ${totalTestimonials} testimonials`;
} else {
countElement.textContent = 'Loading testimonials...';
}
}
// Update load more button visibility
function updateLoadMoreButton() {
const paginationControls = document.getElementById('pagination-controls');
const loadMoreBtn = document.getElementById('load-more-btn');
let hasMoreToLoad = false;
// Method 1: If we have all testimonials cached, check against that
if (allTestimonialsData.length > 0) {
hasMoreToLoad = displayedTestimonialsData.length 0) {
hasMoreToLoad = displayedTestimonialsData.length 0 && displayedTestimonialsData.length % 20 === 0) {
hasMoreToLoad = true;
console.log(`Modulo check: displayed ${displayedTestimonialsData.length}, assuming more exist`);
}
if (hasMoreToLoad && !isLoading) {
paginationControls.style.display = 'flex';
loadMoreBtn.disabled = false;
loadMoreBtn.textContent = 'Load More';
console.log('✅ Showing load more button');
} else {
paginationControls.style.display = 'none';
console.log('❌ Hiding load more button', { hasMoreToLoad, isLoading });
}
}
// Infinite scroll functionality
function initInfiniteScroll() {
const resultsSection = document.querySelector('.results-section');
let isScrollLoading = false;
resultsSection.addEventListener('scroll', async () => {
if (isScrollLoading || isLoading) return;
const scrollTop = resultsSection.scrollTop;
const scrollHeight = resultsSection.scrollHeight;
const clientHeight = resultsSection.clientHeight;
// Trigger when user scrolls to within 200px of bottom
if (scrollTop + clientHeight >= scrollHeight - 200) {
const hasMoreToLoad = () => {
if (totalTestimonials > 0 && displayedTestimonialsData.length = 20) return true;
if (allTestimonialsData.length > 0 && displayedTestimonialsData.length {
applyFilters();
}, 300);
});
console.log('Testimonials filter initialized');
initializeTestimonials();
// Initialize infinite scroll
setTimeout(() => {
initInfiniteScroll();
console.log('Infinite scroll initialized');
}, 1000);
});
// Debug function
window.debugTestimonials = async function() {
console.log('=== Testimonials Debug Information ===');
console.log('Base URL:', WP_CONFIG.baseUrl);
console.log('Category ID:', WP_CONFIG.testimonialsCategory);
console.log('Full API URL:', `${WP_CONFIG.baseUrl}${WP_CONFIG.restBase}${WP_CONFIG.postsEndpoint}?categories=${WP_CONFIG.testimonialsCategory}`);
try {
const response = await fetch(`${WP_CONFIG.baseUrl}${WP_CONFIG.restBase}${WP_CONFIG.postsEndpoint}?per_page=1&categories=${WP_CONFIG.testimonialsCategory}`);
console.log('API Response Status:', response.status);
console.log('API Response Headers:', [...response.headers.entries()]);
if (response.ok) {
const data = await response.json();
console.log('Sample API Data:', data);
} else {
console.log('API Error Response:', await response.text());
}
} catch (error) {
console.error('API Connection Error:', error);
}
console.log('Current Testimonials Data:', testimonialsData);
console.log('=== End Debug Information ===');
};
Filter Testimonials
Loading testimonials in background...
Client Testimonials
Loading...
${testimonial.content}
${testimonial.date.toLocaleDateString()}


