← back to Hub

components/ActivityTab.tsx

214 lines

'use client';

import { useState, useEffect, useCallback } from 'react';
import { SkeletonList } from './Skeleton';
import {
  Activity,
  FileText,
  Handshake,
  Award,
  Megaphone,
  Mail,
  Zap,
  Users,
  Clock,
  RefreshCw,
  Filter,
} from 'lucide-react';
import SortDropdown from './shared/SortDropdown';
import { useClientSort, SortConfig } from '@/hooks/useClientSort';

interface ActivityItem {
  id: string;
  app: string;
  type: string;
  title: string;
  description: string;
  created_at: string;
}

const TYPE_ICONS: Record<string, React.ElementType> = {
  draft: FileText,
  collaboration: Handshake,
  grant: Award,
  proposal: FileText,
  petition: Megaphone,
  campaign: Mail,
  match: Zap,
  contact: Users,
};

const SORT_OPTIONS = [
  { value: 'date_desc', label: 'Newest First' },
  { value: 'date_asc', label: 'Oldest First' },
  { value: 'app', label: 'App A-Z' },
  { value: 'type', label: 'Type A-Z' },
];

const SORT_CONFIGS: Record<string, SortConfig> = {
  date_desc: { key: 'created_at', direction: 'desc', type: 'date' },
  date_asc:  { key: 'created_at', direction: 'asc', type: 'date' },
  app:       { key: 'app', direction: 'asc', type: 'string' },
  type:      { key: 'type', direction: 'asc', type: 'string' },
};

const APP_COLORS: Record<string, string> = {
  sdcc: '#3b82f6',
  grant: '#059669',
  patty: '#7c3aed',
  freddy: '#d97706',
};

function formatTime(ts: string) {
  const d = new Date(ts);
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
    ' ' + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZone: 'America/Los_Angeles' });
}

export default function ActivityTab() {
  const [items, setItems] = useState<ActivityItem[]>([]);
  const [filter, setFilter] = useState<string>('all');
  const [loading, setLoading] = useState(true);
  const [refreshing, setRefreshing] = useState(false);
  const [sortBy, setSortBy] = useState('date_desc');

  const fetchActivity = useCallback(async (isRefresh = false) => {
    if (isRefresh) setRefreshing(true);
    try {
      const res = await fetch('/api/activity', { credentials: 'include' });
      if (res.ok) {
        const data = await res.json();
        setItems(data.items || []);
      }
    } catch (err) {
      console.error('Activity fetch error:', err);
    } finally {
      setLoading(false);
      setRefreshing(false);
    }
  }, []);

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

  // Auto-refresh every 30 seconds
  useEffect(() => {
    const interval = setInterval(() => fetchActivity(true), 30000);
    return () => clearInterval(interval);
  }, [fetchActivity]);

  const filtered = filter === 'all' ? items : items.filter(i => i.app === filter);
  const sortedFiltered = useClientSort(filtered, sortBy, SORT_CONFIGS);

  if (loading) {
    return <div className="p-5"><SkeletonList count={4} /></div>;
  }

  return (
    <div className="p-5 space-y-6">
      {/* Header */}
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-3">
          <Activity size={20} style={{ color: 'var(--color-accent)' }} />
          <div>
            <h2 className="text-xl font-bold" style={{ color: 'var(--color-text)' }}>Activity Feed</h2>
            <p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>Cross-app activity (auto-refreshes every 30s)</p>
          </div>
        </div>
        <button
          onClick={() => fetchActivity(true)}
          disabled={refreshing}
          className="btn btn-secondary btn-sm"
        >
          <RefreshCw size={14} className={refreshing ? 'animate-spin' : ''} />
          Refresh
        </button>
      </div>

      {/* Filters */}
      <div className="flex items-center gap-2">
        <Filter size={14} style={{ color: 'var(--color-text-muted)' }} />
        {['all', 'sdcc', 'grant', 'patty', 'freddy'].map((f) => (
          <button
            key={f}
            onClick={() => setFilter(f)}
            className="btn btn-sm"
            style={{
              backgroundColor: filter === f ? (f === 'all' ? 'var(--color-primary)' : `${APP_COLORS[f]}`) : 'var(--color-surface-el)',
              color: filter === f ? '#fff' : 'var(--color-text-secondary)',
              borderColor: filter === f ? 'transparent' : 'var(--color-border)',
            }}
          >
            {f === 'all' ? 'All' : f.toUpperCase()}
          </button>
        ))}
        <div style={{ marginLeft: 'auto' }}>
          <SortDropdown options={SORT_OPTIONS} value={sortBy} onChange={setSortBy} />
        </div>
      </div>

      {/* Activity List */}
      <div className="space-y-2">
        {sortedFiltered.map((item) => {
          const TypeIcon = TYPE_ICONS[item.type] || Activity;
          const appColor = APP_COLORS[item.app] || 'var(--color-accent)';
          return (
            <div
              key={item.id}
              className="card flex items-start gap-3"
            >
              <div
                className="flex items-center justify-center shrink-0 mt-0.5"
                style={{
                  width: 32,
                  height: 32,
                  borderRadius: 8,
                  backgroundColor: `${appColor}18`,
                }}
              >
                <TypeIcon size={16} style={{ color: appColor }} />
              </div>
              <div className="flex-1 min-w-0">
                <div className="flex items-center gap-2 mb-1">
                  <span
                    className="badge"
                    style={{
                      backgroundColor: `${appColor}18`,
                      color: appColor,
                      border: `1px solid ${appColor}40`,
                      fontSize: 10,
                      padding: '1px 6px',
                    }}
                  >
                    {item.app.toUpperCase()}
                  </span>
                  <span className="badge" style={{ backgroundColor: 'var(--color-surface-el)', color: 'var(--color-text-muted)', border: '1px solid var(--color-border)', fontSize: 10, padding: '1px 6px' }}>
                    {item.type}
                  </span>
                </div>
                <div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
                  {item.title}
                </div>
                <div className="text-xs mt-0.5" style={{ color: 'var(--color-text-muted)' }}>
                  {item.description}
                </div>
              </div>
              <div className="flex items-center gap-1 shrink-0">
                <Clock size={11} style={{ color: 'var(--color-text-muted)' }} />
                <span className="text-xs whitespace-nowrap" style={{ color: 'var(--color-text-muted)' }}>
                  {formatTime(item.created_at)}
                </span>
              </div>
            </div>
          );
        })}
        {sortedFiltered.length === 0 && (
          <div className="text-center py-8">
            <Activity size={32} style={{ color: 'var(--color-text-muted)', margin: '0 auto 8px' }} />
            <p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>No activity found</p>
          </div>
        )}
      </div>
    </div>
  );
}