"use client"; import { useEffect, useState } from "react"; type Phase = "scraping" | "enriching" | "emails" | "done"; interface JobStatus { status: "running" | "complete" | "failed"; totalLeads: number; emailsFound: number; } interface LoadingCardProps { jobId: string; onDone: (totalFound: number) => void; onError: () => void; } const STEPS = [ { id: "scraping" as Phase, label: "Unternehmen gefunden" }, { id: "enriching" as Phase, label: "Kontakte ermitteln" }, { id: "emails" as Phase, label: "E-Mails suchen" }, { id: "done" as Phase, label: "Fertig" }, ]; function getPhase(s: JobStatus): Phase { if (s.status === "complete") return "done"; if (s.emailsFound > 0) return "emails"; if (s.totalLeads > 0) return "enriching"; return "scraping"; } function stepState(step: Phase, currentPhase: Phase): "done" | "active" | "pending" { const order: Phase[] = ["scraping", "enriching", "emails", "done"]; const stepIdx = order.indexOf(step); const currentIdx = order.indexOf(currentPhase); if (stepIdx < currentIdx) return "done"; if (stepIdx === currentIdx) return "active"; return "pending"; } export function LoadingCard({ jobId, onDone, onError }: LoadingCardProps) { const [jobStatus, setJobStatus] = useState({ status: "running", totalLeads: 0, emailsFound: 0, }); const [progressWidth, setProgressWidth] = useState(40); useEffect(() => { let cancelled = false; let pendulumInterval: ReturnType | null = null; let pollTimeout: ReturnType | null = null; // Pendulum animation while running let goingUp = true; pendulumInterval = setInterval(() => { if (cancelled) return; setProgressWidth((prev) => { if (goingUp) { if (prev >= 85) { goingUp = false; return 84; } return prev + 1; } else { if (prev <= 40) { goingUp = true; return 41; } return prev - 1; } }); }, 120); async function poll() { if (cancelled) return; try { const res = await fetch(`/api/jobs/${jobId}/status`); if (!res.ok) throw new Error("fetch failed"); const data = await res.json() as JobStatus; if (!cancelled) { setJobStatus(data); if (data.status === "complete") { if (pendulumInterval) clearInterval(pendulumInterval); setProgressWidth(100); setTimeout(() => { if (!cancelled) onDone(data.totalLeads); }, 800); } else if (data.status === "failed") { if (pendulumInterval) clearInterval(pendulumInterval); onError(); } else { pollTimeout = setTimeout(poll, 2500); } } } catch { if (!cancelled) { pollTimeout = setTimeout(poll, 3000); } } } poll(); return () => { cancelled = true; if (pendulumInterval) clearInterval(pendulumInterval); if (pollTimeout) clearTimeout(pollTimeout); }; }, [jobId, onDone, onError]); const phase = getPhase(jobStatus); return (
{/* Header */}
Ergebnisse werden gesucht {jobStatus.totalLeads > 0 || jobStatus.emailsFound > 0 ? `${jobStatus.emailsFound} von ${jobStatus.totalLeads} gefunden` : "Wird gestartet…"}
{/* Progress bar */}
{/* Steps */}
{STEPS.map((step) => { const state = stepState(step.id, phase); return (
{step.label}
); })}
{/* Email note */} {phase === "emails" && (

E-Mail-Suche kann einen Moment dauern.

)} {/* Warning banner */}
⚠️ Bitte diesen Tab nicht schließen, während die Suche läuft.
); }