← back to Norma
components/email-analyzer/SdccEmailsPanel.tsx
221 lines
'use client';
/**
* "Old Emails from SDC" — browse & load historical SDCC (Student Debt Crisis)
* emails from Natalia's Gmail archive straight into the Email Analyzer.
*
* Uses the existing /api/gmail/messages endpoint (DB-backed, already synced).
*/
import { useState, useEffect, useCallback } from 'react';
import { Inbox, Search, Calendar, User, Play } from 'lucide-react';
import { useToast } from '@/components/ToastProvider';
interface GmailMessage {
id: number | string;
subject: string | null;
from_address: string | null;
from_name: string | null;
body_text: string | null;
body_html: string | null;
received_at: string | null;
is_read: boolean;
}
interface SdccEmailsPanelProps {
onLoadIntoAnalyzer: (email: { subject: string; body: string; from: string }) => void;
}
export default function SdccEmailsPanel({ onLoadIntoAnalyzer }: SdccEmailsPanelProps) {
const { addToast } = useToast();
const [q, setQ] = useState('');
const [messages, setMessages] = useState<GmailMessage[]>([]);
const [loading, setLoading] = useState(false);
const [loadingDetail, setLoadingDetail] = useState<string | number | null>(null);
const load = useCallback(async () => {
setLoading(true);
try {
const params = new URLSearchParams({ limit: '40', sort: 'date', sort_dir: 'desc' });
if (q.trim()) params.set('q', q.trim());
const res = await fetch(`/api/gmail/messages?${params.toString()}`);
if (!res.ok) throw new Error('load failed');
const data = await res.json();
setMessages(data.messages || data || []);
} catch {
addToast('Failed to load SDCC emails', 'error');
} finally {
setLoading(false);
}
}, [q, addToast]);
useEffect(() => {
const t = setTimeout(() => { void load(); }, 220);
return () => clearTimeout(t);
}, [load]);
async function handleLoad(m: GmailMessage) {
setLoadingDetail(m.id);
try {
let body = m.body_text || m.body_html || '';
// If the list endpoint didn't include the full body, fetch detail.
if (!body.trim()) {
const res = await fetch(`/api/gmail/messages/${m.id}`);
if (res.ok) {
const data = await res.json();
body = data.message?.body_html || data.message?.body_text || data.body_html || data.body_text || '';
}
}
if (!body.trim()) {
addToast('This email has no body content to analyze', 'error');
return;
}
onLoadIntoAnalyzer({
subject: m.subject || '(no subject)',
body,
from: m.from_name ? `${m.from_name} <${m.from_address || ''}>` : (m.from_address || 'unknown'),
});
addToast(`Loaded "${m.subject?.slice(0, 40) || 'email'}" into analyzer`, 'success');
} catch {
addToast('Failed to load email body', 'error');
} finally {
setLoadingDetail(null);
}
}
function timeAgo(iso: string | null): string {
if (!iso) return '';
const diff = Date.now() - new Date(iso).getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days < 1) return 'today';
if (days < 7) return `${days}d ago`;
if (days < 31) return `${Math.floor(days / 7)}w ago`;
if (days < 365) return `${Math.floor(days / 30)}mo ago`;
return `${Math.floor(days / 365)}y ago`;
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ position: 'relative' }}>
<Search
size={13}
style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--color-text-muted)' }}
/>
<input
type="text"
className="input"
style={{ width: '100%', paddingLeft: 30, fontSize: 13 }}
placeholder="Search SDCC emails... (e.g., 'SAVE plan', 'newsletter')"
value={q}
onChange={(e) => setQ(e.target.value)}
/>
</div>
{loading ? (
<div style={{ padding: '18px 10px', textAlign: 'center', fontSize: 12, color: 'var(--color-text-muted)' }}>
Loading SDCC archive...
</div>
) : messages.length === 0 ? (
<div style={{ padding: '18px 10px', textAlign: 'center', fontSize: 12, color: 'var(--color-text-muted)' }}>
<Inbox size={18} style={{ opacity: 0.4, marginBottom: 4 }} />
<div>No emails found{q ? ` matching "${q}"` : ''}.</div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, maxHeight: 520, overflowY: 'auto' }}>
{messages.map((m) => (
<div
key={m.id}
style={{
padding: 8,
border: '1px solid var(--color-border)',
borderRadius: 8,
background: m.is_read ? 'var(--color-surface)' : 'var(--color-surface-el)',
cursor: 'pointer',
transition: 'var(--transition)',
}}
onClick={() => { void handleLoad(m); }}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
void handleLoad(m);
}
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 }}>
<span
style={{
fontSize: 13,
fontWeight: m.is_read ? 500 : 700,
color: 'var(--color-text)',
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={m.subject || ''}
>
{m.subject || '(no subject)'}
</span>
<span style={{ fontSize: 10, color: 'var(--color-text-muted)', flexShrink: 0, display: 'flex', alignItems: 'center', gap: 3 }}>
<Calendar size={10} />
{timeAgo(m.received_at)}
</span>
</div>
<div
style={{
fontSize: 11,
color: 'var(--color-text-secondary)',
display: 'flex',
alignItems: 'center',
gap: 4,
marginBottom: 4,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
<User size={10} style={{ color: 'var(--color-text-muted)', flexShrink: 0 }} />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
{m.from_name || m.from_address || 'unknown'}
</span>
</div>
<div
style={{
fontSize: 11,
color: 'var(--color-text-muted)',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
lineHeight: 1.35,
marginBottom: 4,
}}
>
{(m.body_text || '').slice(0, 200)}
</div>
<button
type="button"
className="btn btn-ghost btn-sm"
style={{
gap: 3,
fontSize: 11,
padding: '2px 8px',
minHeight: 24,
color: 'var(--color-primary)',
}}
onClick={(e) => { e.stopPropagation(); void handleLoad(m); }}
disabled={loadingDetail === m.id}
>
<Play size={10} />
{loadingDetail === m.id ? 'Loading...' : 'Analyze this email'}
</button>
</div>
))}
</div>
)}
</div>
);
}