Files
lead-scraper/components/shared/ProgressCard.tsx
Timo Uttenweiler 115cdacd08 UI improvements: Leadspeicher, Maps enrichment, exports
- Rename LeadVault → Leadspeicher throughout (sidebar, topbar, page)
- SidePanel: full lead detail view with contact, source, tags (read-only), Google Maps link for address
- Tags: kontaktiert stored as tag (toggleable), favorit tag toggle
- Remove Status column, StatusBadge dropdown, Priority feature
- Remove Aktualisieren button from Leadspeicher
- Bulk actions: remove status dropdown
- Export: LeadVault Excel-only, clean columns, freeze row + autofilter
- Export dropdown: click-based (fix overflow-hidden clipping)
- ExportButtons: remove CSV, Excel only everywhere
- Maps page: post-search Anymailfinder enrichment button
- ProgressCard: "Suche läuft..." instead of "Warte auf Anymailfinder-Server..."
- Quick SERP renamed to "Schnell neue Suche"
- Results page: Excel export, always-enabled download button
- Anymailfinder: fix bulk field names, array-of-arrays format
- Apify: fix countryCode lowercase
- API: sourceTerm search, contacted/favorite tag filters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 18:12:31 +01:00

92 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { cn } from "@/lib/utils";
interface ProgressCardProps {
title: string;
current: number;
total: number;
subtitle?: string;
status?: "running" | "complete" | "failed" | "idle";
}
export function ProgressCard({ title, current, total, subtitle, status = "running" }: ProgressCardProps) {
const pct = total > 0 ? Math.round((current / total) * 100) : 0;
// Indeterminate = läuft aber noch kein Fortschritt (z.B. warten auf Anymailfinder Bulk)
const indeterminate = status === "running" && current === 0 && total > 0;
return (
<div className="bg-[#111118] border border-[#1e1e2e] rounded-xl p-5 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-white">{title}</h3>
{subtitle && <p className="text-xs text-gray-500 mt-0.5">{subtitle}</p>}
</div>
<StatusBadge status={status} />
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs text-gray-400">
{indeterminate ? (
<>
<span className="flex items-center gap-1.5">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: "0ms" }} />
<span className="inline-block w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: "150ms" }} />
<span className="inline-block w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: "300ms" }} />
<span className="text-blue-300 ml-1">Suche läuft...</span>
</span>
<span className="text-gray-600">{total} Domains</span>
</>
) : (
<>
<span>{current.toLocaleString()} / {total.toLocaleString()}</span>
<span>{pct}%</span>
</>
)}
</div>
<div className="h-2 bg-[#1e1e2e] rounded-full overflow-hidden relative">
{indeterminate ? (
<div className="absolute inset-0 overflow-hidden rounded-full">
<div className="h-full w-1/3 bg-gradient-to-r from-transparent via-blue-500 to-transparent animate-[shimmer_1.5s_ease-in-out_infinite] rounded-full" />
</div>
) : (
<div
className={cn(
"h-full rounded-full transition-all duration-500",
status === "failed"
? "bg-red-500"
: status === "complete"
? "bg-green-500"
: "bg-gradient-to-r from-blue-500 to-purple-600"
)}
style={{ width: `${Math.max(pct, status === "running" ? 2 : 0)}%` }}
/>
)}
</div>
{indeterminate && (
<p className="text-[11px] text-gray-600">
Batch-Verarbeitung läuft auf Anymailfinder-Servern. Dauert ca. 13 Min. für {total} Domains.
</p>
)}
</div>
</div>
);
}
export function StatusBadge({ status }: { status: string }) {
const config: Record<string, { label: string; color: string; dot: string }> = {
running: { label: "Läuft", color: "bg-blue-500/10 text-blue-400 border-blue-500/20", dot: "bg-blue-400 animate-pulse" },
complete: { label: "Abgeschlossen", color: "bg-green-500/10 text-green-400 border-green-500/20", dot: "bg-green-400" },
failed: { label: "Fehlgeschlagen", color: "bg-red-500/10 text-red-400 border-red-500/20", dot: "bg-red-400" },
pending: { label: "Ausstehend", color: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20", dot: "bg-yellow-400" },
idle: { label: "Bereit", color: "bg-gray-500/10 text-gray-400 border-gray-500/20", dot: "bg-gray-400" },
};
const c = config[status] || config.idle;
return (
<span className={`inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium border ${c.color}`}>
<span className={`w-1.5 h-1.5 rounded-full ${c.dot}`} />
{c.label}
</span>
);
}