← back to Patty

app/petitions/[slug]/PostToPlatformsPanel.tsx

647 lines

'use client';

import { useState, useEffect, useCallback } from 'react';

interface PlatformDef {
  id: string;
  label: string;
  color: string;
  icon: string;
  hasConfig?: boolean;
  configLabel?: string;
  configPlaceholder?: string;
  configDefault?: string;
  configType?: 'select' | 'text';
  configOptions?: string[];
}

interface PostLog {
  id: string;
  platform: string;
  status: string;
  error_msg: string | null;
  actor: string | null;
  posted_at: string | null;
  created_at: string;
  post_url: string | null;
  platform_config: Record<string, string> | null;
}

interface PostToPlatformsPanelProps {
  petitionSlug: string;
  petitionTitle: string;
  petitionId: string;
}

const PLATFORMS: PlatformDef[] = [
  {
    id: 'reddit',
    label: 'Reddit',
    color: '#FF4500',
    icon: 'R',
    hasConfig: true,
    configLabel: 'Subreddit',
    configType: 'select',
    configOptions: [
      'studentloans',
      'StudentDebt',
      'PSLF',
      'personalfinance',
      'politics',
      'lostgeneration',
      'antiwork',
    ],
    configDefault: 'studentloans',
  },
  {
    id: 'discord',
    label: 'Discord',
    color: '#5865F2',
    icon: 'D',
    hasConfig: true,
    configLabel: 'Channel ID',
    configType: 'text',
    configPlaceholder: 'e.g. 1234567890',
  },
  {
    id: 'twitter',
    label: 'X / Twitter',
    color: '#1DA1F2',
    icon: 'X',
  },
  {
    id: 'bluesky',
    label: 'Bluesky',
    color: '#0085FF',
    icon: 'B',
  },
  {
    id: 'moveon',
    label: 'MoveOn',
    color: '#E03131',
    icon: 'M',
  },
  {
    id: 'patty',
    label: 'Launch on Patty',
    color: '#7c3aed',
    icon: 'P',
  },
];

export default function PostToPlatformsPanel({
  petitionSlug,
  petitionTitle,
  petitionId,
}: PostToPlatformsPanelProps) {
  const [enabled, setEnabled] = useState<Record<string, boolean>>({});
  const [configs, setConfigs] = useState<Record<string, Record<string, string>>>({});
  const [posting, setPosting] = useState(false);
  const [postResult, setPostResult] = useState<{
    results: Array<{ platform: string; status: string; message: string }>;
    summary: { total: number; posted: number; failed: number };
  } | null>(null);
  const [logs, setLogs] = useState<PostLog[]>([]);
  const [logsLoading, setLogsLoading] = useState(true);
  const [expanded, setExpanded] = useState(false);

  // Initialize defaults
  useEffect(() => {
    const defaultEnabled: Record<string, boolean> = {};
    const defaultConfigs: Record<string, Record<string, string>> = {};
    for (const p of PLATFORMS) {
      defaultEnabled[p.id] = false;
      if (p.hasConfig && p.configDefault) {
        defaultConfigs[p.id] = { [p.configLabel?.toLowerCase().replace(/\s/g, '_') || 'value']: p.configDefault };
      }
    }
    setEnabled(defaultEnabled);
    setConfigs(defaultConfigs);
  }, []);

  const fetchLogs = useCallback(async () => {
    try {
      const res = await fetch(`/api/petitions/${petitionSlug}/post-log`);
      if (res.ok) {
        const data = await res.json();
        setLogs(data.logs || []);
      }
    } catch {
      // silently fail
    } finally {
      setLogsLoading(false);
    }
  }, [petitionSlug]);

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

  function togglePlatform(id: string) {
    setEnabled((prev) => ({ ...prev, [id]: !prev[id] }));
  }

  function selectAll() {
    const next: Record<string, boolean> = {};
    for (const p of PLATFORMS) next[p.id] = true;
    setEnabled(next);
  }

  function selectNone() {
    const next: Record<string, boolean> = {};
    for (const p of PLATFORMS) next[p.id] = false;
    setEnabled(next);
  }

  function updateConfig(platformId: string, key: string, value: string) {
    setConfigs((prev) => ({
      ...prev,
      [platformId]: { ...prev[platformId], [key]: value },
    }));
  }

  async function handlePost() {
    const selectedPlatforms = PLATFORMS.filter((p) => enabled[p.id]).map((p) => ({
      name: p.id,
      enabled: true,
      config: configs[p.id] || {},
    }));

    if (selectedPlatforms.length === 0) return;

    setPosting(true);
    setPostResult(null);

    try {
      const res = await fetch(`/api/petitions/${petitionSlug}/post`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ platforms: selectedPlatforms, action: 'post' }),
      });

      const data = await res.json();

      if (res.ok) {
        setPostResult({
          results: data.results || [],
          summary: data.summary || { total: 0, posted: 0, failed: 0 },
        });
        // Refresh logs
        fetchLogs();
      } else {
        setPostResult({
          results: [{ platform: 'system', status: 'failed', message: data.error || 'Request failed' }],
          summary: { total: 1, posted: 0, failed: 1 },
        });
      }
    } catch {
      setPostResult({
        results: [{ platform: 'system', status: 'failed', message: 'Network error' }],
        summary: { total: 1, posted: 0, failed: 1 },
      });
    } finally {
      setPosting(false);
    }
  }

  const enabledCount = Object.values(enabled).filter(Boolean).length;

  const statusColor = (status: string) => {
    switch (status) {
      case 'posted': return 'var(--color-success)';
      case 'failed': return 'var(--color-error)';
      case 'pending': return 'var(--color-warning)';
      case 'skipped': return 'var(--color-text-muted)';
      default: return 'var(--color-text-muted)';
    }
  };

  const platformColor = (name: string) => {
    const p = PLATFORMS.find((pl) => pl.id === name);
    return p?.color || 'var(--color-text-muted)';
  };

  return (
    <div style={{
      backgroundColor: 'var(--color-surface)',
      border: '1px solid var(--color-border)',
      borderRadius: 'var(--radius-lg)',
      padding: '1.5rem',
    }}>
      {/* Header */}
      <button
        onClick={() => setExpanded(!expanded)}
        style={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          width: '100%',
          background: 'none',
          border: 'none',
          cursor: 'pointer',
          padding: 0,
        }}
      >
        <h3 style={{
          color: 'var(--color-text)',
          fontSize: '1rem',
          fontWeight: 700,
          display: 'flex',
          alignItems: 'center',
          gap: '0.5rem',
          margin: 0,
        }}>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/>
            <polyline points="16 6 12 2 8 6"/>
            <line x1="12" y1="2" x2="12" y2="15"/>
          </svg>
          Post to Platforms
        </h3>
        <svg
          width="16"
          height="16"
          viewBox="0 0 24 24"
          fill="none"
          stroke="var(--color-text-muted)"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
          style={{
            transition: 'transform 150ms ease',
            transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)',
          }}
        >
          <polyline points="6 9 12 15 18 9"/>
        </svg>
      </button>

      {/* Expandable Content */}
      <div style={{
        maxHeight: expanded ? '2000px' : '0',
        overflow: 'hidden',
        transition: 'max-height 300ms cubic-bezier(0.4, 0, 0.2, 1)',
      }}>
        <div style={{ paddingTop: '1rem' }}>
          {/* Platform toggles */}
          <div style={{
            display: 'flex',
            flexWrap: 'wrap',
            gap: '0.5rem',
            marginBottom: '0.75rem',
          }}>
            {PLATFORMS.map((p) => (
              <label
                key={p.id}
                style={{
                  display: 'flex',
                  alignItems: 'center',
                  gap: '0.375rem',
                  padding: '0.375rem 0.625rem',
                  borderRadius: 'var(--radius-sm)',
                  backgroundColor: enabled[p.id]
                    ? `${p.color}18`
                    : 'var(--color-surface-el)',
                  border: `1px solid ${enabled[p.id] ? `${p.color}55` : 'var(--color-border)'}`,
                  cursor: 'pointer',
                  fontSize: '0.8125rem',
                  fontWeight: 500,
                  color: enabled[p.id] ? p.color : 'var(--color-text-secondary)',
                  transition: 'all 150ms ease',
                  userSelect: 'none',
                }}
              >
                <input
                  type="checkbox"
                  checked={enabled[p.id] || false}
                  onChange={() => togglePlatform(p.id)}
                  style={{
                    accentColor: p.color,
                    cursor: 'pointer',
                    width: '0.875rem',
                    height: '0.875rem',
                  }}
                />
                <span style={{
                  width: '6px',
                  height: '6px',
                  borderRadius: '50%',
                  backgroundColor: p.color,
                  flexShrink: 0,
                }} />
                {p.label}
              </label>
            ))}
          </div>

          {/* All / None buttons */}
          <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.75rem' }}>
            <button
              onClick={selectAll}
              style={{
                fontSize: '0.75rem',
                padding: '0.125rem 0.5rem',
                borderRadius: 'var(--radius-sm)',
                border: '1px solid rgba(124, 58, 237, 0.4)',
                background: 'transparent',
                color: 'var(--color-primary)',
                cursor: 'pointer',
              }}
            >
              All
            </button>
            <button
              onClick={selectNone}
              style={{
                fontSize: '0.75rem',
                padding: '0.125rem 0.5rem',
                borderRadius: 'var(--radius-sm)',
                border: '1px solid var(--color-border)',
                background: 'transparent',
                color: 'var(--color-text-muted)',
                cursor: 'pointer',
              }}
            >
              None
            </button>
          </div>

          {/* Platform-specific config fields */}
          {PLATFORMS.filter((p) => p.hasConfig && enabled[p.id]).map((p) => {
            const configKey = p.configLabel?.toLowerCase().replace(/\s/g, '_') || 'value';
            return (
              <div
                key={p.id}
                style={{
                  display: 'flex',
                  alignItems: 'center',
                  gap: '0.5rem',
                  marginBottom: '0.5rem',
                  padding: '0.5rem 0.625rem',
                  borderRadius: 'var(--radius-sm)',
                  backgroundColor: `${p.color}08`,
                  border: `1px solid ${p.color}22`,
                }}
              >
                <span style={{
                  width: '6px',
                  height: '6px',
                  borderRadius: '50%',
                  backgroundColor: p.color,
                  flexShrink: 0,
                }} />
                <span style={{
                  fontSize: '0.75rem',
                  color: 'var(--color-text-secondary)',
                  minWidth: '60px',
                  fontWeight: 500,
                }}>
                  {p.configLabel}:
                </span>
                {p.configType === 'select' && p.configOptions ? (
                  <select
                    value={configs[p.id]?.[configKey] || p.configDefault || ''}
                    onChange={(e) => updateConfig(p.id, configKey, e.target.value)}
                    className="input"
                    style={{
                      flex: 1,
                      padding: '0.25rem 0.5rem',
                      fontSize: '0.8125rem',
                    }}
                  >
                    {p.configOptions.map((opt) => (
                      <option key={opt} value={opt}>
                        r/{opt}
                      </option>
                    ))}
                  </select>
                ) : (
                  <input
                    type="text"
                    className="input"
                    placeholder={p.configPlaceholder || ''}
                    value={configs[p.id]?.[configKey] || ''}
                    onChange={(e) => updateConfig(p.id, configKey, e.target.value)}
                    style={{
                      flex: 1,
                      padding: '0.25rem 0.5rem',
                      fontSize: '0.8125rem',
                    }}
                  />
                )}
              </div>
            );
          })}

          {/* Review & Post button */}
          <button
            onClick={handlePost}
            disabled={posting || enabledCount === 0}
            className="btn btn-primary"
            style={{
              width: '100%',
              padding: '0.75rem 1rem',
              fontSize: '0.9375rem',
              fontWeight: 700,
              marginTop: '0.5rem',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              gap: '0.5rem',
            }}
          >
            {posting ? (
              <>
                <span className="spinner" style={{ width: '1rem', height: '1rem', borderWidth: '2px' }} />
                Posting...
              </>
            ) : (
              <>
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/>
                  <polyline points="16 6 12 2 8 6"/>
                  <line x1="12" y1="2" x2="12" y2="15"/>
                </svg>
                Review &amp; Post ({enabledCount})
              </>
            )}
          </button>

          {/* Post results */}
          {postResult && (
            <div style={{
              marginTop: '0.75rem',
              padding: '0.75rem',
              borderRadius: 'var(--radius-md)',
              backgroundColor: 'var(--color-surface-el)',
              border: '1px solid var(--color-border)',
            }}>
              <div style={{
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center',
                marginBottom: '0.5rem',
              }}>
                <span style={{
                  fontSize: '0.75rem',
                  fontWeight: 600,
                  color: 'var(--color-text-secondary)',
                  textTransform: 'uppercase',
                  letterSpacing: '0.05em',
                }}>
                  Dispatch Results
                </span>
                <span style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)' }}>
                  {postResult.summary.posted}/{postResult.summary.total} posted
                </span>
              </div>
              {postResult.results.map((r, i) => (
                <div
                  key={i}
                  style={{
                    display: 'flex',
                    alignItems: 'center',
                    gap: '0.5rem',
                    padding: '0.375rem 0',
                    fontSize: '0.8125rem',
                    borderTop: i > 0 ? '1px solid var(--color-border)' : 'none',
                  }}
                >
                  <span style={{
                    width: '6px',
                    height: '6px',
                    borderRadius: '50%',
                    backgroundColor: statusColor(r.status),
                    flexShrink: 0,
                  }} />
                  <span style={{
                    fontWeight: 600,
                    color: platformColor(r.platform),
                    minWidth: '70px',
                    textTransform: 'capitalize',
                  }}>
                    {r.platform}
                  </span>
                  <span style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem', flex: 1 }}>
                    {r.message}
                  </span>
                  <span className={`badge badge-${r.status === 'posted' ? 'success' : r.status === 'failed' ? 'error' : 'warning'}`}>
                    {r.status}
                  </span>
                </div>
              ))}
            </div>
          )}

          {/* Post history log */}
          <div style={{ marginTop: '1rem' }}>
            <h4 style={{
              fontSize: '0.8125rem',
              fontWeight: 600,
              color: 'var(--color-text-secondary)',
              marginBottom: '0.5rem',
              display: 'flex',
              alignItems: 'center',
              gap: '0.375rem',
            }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <circle cx="12" cy="12" r="10"/>
                <polyline points="12 6 12 12 16 14"/>
              </svg>
              Posting History
            </h4>

            {logsLoading ? (
              <div style={{ display: 'flex', justifyContent: 'center', padding: '1rem 0' }}>
                <span className="spinner" style={{ width: '1.25rem', height: '1.25rem' }} />
              </div>
            ) : logs.length === 0 ? (
              <p style={{
                color: 'var(--color-text-muted)',
                fontSize: '0.8125rem',
                textAlign: 'center',
                padding: '0.75rem 0',
                fontStyle: 'italic',
              }}>
                No posts yet. Select platforms above and click Review &amp; Post.
              </p>
            ) : (
              <div style={{
                display: 'flex',
                flexDirection: 'column',
                gap: '0.375rem',
                maxHeight: '200px',
                overflowY: 'auto',
              }}>
                {logs.map((log) => (
                  <div
                    key={log.id}
                    style={{
                      display: 'flex',
                      alignItems: 'center',
                      gap: '0.5rem',
                      padding: '0.375rem 0.5rem',
                      borderRadius: 'var(--radius-sm)',
                      backgroundColor: 'var(--color-surface-el)',
                      border: '1px solid var(--color-border)',
                      fontSize: '0.75rem',
                    }}
                  >
                    <span style={{
                      width: '6px',
                      height: '6px',
                      borderRadius: '50%',
                      backgroundColor: platformColor(log.platform),
                      flexShrink: 0,
                    }} />
                    <span style={{
                      fontWeight: 600,
                      color: platformColor(log.platform),
                      minWidth: '55px',
                      textTransform: 'capitalize',
                    }}>
                      {log.platform}
                    </span>
                    <span style={{
                      color: statusColor(log.status),
                      fontWeight: 600,
                      minWidth: '42px',
                      textTransform: 'capitalize',
                    }}>
                      {log.status}
                    </span>
                    <span style={{
                      flex: 1,
                      color: 'var(--color-text-muted)',
                      overflow: 'hidden',
                      textOverflow: 'ellipsis',
                      whiteSpace: 'nowrap',
                    }}>
                      {log.error_msg || (log.actor ? `by ${log.actor}` : '')}
                    </span>
                    <span style={{ color: 'var(--color-text-muted)', flexShrink: 0 }}>
                      {log.posted_at
                        ? new Date(log.posted_at).toLocaleDateString('en-US', {
                            month: 'short',
                            day: 'numeric',
                            hour: '2-digit',
                            minute: '2-digit',
                          })
                        : new Date(log.created_at).toLocaleDateString('en-US', {
                            month: 'short',
                            day: 'numeric',
                            hour: '2-digit',
                            minute: '2-digit',
                          })}
                    </span>
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}