"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(3); // Phase → minimum progress threshold (never go below these) const PHASE_MIN: Record = { scraping: 3, enriching: 35, emails: 60, done: 100, }; const PHASE_MAX: Record = { scraping: 34, enriching: 59, emails: 88, done: 100, }; useEffect(() => { let cancelled = false; let crawlInterval: ReturnType | null = null; let pollTimeout: ReturnType | null = null; // Slowly creep forward — never backwards crawlInterval = setInterval(() => { if (cancelled) return; setProgressWidth(prev => { if (prev >= 88) return prev; // hard cap while loading return prev + 0.4; // ~0.4% per 200ms → ~2min to reach 88% }); }, 200); 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); const phase = getPhase(data); // Advance to at least the phase minimum — never go backwards setProgressWidth(prev => Math.max(prev, PHASE_MIN[phase])); if (data.status === "complete") { if (crawlInterval) clearInterval(crawlInterval); setProgressWidth(100); setTimeout(() => { if (!cancelled) onDone(data.totalLeads); }, 800); } else if (data.status === "failed") { if (crawlInterval) clearInterval(crawlInterval); onError(); } else { // Cap crawl at phase max while in that phase setProgressWidth(prev => Math.min(prev, PHASE_MAX[phase])); pollTimeout = setTimeout(poll, 2500); } } } catch { if (!cancelled) { pollTimeout = setTimeout(poll, 3000); } } } poll(); return () => { cancelled = true; if (crawlInterval) clearInterval(crawlInterval); 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.
); }