: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; } }

Filter Testimonials

Loading testimonials in background...

Client Testimonials

Loading...
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 `
${testimonial.content}
${testimonial.author}

${testimonial.jobTitle}${testimonial.company ? ', ' + testimonial.company : ''}

${testimonial.jobTitle ? `${testimonial.jobTitle}` : ''} ${testimonial.company ? `${testimonial.company}` : ''}
${testimonial.date.toLocaleDateString()}
`; }).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 ==='); };