← back to Norma

components/NewsTab.tsx

1411 lines

'use client';

import { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
  ExternalLink,
  Newspaper,
  RefreshCw,
  Search,
  Clock,
  Inbox,
  Send,
  ChevronDown,
  ChevronUp,
  User,
  Filter,
  X,
  Mail,
  Scan,
  Users,
  Pin,
  PinOff,
} from 'lucide-react';
import { useToast } from './ToastProvider';
import { useDebounce } from '@/hooks/useDebounce';
import { SkeletonList } from './Skeleton';
import SortDropdown from './shared/SortDropdown';
import { useClientSort, SortConfig } from '@/hooks/useClientSort';
import ComposeModal from './email-sends/ComposeModal';
import { getOutletFaviconUrl } from '@/lib/outlet-logo';

const NEWS_SORT_OPTIONS = [
  { value: 'date_desc', label: 'Newest First' },
  { value: 'date_asc', label: 'Oldest First' },
  { value: 'relevance_desc', label: 'Most Relevant' },
  { value: 'outlet', label: 'Outlet A-Z' },
];

const NEWS_SORT_CONFIGS: Record<string, SortConfig> = {
  date_desc: { key: 'published_at', direction: 'desc', type: 'date' },
  date_asc: { key: 'published_at', direction: 'asc', type: 'date' },
  relevance_desc: { key: 'relevance_score', direction: 'desc', type: 'number' },
  outlet: { key: 'outlet', direction: 'asc', type: 'string' },
};

/* ─── Types ──────────────────────────────────────────────────────────────── */
interface MentionEntity {
  name: string;
  type: 'employee' | 'alias' | 'competitor';
  match_context: string;
}

interface NewsItem {
  id: string;
  headline: string;
  outlet: string | null;
  url: string | null;
  published_at: string | null;
  summary: string | null;
  tags: string[];
  relevance_score: number | null;
  is_used: boolean;
  source_type: string;
  created_at: string;
  author_name: string | null;
  author_linkedin: string | null;
  mentioned_entities: MentionEntity[] | null;
  is_pinned: boolean;
}

/* ─── Helpers ────────────────────────────────────────────────────────────── */
function formatDateTime(dateStr: string | null): string {
  if (!dateStr) return '';
  return new Date(dateStr).toLocaleString('en-US', {
    timeZone: 'America/Los_Angeles',
    month: 'short',
    day: 'numeric',
    year: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
    hour12: true,
  }) + ' PT';
}



/* ─── Helpers: extract author guess from headline/outlet ─────────────────── */
function guessAuthorLinkedIn(outlet: string | null): string {
  if (!outlet) return '';
  return `https://www.linkedin.com/search/results/people/?keywords=${encodeURIComponent(outlet + ' journalist policy advocacy')}`;
}

function buildOutreachLetter(headline: string, outlet: string | null, authorName?: string | null): string {
  const greeting = authorName ? 'Dear ' + authorName + ',' : 'Dear Editor/Reporter,';
  return greeting + '\n' + `
I'm writing from our organization regarding your recent article: "${headline}"

Your coverage ${outlet ? `in ${outlet} ` : ''}plays a vital role in keeping important issues in the public conversation. Stories like yours help affected communities feel seen and drive policy change.

We work directly with communities, run advocacy campaigns, and provide resources for those navigating complex systems. We'd love to be a resource for your future reporting:

- Expert quotes on policy and community impact
- Real community stories (with permission) for human interest angles
- Data and research on relevant trends
- Policy analysis from our advocacy team

Would you be open to connecting? We're always available for background conversations or on-the-record interviews.

Best regards,
[Your Name]
[Your Title], [Your Organization]
[org-email]
[org-website]`;
}

/* ─── News Card ──────────────────────────────────────────────────────────── */
function NewsCard({ item, index, onEmail, onAuthorClick, onTogglePin, onCreatePost }: { item: NewsItem; index: number; onEmail?: (item: NewsItem) => void; onAuthorClick?: (name: string) => void; onTogglePin?: (id: string, pinned: boolean) => void; onCreatePost?: (item: NewsItem) => void }) {
  const [showLetter, setShowLetter] = useState(false);
  const [showArticle, setShowArticle] = useState(false);
  const [articleContent, setArticleContent] = useState<string | null>(null);
  const [fetchingArticle, setFetchingArticle] = useState(false);
  const [authorName, setAuthorName] = useState<string | null>(item.author_name);
  const [authorLinkedin, setAuthorLinkedin] = useState<string | null>(item.author_linkedin);
  const [resolving, setResolving] = useState(false);
  const [resolved, setResolved] = useState(!!item.author_name);

  // Auto-resolve disabled — Google News URLs can't be followed server-side.
  // Authors are resolved via batch script (scripts/batch-resolve-authors.js)
  // or manually via the "Retry" button.

  async function handleOpenArticle() {
    if (showArticle) { setShowArticle(false); return; }
    if (articleContent) { setShowArticle(true); return; }
    if (!item.url) return;
    setFetchingArticle(true);
    try {
      const res = await fetch(`/api/news/fetch-article?url=${encodeURIComponent(item.url)}`);
      if (res.ok) {
        const data = await res.json();
        setArticleContent(data.content || data.text || 'Could not extract article content.');
      } else {
        setArticleContent('Failed to fetch article. Try opening in a new tab.');
      }
    } catch {
      setArticleContent('Failed to fetch article. Try opening in a new tab.');
    } finally {
      setFetchingArticle(false);
      setShowArticle(true);
    }
  }

  async function handleResolve() {
    if (!item.url || resolving) return;
    setResolving(true);
    try {
      const res = await fetch('/api/news/resolve-authors?id=' + item.id);
      const data = await res.json();
      if (data.authorName) {
        setAuthorName(data.authorName);
        setAuthorLinkedin(data.linkedinUrl || null);
      }
      setResolved(true);
    } catch { /* ignore */ }
    setResolving(false);
  }

  const linkedInUrl = authorLinkedin || guessAuthorLinkedIn(item.outlet);
  const letter = buildOutreachLetter(item.headline, item.outlet, authorName);

  const [expanded, setExpanded] = useState(false);

  return (
    <motion.article
      initial={{ opacity: 0, y: 4 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.1, delay: index * 0.015 }}
      style={{
        borderBottom: '1px solid var(--color-border)',
        padding: '10px 0',
        ...(item.is_pinned ? { backgroundColor: 'var(--color-primary-dim)', borderRadius: 'var(--radius-sm)', paddingLeft: 8, paddingRight: 8, marginBottom: 2 } : {}),
      }}
    >
      {/* ���─ Row 1: Headline + author + outlet + time (always visible) ─── */}
      <div
        style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}
        onClick={() => setExpanded(!expanded)}
      >
        {/* Pin button */}
        <button
          type="button"
          title={item.is_pinned ? 'Unpin article' : 'Pin to top'}
          onClick={(e) => { e.stopPropagation(); onTogglePin?.(item.id, item.is_pinned); }}
          style={{
            background: 'none',
            border: 'none',
            cursor: 'pointer',
            padding: 2,
            flexShrink: 0,
            color: item.is_pinned ? 'var(--color-primary)' : 'var(--color-text-muted)',
            opacity: item.is_pinned ? 1 : 0.4,
            transition: 'opacity 0.15s, color 0.15s',
          }}
          onMouseEnter={(e) => { e.currentTarget.style.opacity = '1'; }}
          onMouseLeave={(e) => { if (!item.is_pinned) e.currentTarget.style.opacity = '0.4'; }}
        >
          {item.is_pinned ? <Pin size={13} /> : <PinOff size={13} />}
        </button>
        {/* Outlet favicon */}
        {item.url ? (
          <a
            href={item.url}
            target="_blank"
            rel="noopener noreferrer"
            onClick={e => e.stopPropagation()}
            title={item.outlet || 'Source'}
            style={{ flexShrink: 0, lineHeight: 0 }}
          >
            {(() => {
              const faviconUrl = getOutletFaviconUrl(item.outlet);
              return faviconUrl ? (
                <img
                  src={faviconUrl}
                  alt={item.outlet || ''}
                  width={22}
                  height={22}
                  style={{ borderRadius: 4, background: 'var(--color-surface-el)' }}
                  onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none'; e.currentTarget.nextElementSibling && ((e.currentTarget.nextElementSibling as HTMLElement).style.display = 'flex'); }}
                />
              ) : null;
            })()}
            <div style={{
              width: 22, height: 22, borderRadius: 4,
              backgroundColor: 'var(--color-surface-el)',
              display: getOutletFaviconUrl(item.outlet) ? 'none' : 'flex',
              alignItems: 'center', justifyContent: 'center',
            }}>
              <Newspaper size={12} style={{ color: 'var(--color-text-muted)' }} />
            </div>
          </a>
        ) : (
          <div style={{
            width: 22, height: 22, borderRadius: 4,
            backgroundColor: 'var(--color-surface-el)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            flexShrink: 0,
          }}>
            <Newspaper size={12} style={{ color: 'var(--color-text-muted)' }} />
          </div>
        )}
        <ChevronDown
          size={14}
          style={{
            color: 'var(--color-text-muted)',
            flexShrink: 0,
            transform: expanded ? 'rotate(0deg)' : 'rotate(-90deg)',
            transition: 'transform 0.15s',
          }}
        />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
            {item.url ? (
              <a
                href={item.url}
                target="_blank"
                rel="noopener noreferrer"
                onClick={e => e.stopPropagation()}
                className="font-semibold hover:underline"
                style={{ color: 'var(--color-text)', fontSize: 14, lineHeight: 1.4, textDecoration: 'none' }}
              >
                {item.headline}
              </a>
            ) : (
              <span className="font-semibold" style={{ color: 'var(--color-text)', fontSize: 14, lineHeight: 1.4 }}>
                {item.headline}
              </span>
            )}
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 2, flexWrap: 'wrap' }}>
            {authorName ? (
              <button
                type="button"
                onClick={e => { e.stopPropagation(); onAuthorClick?.(authorName); }}
                style={{ color: 'var(--color-primary)', background: 'none', border: 'none', cursor: 'pointer', padding: 0, fontSize: 12, fontWeight: 600, textDecoration: 'underline', textDecorationStyle: 'dotted', textUnderlineOffset: '2px' }}
              >
                {authorName}
              </button>
            ) : (
              <span style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>Unknown Author</span>
            )}
            {item.outlet && (
              item.url ? (
                <a
                  href={item.url}
                  target="_blank"
                  rel="noopener noreferrer"
                  onClick={e => e.stopPropagation()}
                  style={{ fontSize: 12, color: 'var(--color-text-secondary)', fontWeight: 500, textDecoration: 'none' }}
                  onMouseEnter={(e) => { e.currentTarget.style.textDecoration = 'underline'; }}
                  onMouseLeave={(e) => { e.currentTarget.style.textDecoration = 'none'; }}
                >
                  {item.outlet}
                </a>
              ) : (
                <span style={{ fontSize: 12, color: 'var(--color-text-secondary)', fontWeight: 500 }}>{item.outlet}</span>
              )
            )}
            {item.published_at && (
              item.url ? (
                <a
                  href={item.url}
                  target="_blank"
                  rel="noopener noreferrer"
                  onClick={e => e.stopPropagation()}
                  style={{ fontSize: 11, color: 'var(--color-text-muted)', textDecoration: 'none' }}
                  onMouseEnter={(e) => { e.currentTarget.style.textDecoration = 'underline'; }}
                  onMouseLeave={(e) => { e.currentTarget.style.textDecoration = 'none'; }}
                >
                  {formatDateTime(item.published_at)}
                </a>
              ) : (
                <span style={{ fontSize: 11, color: 'var(--color-text-muted)' }}>
                  {formatDateTime(item.published_at)}
                </span>
              )
            )}
            {/* Mention pills (always visible in collapsed row) */}
            {item.mentioned_entities && item.mentioned_entities.length > 0 && (
              <>
                {item.mentioned_entities.filter(m => m.type === 'employee' || m.type === 'alias').map((m, idx) => (
                  item.url ? (
                    <a
                      key={`mention-${idx}`}
                      href={item.url}
                      target="_blank"
                      rel="noopener noreferrer"
                      onClick={e => e.stopPropagation()}
                      title={m.match_context}
                      style={{
                        display: 'inline-flex',
                        alignItems: 'center',
                        gap: 3,
                        padding: '1px 7px',
                        borderRadius: 9999,
                        fontSize: 10,
                        fontWeight: 600,
                        backgroundColor: m.type === 'employee' ? 'rgba(16,185,129,0.15)' : 'rgba(99,102,241,0.12)',
                        border: m.type === 'employee' ? '1px solid rgba(16,185,129,0.3)' : '1px solid rgba(99,102,241,0.25)',
                        color: m.type === 'employee' ? 'var(--color-primary)' : '#818cf8',
                        whiteSpace: 'nowrap',
                        textDecoration: 'none',
                      }}
                    >
                      <Users size={9} />
                      {m.name}
                    </a>
                  ) : (
                    <span
                      key={`mention-${idx}`}
                      title={m.match_context}
                      style={{
                        display: 'inline-flex',
                        alignItems: 'center',
                        gap: 3,
                        padding: '1px 7px',
                        borderRadius: 9999,
                        fontSize: 10,
                        fontWeight: 600,
                        backgroundColor: m.type === 'employee' ? 'rgba(16,185,129,0.15)' : 'rgba(99,102,241,0.12)',
                        border: m.type === 'employee' ? '1px solid rgba(16,185,129,0.3)' : '1px solid rgba(99,102,241,0.25)',
                        color: m.type === 'employee' ? 'var(--color-primary)' : '#818cf8',
                        whiteSpace: 'nowrap',
                      }}
                    >
                      <Users size={9} />
                      {m.name}
                    </span>
                  )
                ))}
                {item.mentioned_entities.filter(m => m.type === 'competitor').map((m, idx) => (
                  item.url ? (
                    <a
                      key={`comp-${idx}`}
                      href={item.url}
                      target="_blank"
                      rel="noopener noreferrer"
                      onClick={e => e.stopPropagation()}
                      title={m.match_context}
                      style={{
                        display: 'inline-flex',
                        alignItems: 'center',
                        gap: 3,
                        padding: '1px 7px',
                        borderRadius: 9999,
                        fontSize: 10,
                        fontWeight: 600,
                        backgroundColor: 'rgba(245,158,11,0.12)',
                        border: '1px solid rgba(245,158,11,0.25)',
                        color: 'var(--color-secondary)',
                        whiteSpace: 'nowrap',
                        textDecoration: 'none',
                      }}
                    >
                      {m.name}
                    </a>
                  ) : (
                    <span
                      key={`comp-${idx}`}
                      title={m.match_context}
                      style={{
                        display: 'inline-flex',
                        alignItems: 'center',
                        gap: 3,
                        padding: '1px 7px',
                        borderRadius: 9999,
                        fontSize: 10,
                        fontWeight: 600,
                        backgroundColor: 'rgba(245,158,11,0.12)',
                        border: '1px solid rgba(245,158,11,0.25)',
                        color: 'var(--color-secondary)',
                        whiteSpace: 'nowrap',
                      }}
                    >
                      {m.name}
                    </span>
                  )
                ))}
              </>
            )}
          </div>
        </div>
        {item.url && (
          <a
            href={item.url}
            target="_blank"
            rel="noopener noreferrer"
            onClick={e => e.stopPropagation()}
            className="btn btn-ghost btn-sm"
            style={{ flexShrink: 0, fontSize: 11, padding: '4px 8px', textDecoration: 'none' }}
          >
            <ExternalLink size={12} />
          </a>
        )}
      </div>

      {/* ── Expanded: summary, tags, actions ─── */}
      {expanded && (
        <div style={{ marginLeft: 22, marginTop: 8 }}>
          {/* Summary */}
          {item.summary && (
            <p style={{ fontSize: 13, color: 'var(--color-text-secondary)', lineHeight: 1.5, margin: '0 0 8px' }}>
              {item.summary}
            </p>
          )}

          {/* Tags */}
          {item.tags && item.tags.length > 0 && (
            <div className="flex flex-wrap gap-1 mb-2">
              {item.tags.slice(0, 8).map((tag) => (
                item.url ? (
                  <a
                    key={tag}
                    href={item.url}
                    target="_blank"
                    rel="noopener noreferrer"
                    style={{
                      padding: '2px 8px', borderRadius: 9999, fontSize: 11,
                      backgroundColor: 'var(--color-primary-dim)',
                      border: '1px solid rgba(10,124,89,0.15)',
                      color: 'var(--color-primary)',
                      textDecoration: 'none',
                    }}
                  >
                    {tag}
                  </a>
                ) : (
                  <span
                    key={tag}
                    style={{
                      padding: '2px 8px', borderRadius: 9999, fontSize: 11,
                      backgroundColor: 'var(--color-primary-dim)',
                      border: '1px solid rgba(10,124,89,0.15)',
                      color: 'var(--color-primary)',
                    }}
                  >
                    {tag}
                  </span>
                )
              ))}
              {item.source_type && item.source_type !== 'manual' && (
                item.url ? (
                  <a
                    href={item.url}
                    target="_blank"
                    rel="noopener noreferrer"
                    style={{
                      padding: '2px 8px', borderRadius: 9999, fontSize: 11,
                      backgroundColor: 'var(--color-surface-el)',
                      border: '1px solid var(--color-border)',
                      color: 'var(--color-text-muted)',
                      textDecoration: 'none',
                    }}
                  >
                    {item.source_type}
                  </a>
                ) : (
                  <span style={{
                    padding: '2px 8px', borderRadius: 9999, fontSize: 11,
                    backgroundColor: 'var(--color-surface-el)',
                    border: '1px solid var(--color-border)',
                    color: 'var(--color-text-muted)',
                  }}>
                    {item.source_type}
                  </span>
                )
              )}
            </div>
          )}

          {/* ── Author Outreach Row ──────────────────────────────────────── */}
          <div className="flex items-center gap-2 mt-2 flex-wrap">
            {/* LinkedIn link */}
            {authorName && (
              <a
                href={linkedInUrl}
                target="_blank"
                rel="noopener noreferrer"
                className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md"
                style={{
                  backgroundColor: 'rgba(10,102,194,0.1)',
                  border: '1px solid rgba(10,102,194,0.2)',
                  color: '#2563eb',
                  textDecoration: 'none',
                }}
              >
                <svg width="11" height="11" viewBox="0 0 24 24" fill="#0a66c2"><path d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19a.66.66 0 000 .14V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z"/></svg>
                LinkedIn
              </a>
            )}

            {/* Re-detect author button (only if unknown after auto-resolve) */}
            {!authorName && resolved && item.url && (
              <button
                type="button"
                className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md"
                style={{
                  backgroundColor: 'rgba(52,211,153,0.08)',
                  border: '1px solid rgba(52,211,153,0.2)',
                  color: 'var(--color-primary)',
                  cursor: 'pointer',
                }}
                onClick={handleResolve}
              >
                <Search size={10} />
                Retry
              </button>
            )}
            <button
              type="button"
              className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md"
              style={{
                backgroundColor: 'rgba(34,197,94,0.08)',
                border: '1px solid rgba(34,197,94,0.2)',
                color: 'var(--color-primary)',
                cursor: 'pointer',
              }}
              onClick={() => setShowLetter((v) => !v)}
            >
              <Send size={10} />
              {showLetter ? 'Hide' : 'Outreach Letter'}
              {showLetter ? <ChevronUp size={10} /> : <ChevronDown size={10} />}
            </button>
            {item.url && (
              <button
                type="button"
                className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-md"
                style={{
                  backgroundColor: showArticle ? 'rgba(99,102,241,0.15)' : 'rgba(99,102,241,0.08)',
                  border: '1px solid rgba(99,102,241,0.2)',
                  color: '#818cf8',
                  cursor: 'pointer',
                }}
                onClick={handleOpenArticle}
                disabled={fetchingArticle}
              >
                {fetchingArticle ? (
                  <><span className="spinner" style={{ width: 8, height: 8 }} /> Loading...</>
                ) : (
                  <><Newspaper size={10} /> {showArticle ? 'Hide Article' : 'Read Article'}</>
                )}
              </button>
            )}
            {onEmail && (
              <button
                type="button"
                onClick={() => onEmail(item)}
                className="btn btn-ghost btn-sm"
                title="Start email from this article"
                style={{
                  fontSize: '0.7rem',
                  padding: '4px 8px',
                  minHeight: 'auto',
                }}
              >
                <Mail size={14} />
              </button>
            )}
            {onCreatePost && (
              <button
                type="button"
                onClick={() => onCreatePost(item)}
                className="btn btn-ghost btn-sm"
                title="Create social post from this article"
                style={{
                  fontSize: '0.7rem',
                  padding: '4px 8px',
                  minHeight: 'auto',
                  color: '#10b981',
                }}
              >
                <Send size={14} />
              </button>
            )}
          </div>

          {/* ── Collapsed Article Content ──────────────────────────────── */}
          <AnimatePresence>
            {showArticle && articleContent && (
              <motion.div
                initial={{ opacity: 0, height: 0 }}
                animate={{ opacity: 1, height: 'auto' }}
                exit={{ opacity: 0, height: 0 }}
                className="mt-3"
              >
                <div
                  className="text-xs leading-relaxed p-4 rounded-lg overflow-y-auto"
                  style={{
                    backgroundColor: 'var(--color-bg)',
                    border: '1px solid var(--color-border)',
                    color: 'var(--color-text-secondary)',
                    maxHeight: 400,
                    whiteSpace: 'pre-wrap',
                    fontFamily: 'inherit',
                    lineHeight: 1.7,
                  }}
                >
                  {articleContent}
                </div>
                <div className="flex gap-2 mt-2">
                  {item.url && (
                    <a
                      href={item.url}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="btn btn-ghost btn-sm"
                      style={{ fontSize: '0.7rem' }}
                    >
                      <ExternalLink size={10} /> Open Original
                    </a>
                  )}
                  <button
                    type="button"
                    className="btn btn-ghost btn-sm"
                    style={{ fontSize: '0.7rem' }}
                    onClick={() => navigator.clipboard.writeText(articleContent)}
                  >
                    Copy Text
                  </button>
                </div>
              </motion.div>
            )}
          </AnimatePresence>

          {/* ── Collapsed Draft Letter ───────────────────────────────────── */}
          {showLetter && (
            <motion.div
              initial={{ opacity: 0, height: 0 }}
              animate={{ opacity: 1, height: 'auto' }}
              exit={{ opacity: 0, height: 0 }}
              className="mt-3"
            >
              <pre
                className="text-xs leading-relaxed p-3 rounded-lg overflow-x-auto"
                style={{
                  backgroundColor: 'var(--color-surface-el)',
                  border: '1px solid var(--color-border)',
                  color: 'var(--color-text-secondary)',
                  whiteSpace: 'pre-wrap',
                  fontFamily: 'inherit',
                  maxHeight: 300,
                  overflowY: 'auto',
                }}
              >
                {letter}
              </pre>
              <div className="flex gap-2 mt-2">
                <button
                  type="button"
                  className="btn btn-secondary btn-sm"
                  style={{ fontSize: '0.7rem' }}
                  onClick={() => navigator.clipboard.writeText(letter)}
                >
                  Copy Letter
                </button>
              </div>
            </motion.div>
          )}
        </div>
      )}
    </motion.article>
  );
}

/* ─── Outlet option type ─────────────────────────────────────────────────── */
interface OutletOption {
  outlet: string;
  count: number;
}

/* ─── Tag option type ──────────────────────────────────────────────────── */
interface TagOption {
  tag: string;
  count: number;
}

/* ─── NewsTab ────────────────────────────────────────────────────────────── */
export default function NewsTab({ onNavigate, onNavigateToJournalist }: { onNavigate?: (tab: string) => void; onNavigateToJournalist?: (name: string) => void } = {}) {
  const { addToast } = useToast();
  const [news, setNews] = useState<NewsItem[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [searchQuery, setSearchQuery] = useState('');
  const debouncedSearch = useDebounce(searchQuery, 300);
  const [displayCount, setDisplayCount] = useState(30);
  const [batchResolving, setBatchResolving] = useState(false);
  const [resolveStats, setResolveStats] = useState<string | null>(null);
  const [sortBy, setSortBy] = useState('date_desc');
  const [dateRange, setDateRange] = useState('month');
  const [selectedOutlet, setSelectedOutlet] = useState('');
  const [outlets, setOutlets] = useState<OutletOption[]>([]);
  const [allTags, setAllTags] = useState<TagOption[]>([]);
  const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
  const sortedNews = useClientSort(news, sortBy, NEWS_SORT_CONFIGS);
  const [composeOpen, setComposeOpen] = useState(false);
  const [composeNews, setComposeNews] = useState<{ headline: string; summary: string; url: string } | undefined>();
  const [mentionsOnly, setMentionsOnly] = useState(false);
  const [detectingMentions, setDetectingMentions] = useState(false);
  const [detectStats, setDetectStats] = useState<string | null>(null);
  const [ingesting, setIngesting] = useState(false);
  const [ingestStats, setIngestStats] = useState<string | null>(null);
  const [lastUpdated, setLastUpdated] = useState<string | null>(null);

  /* Fetch outlet list for the filter dropdown */
  useEffect(() => {
    fetch('/api/news/outlets')
      .then((r) => r.json())
      .then((data) => {
        if (data.outlets) setOutlets(data.outlets);
      })
      .catch(() => { /* ignore — dropdown just won't populate */ });
  }, []);

  /* Fetch tag list (scoped by date range and outlet) */
  const fetchTags = useCallback(async () => {
    try {
      const params = new URLSearchParams();
      if (dateRange) params.set('date_range', dateRange);
      if (selectedOutlet) params.set('outlet', selectedOutlet);
      const res = await fetch(`/api/news/tags${params.toString() ? '?' + params.toString() : ''}`);
      if (res.ok) {
        const data = await res.json();
        setAllTags(data.tags ?? []);
      }
    } catch { /* ignore */ }
  }, [dateRange, selectedOutlet]);

  useEffect(() => { fetchTags(); }, [fetchTags]);

  /* Toggle a tag in the selection set */
  const toggleTag = useCallback((tag: string) => {
    setSelectedTags((prev) => {
      const next = new Set(prev);
      if (next.has(tag)) {
        next.delete(tag);
      } else {
        next.add(tag);
      }
      return next;
    });
    setDisplayCount(30);
  }, []);

  const clearAllTags = useCallback(() => {
    setSelectedTags(new Set());
    setDisplayCount(30);
  }, []);

  /* Filter visible tags by search query (client-side) */
  const visibleTags = allTags.filter((t) =>
    !debouncedSearch.trim() || t.tag.toLowerCase().includes(debouncedSearch.trim().toLowerCase())
  );

  /* Serialize selected tags for stable dependency tracking */
  const selectedTagsKey = Array.from(selectedTags).sort().join(',');

  const fetchNews = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const params = new URLSearchParams();
      if (debouncedSearch.trim()) params.set('search', debouncedSearch.trim());
      if (selectedOutlet) params.set('outlet', selectedOutlet);
      if (dateRange) params.set('date_range', dateRange);
      if (selectedTagsKey) params.set('tags', selectedTagsKey);
      if (mentionsOnly) params.set('mentions_only', 'true');

      const res = await fetch(`/api/news${params.toString() ? '?' + params.toString() : ''}`);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const data = await res.json();
      setNews(data.news ?? []);
      if (data.last_updated) setLastUpdated(data.last_updated);
    } catch (err) {
      setError((err as Error).message || 'Failed to load news.');
    } finally {
      setLoading(false);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [debouncedSearch, selectedOutlet, dateRange, selectedTagsKey, mentionsOnly]);

  useEffect(() => { fetchNews(); }, [fetchNews]);

  /* ── Pin/unpin an article ──────────────────────────────────────────────── */
  const togglePin = useCallback(async (id: string, currentlyPinned: boolean) => {
    // Optimistic update
    setNews((prev) =>
      prev.map((n) => (n.id === id ? { ...n, is_pinned: !currentlyPinned } : n))
    );
    try {
      const res = await fetch(`/api/news/${id}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include',
        body: JSON.stringify({ is_pinned: !currentlyPinned }),
      });
      if (!res.ok) throw new Error('Failed');
      addToast(!currentlyPinned ? 'Article pinned to top' : 'Article unpinned', 'success');
    } catch {
      // Rollback
      setNews((prev) =>
        prev.map((n) => (n.id === id ? { ...n, is_pinned: currentlyPinned } : n))
      );
      addToast('Failed to update pin', 'error');
    }
  }, [addToast]);

  const visibleNews = sortedNews.slice(0, displayCount);

  return (
    <div className="p-4 sm:p-6 max-w-3xl mx-auto">
      {/* Header */}
      <div className="flex items-center justify-between mb-4 gap-3 flex-wrap">
        <div className="flex items-center gap-2.5">
          <div
            style={{
              width: 32, height: 32, borderRadius: 'var(--radius-md)',
              background: 'linear-gradient(135deg, rgba(52,211,153,0.2), rgba(16,185,129,0.2))',
              border: '1px solid rgba(52,211,153,0.3)',
              display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
            }}
            aria-hidden="true"
          >
            <Newspaper size={16} style={{ color: 'var(--color-primary)' }} />
          </div>
          <div>
            <div className="flex items-center gap-2">
              <h1 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>
                News
              </h1>
              {lastUpdated && !loading && (
                <span className="text-xs" style={{ color: 'var(--color-text-muted)', fontWeight: 400 }} title={`Last updated: ${formatDateTime(lastUpdated)}`}>
                  <Clock size={10} style={{ display: 'inline', verticalAlign: 'middle', marginRight: 3, opacity: 0.6 }} />
                  {formatDateTime(lastUpdated)}
                </span>
              )}
            </div>
            <p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>
              {loading ? 'Loading...' : `${news.length} article${news.length !== 1 ? 's' : ''}${selectedOutlet ? ` from ${selectedOutlet}` : ''} — latest first`}
            </p>
          </div>
        </div>

        <div className="flex items-center gap-2">
          <button
            type="button"
            onClick={async () => {
              setIngesting(true);
              setIngestStats(null);
              try {
                const res = await fetch('/api/cron/ingest-news', {
                  method: 'POST',
                  credentials: 'include',
                });
                const data = await res.json();
                if (data.success) {
                  setIngestStats(`Refreshed: ${data.inserted} new, ${data.skipped} skipped out of ${data.total} articles`);
                  addToast(`${data.inserted} new articles ingested`, 'success');
                  await fetchNews();
                } else {
                  setIngestStats(data.error || 'Ingest failed');
                  addToast(data.error || 'News refresh failed', 'error');
                }
              } catch {
                setIngestStats('Error refreshing news');
                addToast('Failed to refresh news', 'error');
              }
              setIngesting(false);
            }}
            disabled={ingesting}
            className="btn btn-primary btn-sm"
            style={{ fontSize: '0.75rem' }}
          >
            <RefreshCw size={13} style={{ animation: ingesting ? 'spin 0.7s linear infinite' : 'none' }} />
            {ingesting ? 'Refreshing...' : 'Refresh News'}
          </button>
          <SortDropdown options={NEWS_SORT_OPTIONS} value={sortBy} onChange={setSortBy} />
          <button
            type="button"
            onClick={async () => {
              setBatchResolving(true);
              setResolveStats(null);
              try {
                const res = await fetch('/api/news/resolve-authors', { method: 'POST' });
                const data = await res.json();
                setResolveStats(data.message || 'Done');
                // Refresh news to get updated author data
                await fetchNews();
              } catch { setResolveStats('Error'); }
              setBatchResolving(false);
            }}
            disabled={batchResolving}
            className="btn btn-ghost btn-sm"
            style={{ fontSize: '0.7rem' }}
          >
            <User size={12} style={{ animation: batchResolving ? 'spin 0.7s linear infinite' : 'none' }} />
            {batchResolving ? 'Resolving...' : 'Find Authors'}
          </button>
          <button
            type="button"
            onClick={async () => {
              setDetectingMentions(true);
              setDetectStats(null);
              try {
                const res = await fetch('/api/news/detect-mentions', { method: 'POST' });
                const data = await res.json();
                setDetectStats(data.message || 'Done');
                await fetchNews();
              } catch { setDetectStats('Error detecting mentions'); }
              setDetectingMentions(false);
            }}
            disabled={detectingMentions}
            className="btn btn-ghost btn-sm"
            style={{ fontSize: '0.7rem' }}
            title="Scan articles for employee/org name mentions"
          >
            <Scan size={12} style={{ animation: detectingMentions ? 'spin 0.7s linear infinite' : 'none' }} />
            {detectingMentions ? 'Scanning...' : 'Detect Mentions'}
          </button>
          <button
            type="button"
            onClick={fetchNews}
            disabled={loading}
            className="btn btn-ghost btn-sm"
            aria-label="Refresh news"
          >
            <RefreshCw
              size={14}
              aria-hidden="true"
              style={{ animation: loading ? 'spin 0.7s linear infinite' : 'none' }}
            />
          </button>
        </div>
      </div>
      {/* Resolve stats banner */}
      {resolveStats && (
        <div className="mb-3 text-xs px-3 py-2 rounded-lg flex items-center gap-2" style={{ backgroundColor: 'rgba(34,197,94,0.08)', border: '1px solid rgba(34,197,94,0.2)', color: 'var(--color-primary)' }}>
          <User size={12} />
          {resolveStats}
          <button type="button" className="ml-auto text-xs" style={{ color: 'var(--color-text-muted)', cursor: 'pointer', background: 'none', border: 'none' }} onClick={() => setResolveStats(null)}>dismiss</button>
        </div>
      )}
      {/* Detect mentions stats banner */}
      {detectStats && (
        <div className="mb-3 text-xs px-3 py-2 rounded-lg flex items-center gap-2" style={{ backgroundColor: 'rgba(16,185,129,0.08)', border: '1px solid rgba(16,185,129,0.2)', color: 'var(--color-primary)' }}>
          <Scan size={12} />
          {detectStats}
          <button type="button" className="ml-auto text-xs" style={{ color: 'var(--color-text-muted)', cursor: 'pointer', background: 'none', border: 'none' }} onClick={() => setDetectStats(null)}>dismiss</button>
        </div>
      )}
      {/* Ingest stats banner */}
      {ingestStats && (
        <div className="mb-3 text-xs px-3 py-2 rounded-lg flex items-center gap-2" style={{ backgroundColor: 'rgba(99,102,241,0.08)', border: '1px solid rgba(99,102,241,0.2)', color: '#818cf8' }}>
          <Newspaper size={12} />
          {ingestStats}
          <button type="button" className="ml-auto text-xs" style={{ color: 'var(--color-text-muted)', cursor: 'pointer', background: 'none', border: 'none' }} onClick={() => setIngestStats(null)}>dismiss</button>
        </div>
      )}

      {/* Date range tabs */}
      <div className="flex items-center gap-1 mb-4" style={{
        borderBottom: '1px solid var(--color-border)',
        paddingBottom: 0,
      }}>
        {([
          { value: 'today', label: 'Today' },
          { value: 'week', label: 'This Week' },
          { value: 'month', label: 'This Month' },
          { value: '3months', label: '3 Months' },
          { value: '6months', label: '6 Months' },
          { value: '', label: 'All' },
        ] as const).map((tab) => (
          <button
            key={tab.value}
            type="button"
            onClick={() => { setDateRange(tab.value); setDisplayCount(30); }}
            style={{
              padding: '8px 14px',
              fontSize: 13,
              fontWeight: dateRange === tab.value ? 600 : 400,
              color: dateRange === tab.value ? 'var(--color-primary)' : 'var(--color-text-secondary)',
              background: 'none',
              border: 'none',
              borderBottom: dateRange === tab.value ? '2px solid var(--color-primary)' : '2px solid transparent',
              cursor: 'pointer',
              transition: 'color 0.15s, border-color 0.15s',
              marginBottom: -1,
            }}
          >
            {tab.label}
          </button>
        ))}
      </div>

      {/* Search bar + Author filter */}
      <div className="flex gap-2 mb-4" style={{ flexWrap: 'wrap' }}>
        <div className="relative" style={{ flex: '1 1 auto', minWidth: 200 }}>
          <Search
            size={14}
            className="absolute left-3 top-1/2 -translate-y-1/2"
            style={{ color: 'var(--color-text-muted)' }}
            aria-hidden="true"
          />
          <input
            type="text"
            className="input w-full"
            style={{ paddingLeft: '2.25rem' }}
            placeholder="Search headlines, outlets, authors..."
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
            aria-label="Search news"
          />
        </div>

        {/* Author/Outlet filter dropdown */}
        <div className="relative" style={{ minWidth: 200, flexShrink: 0 }}>
          <Filter
            size={14}
            className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none"
            style={{ color: 'var(--color-text-muted)' }}
            aria-hidden="true"
          />
          <select
            className="input w-full appearance-none"
            style={{
              paddingLeft: '2.25rem',
              paddingRight: selectedOutlet ? '2rem' : '0.75rem',
              cursor: 'pointer',
            }}
            value={selectedOutlet}
            onChange={(e) => {
              setSelectedOutlet(e.target.value);
              setDisplayCount(30);
            }}
            aria-label="Filter by author/outlet"
          >
            <option value="">All Authors ({outlets.reduce((s, o) => s + o.count, 0)})</option>
            {outlets.map((o) => (
              <option key={o.outlet} value={o.outlet}>
                {o.outlet} ({o.count})
              </option>
            ))}
          </select>
          {selectedOutlet && (
            <button
              type="button"
              className="absolute right-2 top-1/2 -translate-y-1/2"
              style={{
                background: 'none',
                border: 'none',
                cursor: 'pointer',
                color: 'var(--color-text-muted)',
                padding: 2,
                lineHeight: 1,
              }}
              onClick={() => { setSelectedOutlet(''); setDisplayCount(30); }}
              aria-label="Clear author filter"
              title="Clear filter"
            >
              <X size={14} />
            </button>
          )}
        </div>

        {/* Mentions Only toggle */}
        <button
          type="button"
          onClick={() => { setMentionsOnly(!mentionsOnly); setDisplayCount(30); }}
          className="btn btn-sm"
          style={{
            flexShrink: 0,
            fontSize: '0.7rem',
            padding: '6px 10px',
            backgroundColor: mentionsOnly ? 'rgba(16,185,129,0.15)' : 'transparent',
            border: mentionsOnly ? '1px solid rgba(16,185,129,0.4)' : '1px solid var(--color-border)',
            color: mentionsOnly ? 'var(--color-primary)' : 'var(--color-text-secondary)',
            borderRadius: 'var(--radius-md)',
            cursor: 'pointer',
            transition: 'var(--transition)',
            whiteSpace: 'nowrap',
          }}
          title="Show only articles that mention our people or organization"
          aria-pressed={mentionsOnly}
        >
          <Users size={12} />
          Mentions Us
        </button>
      </div>

      {/* Mentions-only active badge */}
      {mentionsOnly && (
        <div
          className="mb-3 flex items-center gap-2 text-xs px-3 py-2 rounded-lg"
          style={{
            backgroundColor: 'rgba(16,185,129,0.08)',
            border: '1px solid rgba(16,185,129,0.2)',
            color: 'var(--color-primary)',
          }}
        >
          <Users size={12} />
          Showing only articles mentioning our people or organization
          <button
            type="button"
            className="ml-auto"
            style={{ color: 'var(--color-text-muted)', cursor: 'pointer', background: 'none', border: 'none' }}
            onClick={() => { setMentionsOnly(false); setDisplayCount(30); }}
          >
            clear
          </button>
        </div>
      )}

      {/* Active filter badge */}
      {selectedOutlet && (
        <div
          className="mb-3 flex items-center gap-2 text-xs px-3 py-2 rounded-lg"
          style={{
            backgroundColor: 'rgba(16,185,129,0.08)',
            border: '1px solid rgba(16,185,129,0.2)',
            color: 'var(--color-primary)',
          }}
        >
          <Filter size={12} />
          Filtered by: <strong>{selectedOutlet}</strong>
          <span style={{ color: 'var(--color-text-muted)' }}>
            ({news.length} article{news.length !== 1 ? 's' : ''})
          </span>
          <button
            type="button"
            className="ml-auto"
            style={{ color: 'var(--color-text-muted)', cursor: 'pointer', background: 'none', border: 'none' }}
            onClick={() => { setSelectedOutlet(''); setDisplayCount(30); }}
          >
            clear
          </button>
        </div>
      )}

      {/* ── Tag Filter Pills ──────────────────────────────────────────────── */}
      {visibleTags.length > 0 && (
        <div className="mb-4">
          <div
            className="flex items-center gap-2 overflow-x-auto pb-1"
            style={{
              scrollbarWidth: 'thin',
              scrollbarColor: 'var(--color-border) transparent',
            }}
          >
            {selectedTags.size > 0 && (
              <button
                type="button"
                onClick={clearAllTags}
                className="inline-flex items-center gap-1 shrink-0"
                style={{
                  padding: '4px 10px',
                  borderRadius: 9999,
                  fontSize: 11,
                  fontWeight: 600,
                  backgroundColor: 'rgba(239,68,68,0.1)',
                  border: '1px solid rgba(239,68,68,0.25)',
                  color: 'var(--color-error)',
                  cursor: 'pointer',
                  whiteSpace: 'nowrap',
                }}
              >
                <X size={10} />
                Clear all
              </button>
            )}
            {visibleTags.map(({ tag, count }) => {
              const isSelected = selectedTags.has(tag);
              return (
                <button
                  key={tag}
                  type="button"
                  onClick={() => toggleTag(tag)}
                  className="shrink-0"
                  style={{
                    padding: '4px 10px',
                    borderRadius: 9999,
                    fontSize: 11,
                    fontWeight: 500,
                    cursor: 'pointer',
                    whiteSpace: 'nowrap',
                    transition: 'var(--transition)',
                    backgroundColor: isSelected ? 'var(--color-primary)' : 'var(--color-surface-el)',
                    border: isSelected
                      ? '1px solid var(--color-primary)'
                      : '1px solid var(--color-border)',
                    color: isSelected ? '#fff' : 'var(--color-text-secondary)',
                  }}
                >
                  {tag}
                  <span
                    style={{
                      marginLeft: 4,
                      fontSize: 10,
                      opacity: 0.7,
                      fontWeight: 400,
                    }}
                  >
                    {count}
                  </span>
                </button>
              );
            })}
          </div>
        </div>
      )}

      {/* Active tag filter badge */}
      {selectedTags.size > 0 && (
        <div
          className="mb-3 flex items-center gap-2 text-xs px-3 py-2 rounded-lg flex-wrap"
          style={{
            backgroundColor: 'var(--color-primary-dim)',
            border: '1px solid rgba(16,185,129,0.2)',
            color: 'var(--color-primary)',
          }}
        >
          <Filter size={12} />
          Tags:
          {Array.from(selectedTags).map((tag) => (
            <span
              key={tag}
              className="inline-flex items-center gap-1"
              style={{
                padding: '1px 8px',
                borderRadius: 9999,
                fontSize: 10,
                fontWeight: 600,
                backgroundColor: 'var(--color-primary)',
                color: '#fff',
              }}
            >
              {tag}
              <button
                type="button"
                style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#fff', padding: 0, lineHeight: 1 }}
                onClick={() => toggleTag(tag)}
                aria-label={`Remove ${tag} filter`}
              >
                <X size={10} />
              </button>
            </span>
          ))}
          <span style={{ color: 'var(--color-text-muted)' }}>
            ({news.length} article{news.length !== 1 ? 's' : ''})
          </span>
          <button
            type="button"
            className="ml-auto"
            style={{ color: 'var(--color-text-muted)', cursor: 'pointer', background: 'none', border: 'none' }}
            onClick={clearAllTags}
          >
            clear all
          </button>
        </div>
      )}

      {/* Loading */}
      {loading && (
        <div aria-live="polite" aria-busy="true">
          <SkeletonList count={5} />
        </div>
      )}

      {/* Error */}
      {!loading && error && (
        <div
          role="alert"
          className="rounded-xl px-4 py-3 flex items-center justify-between gap-3"
          style={{ backgroundColor: 'rgba(239,68,68,0.05)', border: '1px solid rgba(239,68,68,0.3)' }}
        >
          <p className="text-sm" style={{ color: 'var(--color-error)' }}>{error}</p>
          <button type="button" onClick={fetchNews} className="btn btn-secondary btn-sm">
            <RefreshCw size={12} aria-hidden="true" /> Retry
          </button>
        </div>
      )}

      {/* Empty */}
      {!loading && !error && news.length === 0 && (
        <div
          className="rounded-xl flex flex-col items-center justify-center py-16 gap-3"
          style={{ backgroundColor: 'var(--color-surface)', border: '1px dashed var(--color-border)' }}
        >
          <Inbox size={36} style={{ color: 'var(--color-text-muted)' }} aria-hidden="true" />
          <p className="font-medium text-lg" style={{ color: 'var(--color-text)' }}>
            {searchQuery ? 'No articles match your search' : 'No news articles yet'}
          </p>
          <p className="text-sm text-center max-w-xs" style={{ color: 'var(--color-text-muted)' }}>
            {searchQuery ? 'Try a different search term.' : 'News will appear here once the ingestion feeds are synced.'}
          </p>
        </div>
      )}

      {/* News list */}
      {!loading && !error && visibleNews.length > 0 && (
        <>
          <div className="flex flex-col" role="feed" aria-label="Advocacy news articles">
            <AnimatePresence initial={false}>
              {visibleNews.map((item, i) => (
                <NewsCard
                  key={item.id}
                  item={item}
                  index={i}
                  onTogglePin={togglePin}
                  onEmail={(newsItem) => {
                    setComposeNews({
                      headline: newsItem.headline,
                      summary: newsItem.summary || '',
                      url: newsItem.url || '',
                    });
                    setComposeOpen(true);
                  }}
                  onCreatePost={(newsItem) => {
                    if (typeof window !== 'undefined') {
                      localStorage.setItem('social-compose-prefill', JSON.stringify({
                        headline: newsItem.headline,
                        summary: newsItem.summary || '',
                        url: newsItem.url || '',
                        source: 'news',
                      }));
                    }
                    if (onNavigate) onNavigate('social-compose');
                  }}
                  onAuthorClick={(name) => {
                    if (onNavigateToJournalist) {
                      onNavigateToJournalist(name);
                    } else if (onNavigate) {
                      onNavigate('journalists');
                    }
                  }}
                />
              ))}
            </AnimatePresence>
          </div>

          {/* Load more */}
          {displayCount < news.length && (
            <div className="flex justify-center mt-4">
              <button
                type="button"
                onClick={() => setDisplayCount((c) => c + 30)}
                className="btn btn-secondary"
              >
                Load More ({news.length - displayCount} remaining)
              </button>
            </div>
          )}
        </>
      )}

      {/* ── Compose Modal (from news article) ───────────────────────────── */}
      <ComposeModal
        open={composeOpen}
        onClose={() => setComposeOpen(false)}
        newsItem={composeNews}
      />
    </div>
  );
}