← 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();
}