← back to Norma
app/api/globe/states/[code]/route.ts
78 lines
/**
* GET /api/globe/states/[code]
* Returns detailed data for a single US state.
*
* Response:
* { state, heat, orgs }
*/
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ code: string }> },
) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const { code } = await params;
const stateCode = code.toUpperCase().trim();
if (!/^[A-Z]{2}$/.test(stateCode)) {
return NextResponse.json({ error: 'Invalid state code' }, { status: 400 });
}
try {
// Fetch pulse heat data for this state
let heat = null;
await query(
`SELECT
state_code, state_name, heat_score,
petition_count, article_count, org_count,
avg_sentiment, top_topics
FROM pulse_geo_heat
WHERE state_code = $1
LIMIT 1`,
[stateCode],
).then(res => {
if (res.rows.length > 0) {
const r = res.rows[0];
heat = {
state: String(r.state_code),
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 : [],
};
}
}).catch(() => { /* table may not exist */ });
// Fetch organizations in this state
const orgsResult = await query(
`SELECT id, name, type, state
FROM organizations
WHERE UPPER(TRIM(state)) = $1
AND (country IS NULL OR country IN ('US', 'USA', 'United States', ''))
ORDER BY name ASC
LIMIT 30`,
[stateCode],
).catch(() => ({ rows: [] }));
return NextResponse.json({
state: stateCode,
heat,
orgs: orgsResult.rows.map(r => ({
id: String(r.id),
name: String(r.name),
type: r.type ? String(r.type) : null,
})),
});
} catch (err) {
console.error('[api/globe/states/[code]] error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to fetch state detail' }, { status: 500 });
}
}