← back to Norma

app/api/globe/states/route.ts

84 lines

/**
 * GET /api/globe/states
 * Returns US state pulse heat data for the US view mode.
 *
 * Response:
 *   { states: StateHeat[] }
 */

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';

export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    // Try pulse_geo_heat table first
    const heatResult = await query(`
      SELECT
        state_code,
        state_name,
        heat_score,
        petition_count,
        article_count,
        org_count,
        avg_sentiment,
        top_topics,
        updated_at
      FROM pulse_geo_heat
      ORDER BY heat_score DESC
    `).catch(() => ({ rows: [] }));

    if (heatResult.rows.length > 0) {
      return NextResponse.json({
        states: heatResult.rows.map(r => ({
          state: String(r.state_code || r.state_name),
          state_name: String(r.state_name || r.state_code),
          heat_score: Number(r.heat_score) || 0,
          petition_count: Number(r.petition_count) || 0,
          article_count: Number(r.article_count) || 0,
          org_count: Number(r.org_count) || 0,
          avg_sentiment: r.avg_sentiment != null ? Number(r.avg_sentiment) : null,
          top_topics: Array.isArray(r.top_topics) ? r.top_topics : [],
        })),
      });
    }

    // Fallback: aggregate from organizations + pulse_topics
    const stateResult = await query(`
      SELECT
        UPPER(TRIM(o.state)) AS state_code,
        COUNT(DISTINCT o.id)::int AS org_count,
        0 AS petition_count,
        0 AS article_count,
        NULL AS avg_sentiment,
        ARRAY[]::text[] AS top_topics
      FROM organizations o
      WHERE o.state IS NOT NULL
        AND o.country IN ('US', 'USA', 'United States', '')
        AND LENGTH(TRIM(o.state)) BETWEEN 2 AND 3
      GROUP BY UPPER(TRIM(o.state))
      HAVING COUNT(DISTINCT o.id) > 0
      ORDER BY org_count DESC
    `).catch(() => ({ rows: [] }));

    return NextResponse.json({
      states: stateResult.rows.map(r => ({
        state: String(r.state_code),
        state_name: String(r.state_code),
        heat_score: Number(r.org_count) || 0,
        petition_count: 0,
        article_count: 0,
        org_count: Number(r.org_count) || 0,
        avg_sentiment: null,
        top_topics: [],
      })),
    });
  } catch (err) {
    console.error('[api/globe/states] error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch state data' }, { status: 500 });
  }
}