← back to Cypress Awards
frontend/src/app.js
415 lines
// CyPresAwards Frontend Application
// Use relative URL so it works on both IP and domain
const API_BASE_URL = window.location.origin + '/api';
// Application state
const state = {
organizations: [],
categories: [],
legalTopics: [],
currentPage: 1,
pageSize: 20,
totalResults: 0,
filters: {
search: '',
legalTopics: [],
categories: [],
state: ''
}
};
// DOM Elements
const elements = {};
// Initialize DOM elements
function initElements() {
elements.searchInput = document.getElementById('searchInput');
elements.searchBtn = document.getElementById('searchBtn');
elements.legalTopicFilter = document.getElementById('legalTopicFilter');
elements.categoryFilter = document.getElementById('categoryFilter');
elements.stateFilter = document.getElementById('stateFilter');
elements.applyFiltersBtn = document.getElementById('applyFilters');
elements.clearFiltersBtn = document.getElementById('clearFilters');
elements.organizationsList = document.getElementById('organizationsList');
elements.loadingSpinner = document.getElementById('loadingSpinner');
elements.errorMessage = document.getElementById('errorMessage');
elements.resultsCount = document.getElementById('resultsCount');
elements.totalOrgs = document.getElementById('totalOrgs');
elements.totalCategories = document.getElementById('totalCategories');
elements.totalTopics = document.getElementById('totalTopics');
elements.prevPageBtn = document.getElementById('prevPage');
elements.nextPageBtn = document.getElementById('nextPage');
elements.pageInfo = document.getElementById('pageInfo');
}
// API Functions
async function fetchAPI(endpoint, params = {}) {
const queryString = new URLSearchParams(params).toString();
const url = `${API_BASE_URL}${endpoint}${queryString ? '?' + queryString : ''}`;
console.log('Fetching:', url);
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
console.log('Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('Data received:', data.success ? 'Success' : 'Failed');
return data;
}
async function loadCategories() {
try {
const response = await fetchAPI('/categories');
state.categories = response.data || [];
populateFilterSelect(elements.categoryFilter, state.categories);
console.log('Categories loaded:', state.categories.length);
} catch (error) {
console.error('Error loading categories:', error);
}
}
async function loadLegalTopics() {
try {
const response = await fetchAPI('/legal-topics');
state.legalTopics = response.data || [];
populateFilterSelect(elements.legalTopicFilter, state.legalTopics);
console.log('Legal topics loaded:', state.legalTopics.length);
} catch (error) {
console.error('Error loading legal topics:', error);
}
}
async function loadStats() {
try {
const response = await fetchAPI('/stats');
if (response.data) {
elements.totalOrgs.textContent = response.data.totalOrganizations.toLocaleString();
elements.totalCategories.textContent = response.data.totalCategories;
elements.totalTopics.textContent = response.data.totalLegalTopics;
console.log('Stats loaded:', response.data);
}
} catch (error) {
console.error('Error loading stats:', error);
elements.totalOrgs.textContent = '0';
elements.totalCategories.textContent = '0';
elements.totalTopics.textContent = '0';
}
}
async function loadOrganizations() {
console.log('Loading organizations...');
showLoading(true);
hideError();
try {
const params = {
limit: state.pageSize,
offset: (state.currentPage - 1) * state.pageSize
};
if (state.filters.search) {
params.search = state.filters.search;
}
if (state.filters.legalTopics.length > 0) {
params.legalTopics = state.filters.legalTopics.join(',');
}
if (state.filters.categories.length > 0) {
params.categories = state.filters.categories.join(',');
}
if (state.filters.state) {
params.state = state.filters.state;
}
console.log('Request params:', params);
const response = await fetchAPI('/organizations', params);
if (!response.success || !response.data) {
throw new Error('Invalid response from server');
}
state.organizations = response.data;
state.totalResults = response.meta.total;
console.log('Organizations loaded:', state.organizations.length, 'of', state.totalResults);
renderOrganizations();
updatePagination();
updateResultsCount();
} catch (error) {
console.error('Error loading organizations:', error);
showError(`Failed to load organizations: ${error.message}`);
} finally {
showLoading(false);
}
}
// UI Rendering Functions
function populateFilterSelect(selectElement, items) {
if (!selectElement || !items) return;
const firstOption = selectElement.options[0];
selectElement.innerHTML = '';
if (firstOption) {
selectElement.appendChild(firstOption);
}
items.forEach(item => {
const option = document.createElement('option');
option.value = item.name;
option.textContent = item.name;
selectElement.appendChild(option);
});
}
function renderOrganizations() {
if (!elements.organizationsList) return;
elements.organizationsList.innerHTML = '';
if (state.organizations.length === 0) {
elements.organizationsList.innerHTML = `
<div style="text-align: center; padding: 3rem; color: #666;">
<h3>No organizations found</h3>
<p>Try adjusting your search filters</p>
</div>
`;
return;
}
state.organizations.forEach(org => {
const card = createOrganizationCard(org);
elements.organizationsList.appendChild(card);
});
console.log('Rendered', state.organizations.length, 'organization cards');
}
function createOrganizationCard(org) {
const card = document.createElement('div');
card.className = 'org-card';
const categories = org.categories ? parseArray(org.categories) : [];
const legalTopics = org.legal_topics ? parseArray(org.legal_topics) : [];
const logoHTML = org.logo_url
? `<img src="${escapeHtml(org.logo_url)}" alt="${escapeHtml(org.name)} logo" class="org-logo" onerror="this.style.display='none'">`
: `<div class="org-logo-placeholder">${org.name.charAt(0).toUpperCase()}</div>`;
const categoryTags = categories.filter(c => c).map(cat =>
`<span class="tag category">${escapeHtml(cat)}</span>`
).join('');
const topicTags = legalTopics.filter(t => t).map(topic =>
`<span class="tag legal-topic">${escapeHtml(topic)}</span>`
).join('');
const location = [org.location_city, org.location_state, org.location_country]
.filter(Boolean)
.join(', ') || 'Location not specified';
card.innerHTML = `
${logoHTML}
<div class="org-content">
<div class="org-header">
<h3 class="org-name">${escapeHtml(org.name)}</h3>
<p class="org-location">📍 ${escapeHtml(location)}</p>
</div>
${org.mission_statement ? `<p class="org-mission">${escapeHtml(truncateText(org.mission_statement, 350))}</p>` : ''}
<div class="org-tags">
${topicTags}
${categoryTags}
</div>
<div class="org-links">
<a href="${escapeHtml(org.website_url)}" target="_blank" rel="noopener" class="org-link">Visit Website →</a>
${org.cypres_page_url ? `<a href="${escapeHtml(org.cypres_page_url)}" target="_blank" rel="noopener" class="org-link">Cy Pres Page →</a>` : ''}
</div>
</div>
`;
return card;
}
// Event Handlers
function handleSearch() {
state.filters.search = elements.searchInput.value.trim();
state.currentPage = 1;
loadOrganizations();
}
function handleApplyFilters() {
state.filters.legalTopics = Array.from(elements.legalTopicFilter.selectedOptions)
.map(opt => opt.value)
.filter(val => val !== '');
state.filters.categories = Array.from(elements.categoryFilter.selectedOptions)
.map(opt => opt.value)
.filter(val => val !== '');
state.filters.state = elements.stateFilter.value;
state.currentPage = 1;
loadOrganizations();
}
function handleClearFilters() {
elements.searchInput.value = '';
elements.legalTopicFilter.selectedIndex = 0;
elements.categoryFilter.selectedIndex = 0;
elements.stateFilter.selectedIndex = 0;
state.filters = {
search: '',
legalTopics: [],
categories: [],
state: ''
};
state.currentPage = 1;
loadOrganizations();
}
function changePage(delta) {
const newPage = state.currentPage + delta;
const maxPage = Math.ceil(state.totalResults / state.pageSize);
if (newPage >= 1 && newPage <= maxPage) {
state.currentPage = newPage;
loadOrganizations();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
function updatePagination() {
const maxPage = Math.ceil(state.totalResults / state.pageSize);
elements.prevPageBtn.disabled = state.currentPage === 1;
elements.nextPageBtn.disabled = state.currentPage >= maxPage || maxPage === 0;
elements.pageInfo.textContent = `Page ${state.currentPage} of ${maxPage || 1}`;
}
function updateResultsCount() {
const start = (state.currentPage - 1) * state.pageSize + 1;
const end = Math.min(state.currentPage * state.pageSize, state.totalResults);
if (state.totalResults === 0) {
elements.resultsCount.textContent = 'No results';
} else {
elements.resultsCount.textContent = `Showing ${start}-${end} of ${state.totalResults.toLocaleString()} results`;
}
}
// Utility Functions
function showLoading(show) {
if (elements.loadingSpinner && elements.organizationsList) {
elements.loadingSpinner.classList.toggle('hidden', !show);
elements.organizationsList.classList.toggle('hidden', show);
}
}
function showError(message) {
if (elements.errorMessage) {
elements.errorMessage.textContent = message;
elements.errorMessage.classList.remove('hidden');
}
}
function hideError() {
if (elements.errorMessage) {
elements.errorMessage.classList.add('hidden');
}
}
function truncateText(text, maxLength) {
if (!text) return '';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
function parseArray(pgArray) {
if (!pgArray) return [];
if (Array.isArray(pgArray)) return pgArray;
if (typeof pgArray === 'string') {
return pgArray
.replace(/^\{|\}$/g, '')
.split(',')
.filter(Boolean);
}
return [];
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Setup event listeners
function setupEventListeners() {
if (elements.searchBtn) {
elements.searchBtn.addEventListener('click', handleSearch);
}
if (elements.searchInput) {
elements.searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') handleSearch();
});
}
if (elements.applyFiltersBtn) {
elements.applyFiltersBtn.addEventListener('click', handleApplyFilters);
}
if (elements.clearFiltersBtn) {
elements.clearFiltersBtn.addEventListener('click', handleClearFilters);
}
if (elements.prevPageBtn) {
elements.prevPageBtn.addEventListener('click', () => changePage(-1));
}
if (elements.nextPageBtn) {
elements.nextPageBtn.addEventListener('click', () => changePage(1));
}
}
// Initialize application
async function init() {
console.log('CyPresAwards initializing...');
console.log('API URL:', API_BASE_URL);
try {
initElements();
console.log('DOM elements initialized');
await Promise.all([
loadCategories(),
loadLegalTopics(),
loadStats()
]);
await loadOrganizations();
setupEventListeners();
console.log('✅ Application initialized successfully');
} catch (error) {
console.error('❌ Initialization error:', error);
showError('Failed to initialize application. Please refresh the page.');
}
}
// Start the application when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}